choizhang
fix(graph): Fixed the issue of incorrect handling of edges and nodes during node ID updates
7843329
import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core' | |
import { AbstractGraph } from 'graphology-types' | |
// import { useLayoutCircular } from '@react-sigma/layout-circular' | |
import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2' | |
import { useEffect } from 'react' | |
// import useRandomGraph, { EdgeType, NodeType } from '@/hooks/useRandomGraph' | |
import { EdgeType, NodeType } from '@/hooks/useLightragGraph' | |
import useTheme from '@/hooks/useTheme' | |
import * as Constants from '@/lib/constants' | |
import { useSettingsStore } from '@/stores/settings' | |
import { useGraphStore } from '@/stores/graph' | |
const isButtonPressed = (ev: MouseEvent | TouchEvent) => { | |
if (ev.type.startsWith('mouse')) { | |
if ((ev as MouseEvent).buttons !== 0) { | |
return true | |
} | |
} | |
return false | |
} | |
const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean }) => { | |
const sigma = useSigma<NodeType, EdgeType>() | |
const registerEvents = useRegisterEvents<NodeType, EdgeType>() | |
const setSettings = useSetSettings<NodeType, EdgeType>() | |
const maxIterations = useSettingsStore.use.graphLayoutMaxIterations() | |
const { assign: assignLayout } = useLayoutForceAtlas2({ | |
iterations: maxIterations | |
}) | |
const { theme } = useTheme() | |
const hideUnselectedEdges = useSettingsStore.use.enableHideUnselectedEdges() | |
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents() | |
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel() | |
const renderLabels = useSettingsStore.use.showNodeLabel() | |
const minEdgeSize = useSettingsStore.use.minEdgeSize() | |
const maxEdgeSize = useSettingsStore.use.maxEdgeSize() | |
const selectedNode = useGraphStore.use.selectedNode() | |
const focusedNode = useGraphStore.use.focusedNode() | |
const selectedEdge = useGraphStore.use.selectedEdge() | |
const focusedEdge = useGraphStore.use.focusedEdge() | |
const sigmaGraph = useGraphStore.use.sigmaGraph() | |
/** | |
* When component mount or maxIterations changes | |
* => ensure graph reference and apply layout | |
*/ | |
useEffect(() => { | |
if (sigmaGraph && sigma) { | |
// Ensure sigma binding to sigmaGraph | |
try { | |
if (typeof sigma.setGraph === 'function') { | |
sigma.setGraph(sigmaGraph as unknown as AbstractGraph<NodeType, EdgeType>); | |
console.log('Binding graph to sigma instance'); | |
} else { | |
(sigma as any).graph = sigmaGraph; | |
console.warn('Simgma missing setGraph function, set graph property directly'); | |
} | |
} catch (error) { | |
console.error('Error setting graph on sigma instance:', error); | |
} | |
assignLayout(); | |
console.log('Initial layout applied to graph'); | |
} | |
}, [sigma, sigmaGraph, assignLayout, maxIterations]) | |
/** | |
* Ensure the sigma instance is set in the store | |
* This provides a backup in case the instance wasn't set in GraphViewer | |
*/ | |
useEffect(() => { | |
if (sigma) { | |
// Double-check that the store has the sigma instance | |
const currentInstance = useGraphStore.getState().sigmaInstance; | |
if (!currentInstance) { | |
console.log('Setting sigma instance from GraphControl'); | |
useGraphStore.getState().setSigmaInstance(sigma); | |
} | |
} | |
}, [sigma]); | |
/** | |
* When component mount | |
* => register events | |
*/ | |
useEffect(() => { | |
const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } = | |
useGraphStore.getState() | |
// Define event types | |
type NodeEvent = { node: string; event: { original: MouseEvent | TouchEvent } } | |
type EdgeEvent = { edge: string; event: { original: MouseEvent | TouchEvent } } | |
// Register all events, but edge events will only be processed if enableEdgeEvents is true | |
const events: Record<string, any> = { | |
enterNode: (event: NodeEvent) => { | |
if (!isButtonPressed(event.event.original)) { | |
const graph = sigma.getGraph() | |
if (graph.hasNode(event.node)) { | |
setFocusedNode(event.node) | |
} | |
} | |
}, | |
leaveNode: (event: NodeEvent) => { | |
if (!isButtonPressed(event.event.original)) { | |
setFocusedNode(null) | |
} | |
}, | |
clickNode: (event: NodeEvent) => { | |
const graph = sigma.getGraph() | |
if (graph.hasNode(event.node)) { | |
setSelectedNode(event.node) | |
setSelectedEdge(null) | |
} | |
}, | |
clickStage: () => clearSelection() | |
} | |
// Only add edge event handlers if enableEdgeEvents is true | |
if (enableEdgeEvents) { | |
events.clickEdge = (event: EdgeEvent) => { | |
setSelectedEdge(event.edge) | |
setSelectedNode(null) | |
} | |
events.enterEdge = (event: EdgeEvent) => { | |
if (!isButtonPressed(event.event.original)) { | |
setFocusedEdge(event.edge) | |
} | |
} | |
events.leaveEdge = (event: EdgeEvent) => { | |
if (!isButtonPressed(event.event.original)) { | |
setFocusedEdge(null) | |
} | |
} | |
} | |
// Register the events | |
registerEvents(events) | |
}, [registerEvents, enableEdgeEvents]) | |
/** | |
* When edge size settings change, recalculate edge sizes and refresh the sigma instance | |
* to ensure changes take effect immediately | |
*/ | |
useEffect(() => { | |
if (sigma && sigmaGraph) { | |
// Get the graph from sigma | |
const graph = sigma.getGraph() | |
// Find min and max weight values | |
let minWeight = Number.MAX_SAFE_INTEGER | |
let maxWeight = 0 | |
graph.forEachEdge(edge => { | |
// Get original weight (before scaling) | |
const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1 | |
if (typeof weight === 'number') { | |
minWeight = Math.min(minWeight, weight) | |
maxWeight = Math.max(maxWeight, weight) | |
} | |
}) | |
// Scale edge sizes based on weight range and current min/max edge size settings | |
const weightRange = maxWeight - minWeight | |
if (weightRange > 0) { | |
const sizeScale = maxEdgeSize - minEdgeSize | |
graph.forEachEdge(edge => { | |
const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1 | |
if (typeof weight === 'number') { | |
const scaledSize = minEdgeSize + sizeScale * Math.pow((weight - minWeight) / weightRange, 0.5) | |
graph.setEdgeAttribute(edge, 'size', scaledSize) | |
} | |
}) | |
} else { | |
// If all weights are the same, use default size | |
graph.forEachEdge(edge => { | |
graph.setEdgeAttribute(edge, 'size', minEdgeSize) | |
}) | |
} | |
// Refresh the sigma instance to apply changes | |
sigma.refresh() | |
} | |
}, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize]) | |
/** | |
* When component mount or hovered node change | |
* => Setting the sigma reducers | |
*/ | |
useEffect(() => { | |
const isDarkTheme = theme === 'dark' | |
const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined | |
const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined | |
// Update all dynamic settings directly without recreating the sigma container | |
setSettings({ | |
// Update display settings | |
enableEdgeEvents, | |
renderEdgeLabels, | |
renderLabels, | |
// Node reducer for node appearance | |
nodeReducer: (node, data) => { | |
const graph = sigma.getGraph() | |
const newData: NodeType & { | |
labelColor?: string | |
borderColor?: string | |
} = { ...data, highlighted: data.highlighted || false, labelColor } | |
if (!disableHoverEffect) { | |
newData.highlighted = false | |
const _focusedNode = focusedNode || selectedNode | |
const _focusedEdge = focusedEdge || selectedEdge | |
if (_focusedNode && graph.hasNode(_focusedNode)) { | |
try { | |
if (node === _focusedNode || graph.neighbors(_focusedNode).includes(node)) { | |
newData.highlighted = true | |
if (node === selectedNode) { | |
newData.borderColor = Constants.nodeBorderColorSelected | |
} | |
} | |
} catch (error) { | |
console.error('Error in nodeReducer:', error); | |
} | |
} else if (_focusedEdge && graph.hasEdge(_focusedEdge)) { | |
if (graph.extremities(_focusedEdge).includes(node)) { | |
newData.highlighted = true | |
newData.size = 3 | |
} | |
} else { | |
return newData | |
} | |
if (newData.highlighted) { | |
if (isDarkTheme) { | |
newData.labelColor = Constants.LabelColorHighlightedDarkTheme | |
} | |
} else { | |
newData.color = Constants.nodeColorDisabled | |
} | |
} | |
return newData | |
}, | |
// Edge reducer for edge appearance | |
edgeReducer: (edge, data) => { | |
const graph = sigma.getGraph() | |
const newData = { ...data, hidden: false, labelColor, color: edgeColor } | |
if (!disableHoverEffect) { | |
const _focusedNode = focusedNode || selectedNode | |
if (_focusedNode && graph.hasNode(_focusedNode)) { | |
try { | |
if (hideUnselectedEdges) { | |
if (!graph.extremities(edge).includes(_focusedNode)) { | |
newData.hidden = true | |
} | |
} else { | |
if (graph.extremities(edge).includes(_focusedNode)) { | |
newData.color = Constants.edgeColorHighlighted | |
} | |
} | |
} catch (error) { | |
console.error('Error in edgeReducer:', error); | |
} | |
} else { | |
const _selectedEdge = selectedEdge && graph.hasEdge(selectedEdge) ? selectedEdge : null; | |
const _focusedEdge = focusedEdge && graph.hasEdge(focusedEdge) ? focusedEdge : null; | |
if (_selectedEdge || _focusedEdge) { | |
if (edge === _selectedEdge) { | |
newData.color = Constants.edgeColorSelected | |
} else if (edge === _focusedEdge) { | |
newData.color = Constants.edgeColorHighlighted | |
} else if (hideUnselectedEdges) { | |
newData.hidden = true | |
} | |
} | |
} | |
} | |
return newData | |
} | |
}) | |
}, [ | |
selectedNode, | |
focusedNode, | |
selectedEdge, | |
focusedEdge, | |
setSettings, | |
sigma, | |
disableHoverEffect, | |
theme, | |
hideUnselectedEdges, | |
enableEdgeEvents, | |
renderEdgeLabels, | |
renderLabels | |
]) | |
return null | |
} | |
export default GraphControl | |