|
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 EdgeCurveProgram, { EdgeCurvedArrowProgram } 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 '@react-sigma/core/lib/style.css' |
|
import '@react-sigma/graph-search/lib/style.css' |
|
|
|
|
|
const defaultSigmaSettings: Partial<SigmaSettings> = { |
|
allowInvalidContainer: true, |
|
defaultNodeType: 'default', |
|
defaultEdgeType: 'curvedNoArrow', |
|
renderEdgeLabels: false, |
|
edgeProgramClasses: { |
|
arrow: EdgeArrowProgram, |
|
curvedArrow: EdgeCurvedArrowProgram, |
|
curvedNoArrow: EdgeCurveProgram |
|
}, |
|
nodeProgramClasses: { |
|
default: NodeBorderProgram, |
|
circel: NodeCircleProgram, |
|
point: NodePointProgram |
|
}, |
|
labelGridCellSize: 60, |
|
labelRenderedSizeThreshold: 12, |
|
enableEdgeEvents: true, |
|
labelColor: { |
|
color: '#000', |
|
attribute: 'labelColor' |
|
}, |
|
edgeLabelColor: { |
|
color: '#000', |
|
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 [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings) |
|
const sigmaRef = useRef<any>(null) |
|
|
|
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() |
|
|
|
|
|
|
|
useEffect(() => { |
|
setSigmaSettings(defaultSigmaSettings) |
|
console.log('Initialized sigma settings') |
|
}, []) |
|
|
|
|
|
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={sigmaSettings} |
|
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 && ( |
|
<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"> |
|
<PropertiesView /> |
|
</div> |
|
)} |
|
|
|
{showLegend && ( |
|
<div className="absolute bottom-10 right-2"> |
|
<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 */} |
|
{isFetching && ( |
|
<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"></div> |
|
<p>Loading Graph Data...</p> |
|
</div> |
|
</div> |
|
)} |
|
</div> |
|
) |
|
} |
|
|
|
export default GraphViewer |
|
|