11// Sigma.js WebGL graph view
2- // Renders dependency graph with search, controls, hover/click interactions
2+ // Single source of truth for highlight state shared between search and hover
33
4- import { useState , useEffect } from 'react'
4+ import { useState , useEffect , useCallback } from 'react'
55import {
66 SigmaContainer ,
77 useSigma ,
@@ -22,7 +22,6 @@ interface GraphViewProps {
2222 onSelectFile ?: ( filePath : string ) => void
2323}
2424
25- // dark bg, subtle edges, only important labels visible
2625const SIGMA_SETTINGS = {
2726 defaultNodeColor : '#6366f1' ,
2827 defaultEdgeColor : 'rgba(75, 85, 99, 0.12)' ,
@@ -42,7 +41,6 @@ const SIGMA_SETTINGS = {
4241 stagePadding : 30 ,
4342 defaultNodeBorderSize : 1 ,
4443 defaultNodeBorderColor : 'rgba(255, 255, 255, 0.06)' ,
45- // performance: skip edges when zoomed out
4644 hideEdgesOnMove : true ,
4745}
4846
@@ -52,7 +50,6 @@ function LoadAndDisplay({ graph }: { graph: Graph }) {
5250
5351 useEffect ( ( ) => {
5452 loadGraph ( graph )
55- // let sigma calculate bounds, then fit the camera with padding
5653 requestAnimationFrame ( ( ) => {
5754 requestAnimationFrame ( ( ) => {
5855 sigma . getCamera ( ) . animatedReset ( { duration : 400 } )
@@ -63,10 +60,19 @@ function LoadAndDisplay({ graph }: { graph: Graph }) {
6360 return null
6461}
6562
66- function Interactions ( { onSelectFile } : { onSelectFile ?: ( filePath : string ) => void } ) {
63+ // Single component that owns all highlight state
64+ // Both hover and search funnel into "highlightedNode"
65+ function InteractionsAndHighlight ( {
66+ onSelectFile,
67+ highlightedNode,
68+ setHighlightedNode,
69+ } : {
70+ onSelectFile ?: ( filePath : string ) => void
71+ highlightedNode : string | null
72+ setHighlightedNode : ( node : string | null ) => void
73+ } ) {
6774 const sigma = useSigma ( )
6875 const registerEvents = useRegisterEvents ( )
69- const [ hoveredNode , setHoveredNode ] = useState < string | null > ( null )
7076 const [ tooltip , setTooltip ] = useState < {
7177 nodeId : string
7278 position : { x : number ; y : number }
@@ -75,46 +81,51 @@ function Interactions({ onSelectFile }: { onSelectFile?: (filePath: string) => v
7581 useEffect ( ( ) => {
7682 registerEvents ( {
7783 enterNode : ( { node, event } ) => {
78- setHoveredNode ( node )
84+ setHighlightedNode ( node )
7985 setTooltip ( { nodeId : node , position : { x : event . x , y : event . y } } )
8086 const el = sigma . getContainer ( )
8187 if ( el ) el . style . cursor = 'pointer'
8288 } ,
8389 leaveNode : ( ) => {
84- setHoveredNode ( null )
90+ setHighlightedNode ( null )
8591 setTooltip ( null )
8692 const el = sigma . getContainer ( )
8793 if ( el ) el . style . cursor = 'default'
8894 } ,
8995 clickNode : ( { node } ) => onSelectFile ?.( node ) ,
9096 doubleClickNode : ( { node } ) => {
91- const pos = sigma . getNodeDisplayData ( node )
92- if ( pos ) {
93- sigma . getCamera ( ) . animate (
94- { x : pos . x , y : pos . y , ratio : 0.12 } ,
95- { duration : 400 }
96- )
97- }
97+ // use graph coords (node attributes), not display coords
98+ const graph = sigma . getGraph ( )
99+ if ( ! graph . hasNode ( node ) ) return
100+ const attrs = graph . getNodeAttributes ( node )
101+ sigma . getCamera ( ) . animate (
102+ { x : attrs . x , y : attrs . y , ratio : 0.12 } ,
103+ { duration : 400 }
104+ )
105+ } ,
106+ // click on empty stage clears highlight
107+ clickStage : ( ) => {
108+ setHighlightedNode ( null )
98109 } ,
99110 } )
100- } , [ registerEvents , sigma , onSelectFile ] )
111+ } , [ registerEvents , sigma , onSelectFile , setHighlightedNode ] )
101112
102- // highlight hovered neighborhood, fade everything else
113+ // single reducer driven by highlightedNode -- works for both hover and search
103114 useEffect ( ( ) => {
104115 const graph = sigma . getGraph ( )
105116
106- if ( hoveredNode && graph . hasNode ( hoveredNode ) ) {
107- const neighbors = new Set ( graph . neighbors ( hoveredNode ) )
108- neighbors . add ( hoveredNode )
117+ if ( highlightedNode && graph . hasNode ( highlightedNode ) ) {
118+ const neighbors = new Set ( graph . neighbors ( highlightedNode ) )
119+ neighbors . add ( highlightedNode )
109120
110121 sigma . setSetting ( 'nodeReducer' , ( node , data ) => {
111122 if ( neighbors . has ( node ) ) {
112123 return {
113124 ...data ,
114125 zIndex : 1 ,
115126 label : data . label ,
116- borderSize : node === hoveredNode ? 3 : 1 ,
117- borderColor : node === hoveredNode ? '#ffffff' : 'rgba(255,255,255,0.15)' ,
127+ borderSize : node === highlightedNode ? 3 : 1 ,
128+ borderColor : node === highlightedNode ? '#ffffff' : 'rgba(255,255,255,0.15)' ,
118129 }
119130 }
120131 return { ...data , color : 'rgba(31, 41, 55, 0.25)' , label : '' , zIndex : 0 , borderSize : 0 }
@@ -131,7 +142,7 @@ function Interactions({ onSelectFile }: { onSelectFile?: (filePath: string) => v
131142 sigma . setSetting ( 'nodeReducer' , null )
132143 sigma . setSetting ( 'edgeReducer' , null )
133144 }
134- } , [ hoveredNode , sigma ] )
145+ } , [ highlightedNode , sigma ] )
135146
136147 const tooltipData = ( ( ) => {
137148 if ( ! tooltip ) return null
@@ -154,6 +165,8 @@ function Interactions({ onSelectFile }: { onSelectFile?: (filePath: string) => v
154165
155166export function GraphView ( { data, onSelectFile } : GraphViewProps ) {
156167 const graph = useGraphData ( data )
168+ // shared highlight state -- search and hover both write to this
169+ const [ highlightedNode , setHighlightedNode ] = useState < string | null > ( null )
157170
158171 if ( ! graph || graph . order === 0 ) {
159172 return (
@@ -170,12 +183,15 @@ export function GraphView({ data, onSelectFile }: GraphViewProps) {
170183 settings = { SIGMA_SETTINGS }
171184 >
172185 < LoadAndDisplay graph = { graph } />
173- < Interactions onSelectFile = { onSelectFile } />
174- < SearchBar />
186+ < InteractionsAndHighlight
187+ onSelectFile = { onSelectFile }
188+ highlightedNode = { highlightedNode }
189+ setHighlightedNode = { setHighlightedNode }
190+ />
191+ < SearchBar onFocusNode = { setHighlightedNode } />
175192 < GraphControls />
176193 </ SigmaContainer >
177194
178- { /* legend */ }
179195 < div className = "absolute bottom-4 right-4 bg-zinc-900/80 backdrop-blur-sm border border-zinc-800 rounded-lg px-3 py-2.5 text-[11px]" >
180196 < div className = "text-zinc-400 font-medium mb-1.5" > Legend</ div >
181197 < div className = "space-y-0.5 text-zinc-500" >
0 commit comments