| import Graph, { UndirectedGraph } from 'graphology' |
| import { useCallback, useEffect, useRef } from 'react' |
| import { useTranslation } from 'react-i18next' |
| import { errorMessage } from '@/lib/utils' |
| import * as Constants from '@/lib/constants' |
| import { useGraphStore, RawGraph, RawNodeType, RawEdgeType } from '@/stores/graph' |
| import { toast } from 'sonner' |
| import { queryGraphs } from '@/api/lightrag' |
| import { useBackendState } from '@/stores/state' |
| import { useSettingsStore } from '@/stores/settings' |
|
|
| import seedrandom from 'seedrandom' |
| import { resolveNodeColor, DEFAULT_NODE_COLOR } from '@/utils/graphColor' |
|
|
| |
| const getNodeColorByType = (nodeType: string | undefined): string => { |
| const state = useGraphStore.getState() |
| const { color, map, updated } = resolveNodeColor(nodeType, state.typeColorMap) |
|
|
| if (updated) { |
| useGraphStore.setState({ typeColorMap: map }) |
| } |
|
|
| return color || DEFAULT_NODE_COLOR |
| }; |
|
|
|
|
| const validateGraph = (graph: RawGraph) => { |
| |
| if (!graph) { |
| console.log('Graph validation failed: graph is null'); |
| return false; |
| } |
|
|
| |
| if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) { |
| console.log('Graph validation failed: nodes or edges is not an array'); |
| return false; |
| } |
|
|
| |
| if (graph.nodes.length === 0) { |
| console.log('Graph validation failed: nodes array is empty'); |
| return false; |
| } |
|
|
| |
| for (const node of graph.nodes) { |
| if (!node.id || !node.labels || !node.properties) { |
| console.log('Graph validation failed: invalid node structure'); |
| return false; |
| } |
| } |
|
|
| |
| for (const edge of graph.edges) { |
| if (!edge.id || !edge.source || !edge.target) { |
| console.log('Graph validation failed: invalid edge structure'); |
| return false; |
| } |
| } |
|
|
| |
| for (const edge of graph.edges) { |
| const source = graph.getNode(edge.source); |
| const target = graph.getNode(edge.target); |
| if (source == undefined || target == undefined) { |
| console.log('Graph validation failed: edge references non-existent node'); |
| return false; |
| } |
| } |
|
|
| console.log('Graph validation passed'); |
| return true; |
| } |
|
|
| export type NodeType = { |
| x: number |
| y: number |
| label: string |
| size: number |
| color: string |
| highlighted?: boolean |
| } |
| export type EdgeType = { |
| label: string |
| originalWeight?: number |
| size?: number |
| color?: string |
| hidden?: boolean |
| } |
|
|
| const fetchGraph = async (label: string, maxDepth: number, maxNodes: number) => { |
| let rawData: any; |
|
|
| |
| |
| useGraphStore.getState().setLabelsFetchAttempted(true) |
|
|
| |
| const queryLabel = label || '*'; |
|
|
| try { |
| console.log(`Fetching graph label: ${queryLabel}, depth: ${maxDepth}, nodes: ${maxNodes}`); |
| rawData = await queryGraphs(queryLabel, maxDepth, maxNodes); |
| } catch (e) { |
| useBackendState.getState().setErrorMessage(errorMessage(e), 'Query Graphs Error!'); |
| return null; |
| } |
|
|
| let rawGraph = null; |
|
|
| if (rawData) { |
| const nodeIdMap: Record<string, number> = {} |
| const edgeIdMap: Record<string, number> = {} |
|
|
| for (let i = 0; i < rawData.nodes.length; i++) { |
| const node = rawData.nodes[i] |
| nodeIdMap[node.id] = i |
|
|
| node.x = Math.random() |
| node.y = Math.random() |
| node.degree = 0 |
| node.size = 10 |
| } |
|
|
| for (let i = 0; i < rawData.edges.length; i++) { |
| const edge = rawData.edges[i] |
| edgeIdMap[edge.id] = i |
|
|
| const source = nodeIdMap[edge.source] |
| const target = nodeIdMap[edge.target] |
| if (source !== undefined && target !== undefined) { |
| const sourceNode = rawData.nodes[source] |
| if (!sourceNode) { |
| console.error(`Source node ${edge.source} is undefined`) |
| continue |
| } |
|
|
| const targetNode = rawData.nodes[target] |
| if (!targetNode) { |
| console.error(`Target node ${edge.target} is undefined`) |
| continue |
| } |
| sourceNode.degree += 1 |
| targetNode.degree += 1 |
| } |
| } |
|
|
| |
| let minDegree = Number.MAX_SAFE_INTEGER |
| let maxDegree = 0 |
|
|
| for (const node of rawData.nodes) { |
| minDegree = Math.min(minDegree, node.degree) |
| maxDegree = Math.max(maxDegree, node.degree) |
| } |
| const range = maxDegree - minDegree |
| if (range > 0) { |
| const scale = Constants.maxNodeSize - Constants.minNodeSize |
| for (const node of rawData.nodes) { |
| node.size = Math.round( |
| Constants.minNodeSize + scale * Math.pow((node.degree - minDegree) / range, 0.5) |
| ) |
| } |
| } |
|
|
| rawGraph = new RawGraph() |
| rawGraph.nodes = rawData.nodes |
| rawGraph.edges = rawData.edges |
| rawGraph.nodeIdMap = nodeIdMap |
| rawGraph.edgeIdMap = edgeIdMap |
|
|
| if (!validateGraph(rawGraph)) { |
| rawGraph = null |
| console.warn('Invalid graph data') |
| } |
| console.log('Graph data loaded') |
| } |
|
|
| |
| return { rawGraph, is_truncated: rawData.is_truncated } |
| } |
|
|
| |
| const createSigmaGraph = (rawGraph: RawGraph | null) => { |
| |
| const minEdgeSize = useSettingsStore.getState().minEdgeSize |
| const maxEdgeSize = useSettingsStore.getState().maxEdgeSize |
| |
| if (!rawGraph || !rawGraph.nodes.length) { |
| console.log('No graph data available, skipping sigma graph creation'); |
| return null; |
| } |
|
|
| |
| const graph = new UndirectedGraph() |
|
|
| |
| for (const rawNode of rawGraph?.nodes ?? []) { |
| |
| seedrandom(rawNode.id + Date.now().toString(), { global: true }) |
| const x = Math.random() |
| const y = Math.random() |
|
|
| graph.addNode(rawNode.id, { |
| label: rawNode.labels.join(', '), |
| color: rawNode.color, |
| x: x, |
| y: y, |
| size: rawNode.size, |
| |
| borderColor: Constants.nodeBorderColor, |
| borderSize: 0.2 |
| }) |
| } |
|
|
| |
| for (const rawEdge of rawGraph?.edges ?? []) { |
| |
| const weight = rawEdge.properties?.weight !== undefined ? Number(rawEdge.properties.weight) : 1 |
|
|
| rawEdge.dynamicId = graph.addEdge(rawEdge.source, rawEdge.target, { |
| label: rawEdge.properties?.keywords || undefined, |
| size: weight, |
| originalWeight: weight, |
| type: 'curvedNoArrow' |
| }) |
| } |
|
|
| |
| let minWeight = Number.MAX_SAFE_INTEGER |
| let maxWeight = 0 |
|
|
| |
| graph.forEachEdge(edge => { |
| const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1 |
| minWeight = Math.min(minWeight, weight) |
| maxWeight = Math.max(maxWeight, weight) |
| }) |
|
|
| |
| const weightRange = maxWeight - minWeight |
| if (weightRange > 0) { |
| const sizeScale = maxEdgeSize - minEdgeSize |
| graph.forEachEdge(edge => { |
| const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1 |
| const scaledSize = minEdgeSize + sizeScale * Math.pow((weight - minWeight) / weightRange, 0.5) |
| graph.setEdgeAttribute(edge, 'size', scaledSize) |
| }) |
| } else { |
| |
| graph.forEachEdge(edge => { |
| graph.setEdgeAttribute(edge, 'size', minEdgeSize) |
| }) |
| } |
|
|
| return graph |
| } |
|
|
| const useLightrangeGraph = () => { |
| const { t } = useTranslation() |
| const queryLabel = useSettingsStore.use.queryLabel() |
| const rawGraph = useGraphStore.use.rawGraph() |
| const sigmaGraph = useGraphStore.use.sigmaGraph() |
| const maxQueryDepth = useSettingsStore.use.graphQueryMaxDepth() |
| const maxNodes = useSettingsStore.use.graphMaxNodes() |
| const isFetching = useGraphStore.use.isFetching() |
| const nodeToExpand = useGraphStore.use.nodeToExpand() |
| const nodeToPrune = useGraphStore.use.nodeToPrune() |
| const graphDataVersion = useGraphStore.use.graphDataVersion() |
|
|
|
|
| |
| const dataLoadedRef = useRef(false) |
| const initialLoadRef = useRef(false) |
| |
| const emptyDataHandledRef = useRef(false) |
|
|
| const getNode = useCallback( |
| (nodeId: string) => { |
| return rawGraph?.getNode(nodeId) || null |
| }, |
| [rawGraph] |
| ) |
|
|
| const getEdge = useCallback( |
| (edgeId: string, dynamicId: boolean = true) => { |
| return rawGraph?.getEdge(edgeId, dynamicId) || null |
| }, |
| [rawGraph] |
| ) |
|
|
| |
| const fetchInProgressRef = useRef(false) |
|
|
| |
| useEffect(() => { |
| if (!queryLabel && (rawGraph !== null || sigmaGraph !== null)) { |
| const state = useGraphStore.getState() |
| state.reset() |
| state.setGraphDataFetchAttempted(false) |
| state.setLabelsFetchAttempted(false) |
| dataLoadedRef.current = false |
| initialLoadRef.current = false |
| } |
| }, [queryLabel, rawGraph, sigmaGraph]) |
|
|
| |
| useEffect(() => { |
| |
| if (fetchInProgressRef.current) { |
| return |
| } |
|
|
| |
| if (!queryLabel && emptyDataHandledRef.current) { |
| return; |
| } |
|
|
| |
| |
| if (!isFetching && !useGraphStore.getState().graphDataFetchAttempted) { |
| |
| fetchInProgressRef.current = true |
| useGraphStore.getState().setGraphDataFetchAttempted(true) |
|
|
| const state = useGraphStore.getState() |
| state.setIsFetching(true) |
|
|
| |
| state.clearSelection() |
| if (state.sigmaGraph) { |
| state.sigmaGraph.forEachNode((node) => { |
| state.sigmaGraph?.setNodeAttribute(node, 'highlighted', false) |
| }) |
| } |
|
|
| console.log('Preparing graph data...') |
|
|
| |
| const currentQueryLabel = queryLabel |
| const currentMaxQueryDepth = maxQueryDepth |
| const currentMaxNodes = maxNodes |
|
|
| |
| let dataPromise: Promise<{ rawGraph: RawGraph | null; is_truncated: boolean | undefined } | null>; |
|
|
| |
| if (currentQueryLabel) { |
| dataPromise = fetchGraph(currentQueryLabel, currentMaxQueryDepth, currentMaxNodes); |
| } else { |
| |
| console.log('Query label is empty, show empty graph') |
| dataPromise = Promise.resolve({ rawGraph: null, is_truncated: false }); |
| } |
|
|
| |
| dataPromise.then((result) => { |
| const state = useGraphStore.getState() |
| const data = result?.rawGraph; |
|
|
| |
| if (data && data.nodes) { |
| data.nodes.forEach(node => { |
| |
| const nodeEntityType = node.properties?.entity_type as string | undefined; |
| node.color = getNodeColorByType(nodeEntityType); |
| }); |
| } |
|
|
| if (result?.is_truncated) { |
| toast.info(t('graphPanel.dataIsTruncated', 'Graph data is truncated to Max Nodes')); |
| } |
|
|
| |
| state.reset() |
|
|
| |
| if (!data || !data.nodes || data.nodes.length === 0) { |
| |
| const emptyGraph = new UndirectedGraph(); |
|
|
| |
| emptyGraph.addNode('empty-graph-node', { |
| label: t('graphPanel.emptyGraph'), |
| color: '#5D6D7E', |
| x: 0.5, |
| y: 0.5, |
| size: 15, |
| borderColor: Constants.nodeBorderColor, |
| borderSize: 0.2 |
| }); |
|
|
| |
| state.setSigmaGraph(emptyGraph); |
| state.setRawGraph(null); |
|
|
| |
| state.setGraphIsEmpty(true); |
|
|
| |
| const errorMessage = useBackendState.getState().message; |
| const isAuthError = errorMessage && errorMessage.includes('Authentication required'); |
|
|
| |
| if (!isAuthError && currentQueryLabel) { |
| useSettingsStore.getState().setQueryLabel(''); |
| } |
|
|
| |
| if (!isAuthError) { |
| state.setLastSuccessfulQueryLabel(''); |
| } else { |
| console.log('Keep queryLabel for post-login reload'); |
| } |
|
|
| console.log(`Graph data is empty, created graph with empty graph node. Auth error: ${isAuthError}`); |
| } else { |
| |
| const newSigmaGraph = createSigmaGraph(data); |
| data.buildDynamicMap(); |
|
|
| |
| state.setSigmaGraph(newSigmaGraph); |
| state.setRawGraph(data); |
| state.setGraphIsEmpty(false); |
|
|
| |
| state.setLastSuccessfulQueryLabel(currentQueryLabel); |
|
|
| |
| state.setMoveToSelectedNode(true); |
| } |
|
|
| |
| dataLoadedRef.current = true |
| initialLoadRef.current = true |
| fetchInProgressRef.current = false |
| state.setIsFetching(false) |
|
|
| |
| if ((!data || !data.nodes || data.nodes.length === 0) && !currentQueryLabel) { |
| emptyDataHandledRef.current = true; |
| } |
| }).catch((error) => { |
| console.error('Error fetching graph data:', error) |
|
|
| |
| const state = useGraphStore.getState() |
| state.setIsFetching(false) |
| dataLoadedRef.current = false; |
| fetchInProgressRef.current = false |
| state.setGraphDataFetchAttempted(false) |
| state.setLastSuccessfulQueryLabel('') |
| }) |
| } |
| }, [queryLabel, maxQueryDepth, maxNodes, isFetching, t, graphDataVersion]) |
|
|
| |
| useEffect(() => { |
| const nodeId = useGraphStore.getState().nodeToExpand; |
| if (!nodeId) return; |
|
|
| const handleNodeExpand = async () => { |
| const { sigmaGraph, rawGraph } = useGraphStore.getState(); |
| if (!sigmaGraph || !rawGraph) return; |
|
|
| try { |
| |
| const nodeToExpand = rawGraph.getNode(nodeId); |
| if (!nodeToExpand) { |
| console.error('Node not found:', nodeId); |
| return; |
| } |
|
|
| |
| const label = nodeToExpand.labels[0]; |
| if (!label) { |
| console.error('Node has no label:', nodeId); |
| return; |
| } |
|
|
| |
| const extendedGraph = await queryGraphs(label, 2, 1000); |
|
|
| if (!extendedGraph || !extendedGraph.nodes || !extendedGraph.edges) { |
| console.error('Failed to fetch extended graph'); |
| return; |
| } |
|
|
| |
| const processedNodes: RawNodeType[] = []; |
| for (const node of extendedGraph.nodes) { |
| |
| seedrandom(node.id, { global: true }); |
| const nodeEntityType = node.properties?.entity_type as string | undefined; |
| const color = getNodeColorByType(nodeEntityType); |
|
|
| |
| processedNodes.push({ |
| id: node.id, |
| labels: node.labels, |
| properties: node.properties, |
| size: 10, |
| x: Math.random(), |
| y: Math.random(), |
| color: color, |
| degree: 0 |
| }); |
| } |
|
|
| |
| const processedEdges: RawEdgeType[] = []; |
| for (const edge of extendedGraph.edges) { |
| |
| processedEdges.push({ |
| id: edge.id, |
| source: edge.source, |
| target: edge.target, |
| type: edge.type, |
| properties: edge.properties, |
| dynamicId: '' |
| }); |
| } |
|
|
| |
| const nodePositions: Record<string, {x: number, y: number}> = {}; |
| sigmaGraph.forEachNode((node) => { |
| nodePositions[node] = { |
| x: sigmaGraph.getNodeAttribute(node, 'x'), |
| y: sigmaGraph.getNodeAttribute(node, 'y') |
| }; |
| }); |
|
|
| |
| const existingNodeIds = new Set(sigmaGraph.nodes()); |
|
|
| |
| const nodesToAdd = new Set<string>(); |
| const edgesToAdd = new Set<string>(); |
|
|
| |
| const minDegree = 1; |
| let maxDegree = 0; |
|
|
| |
| let minWeight = Number.MAX_SAFE_INTEGER; |
| let maxWeight = 0; |
|
|
| |
| sigmaGraph.forEachNode(node => { |
| const degree = sigmaGraph.degree(node); |
| maxDegree = Math.max(maxDegree, degree); |
| }); |
|
|
| |
| sigmaGraph.forEachEdge(edge => { |
| const weight = sigmaGraph.getEdgeAttribute(edge, 'originalWeight') || 1; |
| minWeight = Math.min(minWeight, weight); |
| maxWeight = Math.max(maxWeight, weight); |
| }); |
|
|
| |
| for (const node of processedNodes) { |
| |
| if (existingNodeIds.has(node.id)) { |
| continue; |
| } |
|
|
| |
| const isConnected = processedEdges.some( |
| edge => (edge.source === nodeId && edge.target === node.id) || |
| (edge.target === nodeId && edge.source === node.id) |
| ); |
|
|
| if (isConnected) { |
| nodesToAdd.add(node.id); |
| } |
| } |
|
|
| |
| const nodeDegrees = new Map<string, number>(); |
| const existingNodeDegreeIncrements = new Map<string, number>(); |
| const nodesWithDiscardedEdges = new Set<string>(); |
|
|
| for (const edge of processedEdges) { |
| const sourceExists = existingNodeIds.has(edge.source) || nodesToAdd.has(edge.source); |
| const targetExists = existingNodeIds.has(edge.target) || nodesToAdd.has(edge.target); |
|
|
| if (sourceExists && targetExists) { |
| edgesToAdd.add(edge.id); |
| |
| if (nodesToAdd.has(edge.source)) { |
| nodeDegrees.set(edge.source, (nodeDegrees.get(edge.source) || 0) + 1); |
| } else if (existingNodeIds.has(edge.source)) { |
| |
| existingNodeDegreeIncrements.set(edge.source, (existingNodeDegreeIncrements.get(edge.source) || 0) + 1); |
| } |
|
|
| if (nodesToAdd.has(edge.target)) { |
| nodeDegrees.set(edge.target, (nodeDegrees.get(edge.target) || 0) + 1); |
| } else if (existingNodeIds.has(edge.target)) { |
| |
| existingNodeDegreeIncrements.set(edge.target, (existingNodeDegreeIncrements.get(edge.target) || 0) + 1); |
| } |
| } else { |
| |
| if (sigmaGraph.hasNode(edge.source)) { |
| nodesWithDiscardedEdges.add(edge.source); |
| } else if (nodesToAdd.has(edge.source)) { |
| nodesWithDiscardedEdges.add(edge.source); |
| nodeDegrees.set(edge.source, (nodeDegrees.get(edge.source) || 0) + 1); |
| } |
| if (sigmaGraph.hasNode(edge.target)) { |
| nodesWithDiscardedEdges.add(edge.target); |
| } else if (nodesToAdd.has(edge.target)) { |
| nodesWithDiscardedEdges.add(edge.target); |
| nodeDegrees.set(edge.target, (nodeDegrees.get(edge.target) || 0) + 1); |
| } |
| } |
| } |
|
|
| |
| const updateNodeSizes = ( |
| sigmaGraph: UndirectedGraph, |
| nodesWithDiscardedEdges: Set<string>, |
| minDegree: number, |
| maxDegree: number |
| ) => { |
| |
| const range = maxDegree - minDegree || 1; |
| const scale = Constants.maxNodeSize - Constants.minNodeSize; |
|
|
| |
| for (const nodeId of nodesWithDiscardedEdges) { |
| if (sigmaGraph.hasNode(nodeId)) { |
| let newDegree = sigmaGraph.degree(nodeId); |
| newDegree += 1; |
| |
| const limitedDegree = Math.min(newDegree, maxDegree + 1); |
|
|
| const newSize = Math.round( |
| Constants.minNodeSize + scale * Math.pow((limitedDegree - minDegree) / range, 0.5) |
| ); |
|
|
| sigmaGraph.setNodeAttribute(nodeId, 'size', newSize); |
| } |
| } |
| }; |
|
|
| |
| const updateEdgeSizes = ( |
| sigmaGraph: UndirectedGraph, |
| minWeight: number, |
| maxWeight: number |
| ) => { |
| |
| const minEdgeSize = useSettingsStore.getState().minEdgeSize; |
| const maxEdgeSize = useSettingsStore.getState().maxEdgeSize; |
| const weightRange = maxWeight - minWeight || 1; |
| const sizeScale = maxEdgeSize - minEdgeSize; |
|
|
| sigmaGraph.forEachEdge(edge => { |
| const weight = sigmaGraph.getEdgeAttribute(edge, 'originalWeight') || 1; |
| const scaledSize = minEdgeSize + sizeScale * Math.pow((weight - minWeight) / weightRange, 0.5); |
| sigmaGraph.setEdgeAttribute(edge, 'size', scaledSize); |
| }); |
| }; |
|
|
| |
| if (nodesToAdd.size === 0) { |
| updateNodeSizes(sigmaGraph, nodesWithDiscardedEdges, minDegree, maxDegree); |
| toast.info(t('graphPanel.propertiesView.node.noNewNodes')); |
| return; |
| } |
|
|
| |
| |
| for (const [, degree] of nodeDegrees.entries()) { |
| maxDegree = Math.max(maxDegree, degree); |
| } |
|
|
| |
| for (const [nodeId, increment] of existingNodeDegreeIncrements.entries()) { |
| const currentDegree = sigmaGraph.degree(nodeId); |
| const projectedDegree = currentDegree + increment; |
| maxDegree = Math.max(maxDegree, projectedDegree); |
| } |
|
|
| const range = maxDegree - minDegree || 1; |
| const scale = Constants.maxNodeSize - Constants.minNodeSize; |
|
|
| |
| |
| const cameraRatio = useGraphStore.getState().sigmaInstance?.getCamera().ratio || 1; |
| const spreadFactor = Math.max( |
| Math.sqrt(nodeToExpand.size) * 4, |
| Math.sqrt(nodesToAdd.size) * 3 |
| ) / cameraRatio; |
| seedrandom(Date.now().toString(), { global: true }); |
| const randomAngle = Math.random() * 2 * Math.PI |
|
|
| console.log('nodeSize:', nodeToExpand.size, 'nodesToAdd:', nodesToAdd.size); |
| console.log('cameraRatio:', Math.round(cameraRatio*100)/100, 'spreadFactor:', Math.round(spreadFactor*100)/100); |
|
|
| |
| for (const nodeId of nodesToAdd) { |
| const newNode = processedNodes.find(n => n.id === nodeId)!; |
| const nodeDegree = nodeDegrees.get(nodeId) || 0; |
|
|
| |
| |
| const limitedDegree = Math.min(nodeDegree, maxDegree + 1); |
| const nodeSize = Math.round( |
| Constants.minNodeSize + scale * Math.pow((limitedDegree - minDegree) / range, 0.5) |
| ); |
|
|
| |
| const angle = 2 * Math.PI * (Array.from(nodesToAdd).indexOf(nodeId) / nodesToAdd.size); |
|
|
| |
| const x = nodePositions[nodeId]?.x || |
| (nodePositions[nodeToExpand.id].x + Math.cos(randomAngle + angle) * spreadFactor); |
| const y = nodePositions[nodeId]?.y || |
| (nodePositions[nodeToExpand.id].y + Math.sin(randomAngle + angle) * spreadFactor); |
|
|
| |
| sigmaGraph.addNode(nodeId, { |
| label: newNode.labels.join(', '), |
| color: newNode.color, |
| x: x, |
| y: y, |
| size: nodeSize, |
| borderColor: Constants.nodeBorderColor, |
| borderSize: 0.2 |
| }); |
|
|
| |
| if (!rawGraph.getNode(nodeId)) { |
| |
| newNode.size = nodeSize; |
| newNode.x = x; |
| newNode.y = y; |
| newNode.degree = nodeDegree; |
|
|
| |
| rawGraph.nodes.push(newNode); |
| |
| rawGraph.nodeIdMap[nodeId] = rawGraph.nodes.length - 1; |
| } |
| } |
|
|
| |
| for (const edgeId of edgesToAdd) { |
| const newEdge = processedEdges.find(e => e.id === edgeId)!; |
|
|
| |
| if (sigmaGraph.hasEdge(newEdge.source, newEdge.target)) { |
| continue; |
| } |
|
|
| |
| const weight = newEdge.properties?.weight !== undefined ? Number(newEdge.properties.weight) : 1; |
|
|
| |
| minWeight = Math.min(minWeight, weight); |
| maxWeight = Math.max(maxWeight, weight); |
|
|
| |
| newEdge.dynamicId = sigmaGraph.addEdge(newEdge.source, newEdge.target, { |
| label: newEdge.properties?.keywords || undefined, |
| size: weight, |
| originalWeight: weight, |
| type: 'curvedNoArrow' |
| }); |
|
|
| |
| if (!rawGraph.getEdge(newEdge.id, false)) { |
| |
| rawGraph.edges.push(newEdge); |
| |
| rawGraph.edgeIdMap[newEdge.id] = rawGraph.edges.length - 1; |
| |
| rawGraph.edgeDynamicIdMap[newEdge.dynamicId] = rawGraph.edges.length - 1; |
| } else { |
| console.error('Edge already exists in rawGraph:', newEdge.id); |
| } |
| } |
|
|
| |
| rawGraph.buildDynamicMap(); |
|
|
| |
| useGraphStore.getState().resetSearchEngine(); |
|
|
| |
| updateNodeSizes(sigmaGraph, nodesWithDiscardedEdges, minDegree, maxDegree); |
| updateEdgeSizes(sigmaGraph, minWeight, maxWeight); |
|
|
| |
| if (sigmaGraph.hasNode(nodeId)) { |
| const finalDegree = sigmaGraph.degree(nodeId); |
| const limitedDegree = Math.min(finalDegree, maxDegree + 1); |
| const newSize = Math.round( |
| Constants.minNodeSize + scale * Math.pow((limitedDegree - minDegree) / range, 0.5) |
| ); |
| sigmaGraph.setNodeAttribute(nodeId, 'size', newSize); |
| nodeToExpand.size = newSize; |
| nodeToExpand.degree = finalDegree; |
| } |
|
|
| } catch (error) { |
| console.error('Error expanding node:', error); |
| } |
| }; |
|
|
| handleNodeExpand(); |
| |
| window.setTimeout(() => { |
| useGraphStore.getState().triggerNodeExpand(null); |
| }, 0); |
| }, [nodeToExpand, sigmaGraph, rawGraph, t]); |
|
|
| |
| const getNodesThatWillBeDeleted = useCallback((nodeId: string, graph: UndirectedGraph) => { |
| const nodesToDelete = new Set<string>([nodeId]); |
|
|
| |
| graph.forEachNode((node) => { |
| if (node === nodeId) return; |
|
|
| |
| const neighbors = graph.neighbors(node); |
|
|
| |
| |
| if (neighbors.length === 1 && neighbors[0] === nodeId) { |
| nodesToDelete.add(node); |
| } |
| }); |
|
|
| return nodesToDelete; |
| }, []); |
|
|
| |
| useEffect(() => { |
| const nodeId = useGraphStore.getState().nodeToPrune; |
| if (!nodeId) return; |
|
|
| const handleNodePrune = () => { |
| const state = useGraphStore.getState(); |
| const { sigmaGraph, rawGraph } = state; |
| if (!sigmaGraph || !rawGraph) return; |
|
|
| try { |
|
|
| |
| if (!sigmaGraph.hasNode(nodeId)) { |
| console.error('Node not found:', nodeId); |
| return; |
| } |
|
|
| |
| const nodesToDelete = getNodesThatWillBeDeleted(nodeId, sigmaGraph); |
|
|
| |
| if (nodesToDelete.size === sigmaGraph.nodes().length) { |
| toast.error(t('graphPanel.propertiesView.node.deleteAllNodesError')); |
| return; |
| } |
|
|
| |
| state.clearSelection(); |
|
|
| |
| for (const nodeToDelete of nodesToDelete) { |
| |
| sigmaGraph.dropNode(nodeToDelete); |
|
|
| |
| const nodeIndex = rawGraph.nodeIdMap[nodeToDelete]; |
| if (nodeIndex !== undefined) { |
| |
| const edgesToRemove = rawGraph.edges.filter( |
| edge => edge.source === nodeToDelete || edge.target === nodeToDelete |
| ); |
|
|
| |
| for (const edge of edgesToRemove) { |
| const edgeIndex = rawGraph.edgeIdMap[edge.id]; |
| if (edgeIndex !== undefined) { |
| |
| rawGraph.edges.splice(edgeIndex, 1); |
| |
| for (const [id, idx] of Object.entries(rawGraph.edgeIdMap)) { |
| if (idx > edgeIndex) { |
| rawGraph.edgeIdMap[id] = idx - 1; |
| } |
| } |
| |
| delete rawGraph.edgeIdMap[edge.id]; |
| |
| delete rawGraph.edgeDynamicIdMap[edge.dynamicId]; |
| } |
| } |
|
|
| |
| rawGraph.nodes.splice(nodeIndex, 1); |
|
|
| |
| for (const [id, idx] of Object.entries(rawGraph.nodeIdMap)) { |
| if (idx > nodeIndex) { |
| rawGraph.nodeIdMap[id] = idx - 1; |
| } |
| } |
|
|
| |
| delete rawGraph.nodeIdMap[nodeToDelete]; |
| } |
| } |
|
|
| |
| rawGraph.buildDynamicMap(); |
|
|
| |
| useGraphStore.getState().resetSearchEngine(); |
|
|
| |
| if (nodesToDelete.size > 1) { |
| toast.info(t('graphPanel.propertiesView.node.nodesRemoved', { count: nodesToDelete.size })); |
| } |
|
|
|
|
| } catch (error) { |
| console.error('Error pruning node:', error); |
| } |
| }; |
|
|
| handleNodePrune(); |
| |
| window.setTimeout(() => { |
| useGraphStore.getState().triggerNodePrune(null); |
| }, 0); |
| }, [nodeToPrune, sigmaGraph, rawGraph, getNodesThatWillBeDeleted, t]); |
|
|
| const lightrageGraph = useCallback(() => { |
| |
| if (sigmaGraph) { |
| return sigmaGraph as Graph<NodeType, EdgeType> |
| } |
|
|
| |
| console.log('Creating new Sigma graph instance') |
| const graph = new UndirectedGraph() |
| useGraphStore.getState().setSigmaGraph(graph) |
| return graph as Graph<NodeType, EdgeType> |
| }, [sigmaGraph]) |
|
|
| return { lightrageGraph, getNode, getEdge } |
| } |
|
|
| export default useLightrangeGraph |
|
|