| import { useRegisterEvents, useSetSettings, useSigma } from '@react-sigma/core' |
| import { AbstractGraph } from 'graphology-types' |
| |
| import { useLayoutForceAtlas2 } from '@react-sigma/layout-forceatlas2' |
| import { useEffect, useState } from 'react' |
|
|
| |
| 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() |
|
|
| |
| const [systemThemeIsDark, setSystemThemeIsDark] = useState(() => |
| window.matchMedia('(prefers-color-scheme: dark)').matches |
| ) |
|
|
| useEffect(() => { |
| if (theme === 'system') { |
| const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') |
| const handler = (e: MediaQueryListEvent) => setSystemThemeIsDark(e.matches) |
| mediaQuery.addEventListener('change', handler) |
| return () => mediaQuery.removeEventListener('change', handler) |
| } |
| }, [theme]) |
|
|
| |
| |
| |
| |
| useEffect(() => { |
| if (sigmaGraph && sigma) { |
| |
| try { |
| if (typeof sigma.setGraph === 'function') { |
| sigma.setGraph(sigmaGraph as unknown as AbstractGraph<NodeType, EdgeType>); |
| console.log('Binding graph to sigma instance'); |
| } else { |
| console.error('Sigma missing setGraph function — unexpected: sigma v3 should always have setGraph'); |
| } |
| } catch (error) { |
| console.error('Error setting graph on sigma instance:', error); |
| } |
|
|
| assignLayout(); |
| console.log('Initial layout applied to graph'); |
| } |
| }, [sigma, sigmaGraph, assignLayout, maxIterations]) |
|
|
| |
| |
| |
| |
| useEffect(() => { |
| if (sigma) { |
| |
| const currentInstance = useGraphStore.getState().sigmaInstance; |
| if (!currentInstance) { |
| console.log('Setting sigma instance from GraphControl'); |
| useGraphStore.getState().setSigmaInstance(sigma); |
| } |
| } |
| }, [sigma]); |
|
|
| |
| |
| |
| |
| useEffect(() => { |
| const { setFocusedNode, setSelectedNode, setFocusedEdge, setSelectedEdge, clearSelection } = |
| useGraphStore.getState() |
|
|
| |
| type NodeEvent = { node: string; event: { original: MouseEvent | TouchEvent } } |
| type EdgeEvent = { edge: string; event: { original: MouseEvent | TouchEvent } } |
|
|
| |
| 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() |
| } |
|
|
| |
| 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) |
| } |
| } |
| } |
|
|
| |
| registerEvents(events) |
|
|
| |
| return () => { |
| try { |
| console.log('Cleaning up graph event listeners') |
| } catch (error) { |
| console.warn('Error cleaning up graph event listeners:', error) |
| } |
| } |
| }, [registerEvents, enableEdgeEvents, sigma]) |
|
|
| |
| |
| |
| |
| useEffect(() => { |
| if (sigma && sigmaGraph) { |
| |
| const graph = sigma.getGraph() |
|
|
| |
| let minWeight = Number.MAX_SAFE_INTEGER |
| let maxWeight = 0 |
|
|
| graph.forEachEdge(edge => { |
| |
| const weight = graph.getEdgeAttribute(edge, 'originalWeight') || 1 |
| if (typeof weight === 'number') { |
| 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 |
| if (typeof weight === 'number') { |
| 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) |
| }) |
| } |
|
|
| |
| sigma.refresh() |
| } |
| }, [sigma, sigmaGraph, minEdgeSize, maxEdgeSize]) |
|
|
|
|
| |
| |
| |
| |
| useEffect(() => { |
| |
| const isDarkTheme = theme === 'dark' || |
| (theme === 'system' && window.document.documentElement.classList.contains('dark')) |
| const labelColor = isDarkTheme ? Constants.labelColorDarkTheme : undefined |
| const edgeColor = isDarkTheme ? Constants.edgeColorDarkTheme : undefined |
|
|
| |
| setSettings({ |
| |
| enableEdgeEvents, |
| renderEdgeLabels, |
| renderLabels, |
|
|
| |
| nodeReducer: (node, data) => { |
| const graph = sigma.getGraph() |
|
|
| |
| if (!graph.hasNode(node)) { |
| console.warn(`Node ${node} not found in graph during theme switch, returning default data`) |
| return { ...data, highlighted: false, labelColor } |
| } |
|
|
| 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); |
| return { ...data, highlighted: false, labelColor } |
| } |
| } else if (_focusedEdge && graph.hasEdge(_focusedEdge)) { |
| try { |
| if (graph.extremities(_focusedEdge).includes(node)) { |
| newData.highlighted = true |
| newData.size = 3 |
| } |
| } catch (error) { |
| console.error('Error accessing edge extremities in nodeReducer:', error); |
| return { ...data, highlighted: false, labelColor } |
| } |
| } else { |
| return newData |
| } |
|
|
| if (newData.highlighted) { |
| if (isDarkTheme) { |
| newData.labelColor = Constants.LabelColorHighlightedDarkTheme |
| } |
| } else { |
| newData.color = Constants.nodeColorDisabled |
| } |
| } |
| return newData |
| }, |
|
|
| |
| edgeReducer: (edge, data) => { |
| const graph = sigma.getGraph() |
|
|
| |
| if (!graph.hasEdge(edge)) { |
| console.warn(`Edge ${edge} not found in graph during theme switch, returning default data`) |
| return { ...data, hidden: false, labelColor, color: edgeColor } |
| } |
|
|
| const newData = { ...data, hidden: false, labelColor, color: edgeColor } |
|
|
| if (!disableHoverEffect) { |
| const _focusedNode = focusedNode || selectedNode |
| |
| const edgeHighlightColor = isDarkTheme |
| ? Constants.edgeColorHighlightedDarkTheme |
| : Constants.edgeColorHighlightedLightTheme |
|
|
| 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 = edgeHighlightColor |
| } |
| } |
| } catch (error) { |
| console.error('Error in edgeReducer:', error); |
| return { ...data, hidden: false, labelColor, color: edgeColor } |
| } |
| } 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 = edgeHighlightColor |
| } else if (hideUnselectedEdges) { |
| newData.hidden = true |
| } |
| } |
| } |
| } |
| return newData |
| } |
| }) |
| }, [ |
| selectedNode, |
| focusedNode, |
| selectedEdge, |
| focusedEdge, |
| setSettings, |
| sigma, |
| disableHoverEffect, |
| theme, |
| systemThemeIsDark, |
| hideUnselectedEdges, |
| enableEdgeEvents, |
| renderEdgeLabels, |
| renderLabels |
| ]) |
|
|
| return null |
| } |
|
|
| export default GraphControl |
|
|