| import { useEffect, useState, useCallback, useMemo, useRef } from 'react' |
| |
| import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core' |
| import { Settings as SigmaSettings } from 'sigma/settings' |
| import { GraphSearchOption, OptionItem } from '@react-sigma/graph-search' |
| import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/rendering' |
| import { NodeBorderProgram } from '@sigma/node-border' |
| import { EdgeCurvedArrowProgram, createEdgeCurveProgram } from '@sigma/edge-curve' |
|
|
| import FocusOnNode from '@/components/graph/FocusOnNode' |
| import LayoutsControl from '@/components/graph/LayoutsControl' |
| import GraphControl from '@/components/graph/GraphControl' |
| |
| import ZoomControl from '@/components/graph/ZoomControl' |
| import FullScreenControl from '@/components/graph/FullScreenControl' |
| import Settings from '@/components/graph/Settings' |
| import GraphSearch from '@/components/graph/GraphSearch' |
| import GraphLabels from '@/components/graph/GraphLabels' |
| import PropertiesView from '@/components/graph/PropertiesView' |
| import SettingsDisplay from '@/components/graph/SettingsDisplay' |
| import Legend from '@/components/graph/Legend' |
| import LegendButton from '@/components/graph/LegendButton' |
|
|
| import { useSettingsStore } from '@/stores/settings' |
| import { useGraphStore } from '@/stores/graph' |
| import { labelColorDarkTheme, labelColorLightTheme } from '@/lib/constants' |
|
|
| import '@react-sigma/core/lib/style.css' |
| import '@react-sigma/graph-search/lib/style.css' |
|
|
| |
| const createSigmaSettings = (isDarkTheme: boolean): Partial<SigmaSettings> => ({ |
| allowInvalidContainer: true, |
| defaultNodeType: 'default', |
| defaultEdgeType: 'curvedNoArrow', |
| renderEdgeLabels: false, |
| edgeProgramClasses: { |
| arrow: EdgeArrowProgram, |
| curvedArrow: EdgeCurvedArrowProgram, |
| curvedNoArrow: createEdgeCurveProgram() |
| }, |
| nodeProgramClasses: { |
| default: NodeBorderProgram, |
| circel: NodeCircleProgram, |
| point: NodePointProgram |
| }, |
| labelGridCellSize: 60, |
| labelRenderedSizeThreshold: 12, |
| enableEdgeEvents: true, |
| labelColor: { |
| color: isDarkTheme ? labelColorDarkTheme : labelColorLightTheme, |
| attribute: 'labelColor' |
| }, |
| edgeLabelColor: { |
| color: isDarkTheme ? labelColorDarkTheme : labelColorLightTheme, |
| attribute: 'labelColor' |
| }, |
| edgeLabelSize: 8, |
| labelSize: 12 |
| |
| |
| }) |
|
|
| const GraphEvents = () => { |
| const registerEvents = useRegisterEvents() |
| const sigma = useSigma() |
| const [draggedNode, setDraggedNode] = useState<string | null>(null) |
|
|
| useEffect(() => { |
| |
| registerEvents({ |
| downNode: (e) => { |
| setDraggedNode(e.node) |
| sigma.getGraph().setNodeAttribute(e.node, 'highlighted', true) |
| }, |
| |
| mousemovebody: (e) => { |
| if (!draggedNode) return |
| |
| const pos = sigma.viewportToGraph(e) |
| sigma.getGraph().setNodeAttribute(draggedNode, 'x', pos.x) |
| sigma.getGraph().setNodeAttribute(draggedNode, 'y', pos.y) |
|
|
| |
| e.preventSigmaDefault() |
| e.original.preventDefault() |
| e.original.stopPropagation() |
| }, |
| |
| mouseup: () => { |
| if (draggedNode) { |
| setDraggedNode(null) |
| sigma.getGraph().removeNodeAttribute(draggedNode, 'highlighted') |
| } |
| }, |
| |
| mousedown: (e) => { |
| |
| const mouseEvent = e.original as MouseEvent; |
| if (mouseEvent.buttons !== 0 && !sigma.getCustomBBox()) { |
| sigma.setCustomBBox(sigma.getBBox()) |
| } |
| } |
| }) |
| }, [registerEvents, sigma, draggedNode]) |
|
|
| return null |
| } |
|
|
| const GraphViewer = () => { |
| const sigmaRef = useRef<any>(null) |
| const prevTheme = useRef<string>('') |
|
|
| const selectedNode = useGraphStore.use.selectedNode() |
| const focusedNode = useGraphStore.use.focusedNode() |
| const moveToSelectedNode = useGraphStore.use.moveToSelectedNode() |
| const isFetching = useGraphStore.use.isFetching() |
|
|
| const showPropertyPanel = useSettingsStore.use.showPropertyPanel() |
| const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() |
| const enableNodeDrag = useSettingsStore.use.enableNodeDrag() |
| const showLegend = useSettingsStore.use.showLegend() |
| const theme = useSettingsStore.use.theme() |
|
|
| const [isThemeSwitching, setIsThemeSwitching] = useState(false) |
|
|
| |
| const memoizedSigmaSettings = useMemo(() => { |
| const isDarkTheme = theme === 'dark' |
| return createSigmaSettings(isDarkTheme) |
| }, [theme]) |
|
|
| |
| |
| |
| useEffect(() => { |
| const isThemeChange = prevTheme.current && prevTheme.current !== theme |
| if (isThemeChange) { |
| console.log('Theme switching detected:', prevTheme.current, '->', theme) |
| prevTheme.current = theme |
|
|
| const switchTimer = setTimeout(() => setIsThemeSwitching(true), 0) |
| const timer = setTimeout(() => { |
| setIsThemeSwitching(false) |
| console.log('Theme switching completed') |
| }, 150) |
|
|
| return () => { |
| clearTimeout(switchTimer) |
| clearTimeout(timer) |
| } |
| } |
| prevTheme.current = theme |
| console.log('Initialized sigma settings for theme:', theme) |
| }, [theme]) |
|
|
| |
| useEffect(() => { |
| return () => { |
| |
|
|
| const sigma = useGraphStore.getState().sigmaInstance; |
| if (sigma) { |
| try { |
| |
| sigma.kill(); |
| useGraphStore.getState().setSigmaInstance(null); |
| console.log('Cleared sigma instance on Graphviewer unmount'); |
| } catch (error) { |
| console.error('Error cleaning up sigma instance:', error); |
| } |
| } |
| }; |
| }, []); |
|
|
| |
| |
| |
|
|
| const onSearchFocus = useCallback((value: GraphSearchOption | null) => { |
| if (value === null) useGraphStore.getState().setFocusedNode(null) |
| else if (value.type === 'nodes') useGraphStore.getState().setFocusedNode(value.id) |
| }, []) |
|
|
| const onSearchSelect = useCallback((value: GraphSearchOption | null) => { |
| if (value === null) { |
| useGraphStore.getState().setSelectedNode(null) |
| } else if (value.type === 'nodes') { |
| useGraphStore.getState().setSelectedNode(value.id, true) |
| } |
| }, []) |
|
|
| const autoFocusedNode = useMemo(() => focusedNode ?? selectedNode, [focusedNode, selectedNode]) |
| const searchInitSelectedNode = useMemo( |
| (): OptionItem | null => (selectedNode ? { type: 'nodes', id: selectedNode } : null), |
| [selectedNode] |
| ) |
|
|
| |
| return ( |
| <div className="relative h-full w-full overflow-hidden"> |
| <SigmaContainer |
| settings={memoizedSigmaSettings} |
| className="!bg-background !size-full overflow-hidden" |
| ref={sigmaRef} |
| > |
| <GraphControl /> |
| |
| {enableNodeDrag && <GraphEvents />} |
| |
| <FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} /> |
| |
| <div className="absolute top-2 left-2 flex items-start gap-2"> |
| <GraphLabels /> |
| {showNodeSearchBar && !isThemeSwitching && ( |
| <GraphSearch |
| value={searchInitSelectedNode} |
| onFocus={onSearchFocus} |
| onChange={onSearchSelect} |
| /> |
| )} |
| </div> |
| |
| <div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg"> |
| <LayoutsControl /> |
| <ZoomControl /> |
| <FullScreenControl /> |
| <LegendButton /> |
| <Settings /> |
| {/* <ThemeToggle /> */} |
| </div> |
| |
| {showPropertyPanel && ( |
| <div className="absolute top-2 right-2 z-10"> |
| <PropertiesView /> |
| </div> |
| )} |
| |
| {showLegend && ( |
| <div className="absolute bottom-10 right-2 z-0"> |
| <Legend className="bg-background/60 backdrop-blur-lg" /> |
| </div> |
| )} |
| |
| {/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2"> |
| <MiniMap width="100px" height="100px" /> |
| </div> */} |
| |
| <SettingsDisplay /> |
| </SigmaContainer> |
| |
| {/* Loading overlay - shown when data is loading or theme is switching */} |
| {(isFetching || isThemeSwitching) && ( |
| <div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10"> |
| <div className="text-center"> |
| <div className="mb-2 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent mx-auto"></div> |
| <p>{isThemeSwitching ? 'Switching Theme...' : 'Loading Graph Data...'}</p> |
| </div> |
| </div> |
| )} |
| </div> |
| ) |
| } |
|
|
| export default GraphViewer |
|
|