yangdx
Change to undirected graph
af95aa1
import { useEffect, useState, useCallback, useMemo, useRef } from 'react'
// import { MiniMap } from '@react-sigma/minimap'
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 ThemeToggle from '@/components/ThemeToggle'
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'
// Sigma settings
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
// minEdgeThickness: 2
// labelFont: 'Lato, sans-serif'
}
const GraphEvents = () => {
const registerEvents = useRegisterEvents()
const sigma = useSigma()
const [draggedNode, setDraggedNode] = useState<string | null>(null)
useEffect(() => {
// Register the events
registerEvents({
downNode: (e) => {
setDraggedNode(e.node)
sigma.getGraph().setNodeAttribute(e.node, 'highlighted', true)
},
// On mouse move, if the drag mode is enabled, we change the position of the draggedNode
mousemovebody: (e) => {
if (!draggedNode) return
// Get new position of node
const pos = sigma.viewportToGraph(e)
sigma.getGraph().setNodeAttribute(draggedNode, 'x', pos.x)
sigma.getGraph().setNodeAttribute(draggedNode, 'y', pos.y)
// Prevent sigma to move camera:
e.preventSigmaDefault()
e.original.preventDefault()
e.original.stopPropagation()
},
// On mouse up, we reset the autoscale and the dragging mode
mouseup: () => {
if (draggedNode) {
setDraggedNode(null)
sigma.getGraph().removeNodeAttribute(draggedNode, 'highlighted')
}
},
// Disable the autoscale at the first down interaction
mousedown: (e) => {
// Only set custom BBox if it's a drag operation (mouse button is pressed)
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()
// Initialize sigma settings once on component mount
// All dynamic settings will be updated in GraphControl using useSetSettings
useEffect(() => {
setSigmaSettings(defaultSigmaSettings)
console.log('Initialized sigma settings')
}, [])
// Clean up sigma instance when component unmounts
useEffect(() => {
return () => {
// TAB is mount twice in vite dev mode, this is a workaround
const sigma = useGraphStore.getState().sigmaInstance;
if (sigma) {
try {
// Destroy sigma,and clear WebGL context
sigma.kill();
useGraphStore.getState().setSigmaInstance(null);
console.log('Cleared sigma instance on Graphviewer unmount');
} catch (error) {
console.error('Error cleaning up sigma instance:', error);
}
}
};
}, []);
// Note: There was a useLayoutEffect hook here to set up the sigma instance and graph data,
// but testing showed it wasn't executing or having any effect, while the backup mechanism
// in GraphControl was sufficient. This code was removed to simplify implementation
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]
)
// Always render SigmaContainer but control its visibility with CSS
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