@@ -12,7 +12,8 @@ import { useLoadGraph, useReloadGraph } from "./useLoadGraph";
1212import { GraphLoadingOverlay } from "./GraphLoadingOverlay" ;
1313import { createGraphLoadProgress , getGraphLoadTitle } from "./graphLoading" ;
1414import { GRAPH_THEME , withAlpha } from "./graphTheme" ;
15- import { resolveDisplayGraph } from "./graphSceneState" ;
15+ import { resolveDisplayGraph , resolveDisplayStateSnapshot } from "./graphSceneState" ;
16+ import { computeGraphAnalyticsBase } from "./graphAnalytics" ;
1617import {
1718 type GraphPlugin ,
1819 type GraphPluginActionRequest ,
@@ -108,6 +109,15 @@ const LazyGraphInspectorPanel = lazy(() => import("./GraphInspectorPanel").then(
108109const loadExplorationEffectsPlugin = ( ) => import ( "./plugins/explorationEffectsPluginPhaseC" ) . then ( ( module ) => module . explorationEffectsPluginPhaseC ) ;
109110const loadNeighborhoodPanelPlugin = ( ) => import ( "./plugins/neighborhoodPanelPlugin" ) . then ( ( module ) => module . neighborhoodPanelPlugin ) ;
110111const loadTemporalOverlayPlugin = ( ) => import ( "./plugins/temporalOverlayPlugin" ) . then ( ( module ) => module . temporalOverlayPlugin ) ;
112+ const EMPTY_PATH : string [ ] = [ ] ;
113+ const DEBUG_GRAPH_WORKSPACE = import . meta. env . DEV ;
114+
115+ function debugGraphWorkspace ( message : string , payload ?: Record < string , unknown > ) {
116+ if ( ! DEBUG_GRAPH_WORKSPACE ) {
117+ return ;
118+ }
119+ console . debug ( `[GraphWorkspace] ${ message } ` , payload ?? { } ) ;
120+ }
111121
112122function useDebounce < T > ( value : T , delay : number ) : T {
113123 const [ debouncedValue , setDebouncedValue ] = useState < T > ( value ) ;
@@ -670,6 +680,8 @@ export function GraphWorkspace() {
670680 const [ selectedNodeId , setSelectedNodeId ] = useState ( "" ) ;
671681 const [ selectedEdgeId , setSelectedEdgeId ] = useState ( "" ) ;
672682 const [ isLayoutRunning , setIsLayoutRunning ] = useState ( false ) ;
683+ const [ graphReady , setGraphReady ] = useState ( false ) ;
684+ const [ graphVersion , setGraphVersion ] = useState ( 0 ) ;
673685 const [ viewMode , setViewMode ] = useState < GraphViewMode > ( "full" ) ;
674686 const [ aggregationEnabled ] = useState ( true ) ;
675687 const [ collapsedNeighborhoodNodeIds , setCollapsedNeighborhoodNodeIds ] = useState < string [ ] > ( [ ] ) ;
@@ -715,9 +727,18 @@ export function GraphWorkspace() {
715727 } ) ;
716728 const reload = useReloadGraph ( ) ;
717729
730+ const handleLoadProgress = useCallback ( ( progress : GraphLoadProgress ) => {
731+ setLoadingProgress ( progress ) ;
732+ if ( progress . phase !== "ready" && progress . phase !== "stabilizing_layout" ) {
733+ setGraphReady ( false ) ;
734+ }
735+ } , [ ] ) ;
736+
718737 const { data : summary , isLoading, isFetching } = useLoadGraph ( {
719738 enabled : true ,
720739 onGraphReady : ( graphSummary ) => {
740+ setGraphReady ( true ) ;
741+ setGraphVersion ( ( current ) => current + 1 ) ;
721742 setIsLayoutRunning ( ! graphSummary . layoutReady ) ;
722743 if ( settlingOverlayTimeoutRef . current !== null ) {
723744 window . clearTimeout ( settlingOverlayTimeoutRef . current ) ;
@@ -746,7 +767,7 @@ export function GraphWorkspace() {
746767 settlingOverlayTimeoutRef . current = null ;
747768 } , 900 ) ;
748769 } ,
749- onProgress : setLoadingProgress ,
770+ onProgress : handleLoadProgress ,
750771 } ) ;
751772
752773 useEffect ( ( ) => {
@@ -808,6 +829,7 @@ export function GraphWorkspace() {
808829 } ) ;
809830 prevActiveIdsRef . current = nextActiveIds ;
810831 setActiveNodeCount ( data . active_node_count ) ;
832+ setGraphVersion ( ( current ) => current + 1 ) ;
811833 sceneRef . current ?. getRuntime ( ) ?. requestRender ( ) ;
812834 } ) ;
813835 } catch ( fetchError ) {
@@ -877,8 +899,10 @@ export function GraphWorkspace() {
877899 setPathResult ( null ) ;
878900 setSearchResults ( [ ] ) ;
879901 setSearchError ( "" ) ;
880- setIsLayoutRunning ( false ) ;
881- } , [ ] ) ;
902+ if ( viewMode === "focused" ) {
903+ setIsLayoutRunning ( false ) ;
904+ }
905+ } , [ viewMode ] ) ;
882906
883907 const handleEdgeSelect = useCallback ( ( edgeId : string ) => {
884908 setSelectedEdgeId ( edgeId ) ;
@@ -997,6 +1021,7 @@ export function GraphWorkspace() {
9971021 } ,
9981022 ] ) ;
9991023 logEvent ( "add-node" , `Added node ${ payload . label ?? payload . id } ${ payload . nodeType ? ` (${ payload . nodeType } )` : "" } via realtime ws` , { nodeId : payload . id , nodeType : payload . nodeType } ) ;
1024+ setGraphVersion ( ( current ) => current + 1 ) ;
10001025 sceneRef . current ?. getRuntime ( ) ?. requestRender ( ) ;
10011026 }
10021027 if ( eventType === "ADD_EDGE" ) {
@@ -1010,6 +1035,7 @@ export function GraphWorkspace() {
10101035 } ,
10111036 ] ) ;
10121037 logEvent ( "add-edge" , `Added edge ${ payload . edgeType ?? payload . id } (${ payload . source_id } → ${ payload . target_id } ) via realtime ws` , { edgeId : payload . id , edgeType : payload . edgeType , source : payload . source_id , target : payload . target_id } ) ;
1038+ setGraphVersion ( ( current ) => current + 1 ) ;
10131039 sceneRef . current ?. getRuntime ( ) ?. requestRender ( ) ;
10141040 }
10151041 } catch ( socketError ) {
@@ -1026,18 +1052,77 @@ export function GraphWorkspace() {
10261052 setCollapsedNeighborhoodNodeIds ( [ ] ) ;
10271053 } , [ summary ?. edgeCount , summary ?. nodeCount ] ) ;
10281054
1029- const showLoadingOverlay = isLoading || isFetching || loadingProgress ?. phase === "stabilizing_layout" ;
1055+ const showLoadingOverlay = ! graphReady && ( isLoading || isFetching || Boolean ( loadingProgress ) ) ;
1056+ const showSettlingStatus = graphReady && loadingProgress ?. phase === "stabilizing_layout" ;
10301057 const hasGraphContent = Boolean ( summary ?. nodeCount ) ;
1031- const activePath = pathResult ?. path ?? [ ] ;
1032- const activePathEdgeIds = pathResult ?. edge_ids ?? [ ] ;
1058+ const activePath = pathResult ?. path ?? EMPTY_PATH ;
1059+ const activePathEdgeIds = pathResult ?. edge_ids ?? EMPTY_PATH ;
1060+ const structuralSelectedNodeId = useMemo ( ( ) => {
1061+ if ( ! selectedNodeId || ! graph . hasNode ( selectedNodeId ) ) {
1062+ return "" ;
1063+ }
1064+ if ( viewMode === "focused" ) {
1065+ return selectedNodeId ;
1066+ }
1067+ return collapsedNeighborhoodNodeIds . includes ( selectedNodeId ) ? selectedNodeId : "" ;
1068+ } , [ collapsedNeighborhoodNodeIds , selectedNodeId , viewMode ] ) ;
1069+ const structuralActivePath = structuralSelectedNodeId ? activePath : EMPTY_PATH ;
1070+ const structuralActivePathEdgeIds = structuralSelectedNodeId ? activePathEdgeIds : EMPTY_PATH ;
1071+ const groupedViewAvailable = useMemo (
1072+ ( ) => computeGraphAnalyticsBase ( graph , { computeCommunities : true , computeCentrality : false } ) . communitiesByNode . size > 0 ,
1073+ [ graphVersion ] ,
1074+ ) ;
10331075 const displayResult = useMemo (
1034- ( ) => resolveDisplayGraph ( selectedNodeId , activePath , activePathEdgeIds , viewMode , {
1076+ ( ) => resolveDisplayGraph ( structuralSelectedNodeId , structuralActivePath , structuralActivePathEdgeIds , viewMode , {
1077+ aggregationEnabled,
1078+ collapsedNeighborhoodNodeIds,
1079+ groupedViewAvailable,
1080+ } ) ,
1081+ [
1082+ aggregationEnabled ,
1083+ collapsedNeighborhoodNodeIds ,
1084+ graphVersion ,
1085+ groupedViewAvailable ,
1086+ structuralActivePath ,
1087+ structuralActivePathEdgeIds ,
1088+ structuralSelectedNodeId ,
1089+ viewMode ,
1090+ ] ,
1091+ ) ;
1092+ const displayState = useMemo (
1093+ ( ) => resolveDisplayStateSnapshot ( selectedNodeId , activePath , viewMode , {
10351094 aggregationEnabled,
10361095 collapsedNeighborhoodNodeIds,
1096+ groupedViewAvailable,
10371097 } ) ,
1038- [ activePath , activePathEdgeIds , aggregationEnabled , collapsedNeighborhoodNodeIds , selectedNodeId , viewMode ] ,
1098+ [ activePath , aggregationEnabled , collapsedNeighborhoodNodeIds , groupedViewAvailable , selectedNodeId , viewMode ] ,
10391099 ) ;
1040- const displayState = displayResult . state ;
1100+ const displayMeta = displayResult . meta ;
1101+ const previousDisplayGraphRef = useRef ( displayResult . graph ) ;
1102+ const previousDisplayStateRef = useRef ( displayState ) ;
1103+ useEffect ( ( ) => {
1104+ const graphRebuilt = previousDisplayGraphRef . current !== displayResult . graph ;
1105+ const displayStateChanged = previousDisplayStateRef . current !== displayState ;
1106+ debugGraphWorkspace ( "display-state-derived" , {
1107+ selectedNodeId,
1108+ structuralSelectedNodeId,
1109+ viewMode,
1110+ graphRebuilt,
1111+ displayStateChanged,
1112+ aggregationEnabled,
1113+ collapsedNeighborhoodActive : Boolean ( structuralSelectedNodeId && collapsedNeighborhoodNodeIds . includes ( structuralSelectedNodeId ) ) ,
1114+ } ) ;
1115+ previousDisplayGraphRef . current = displayResult . graph ;
1116+ previousDisplayStateRef . current = displayState ;
1117+ } , [
1118+ aggregationEnabled ,
1119+ collapsedNeighborhoodNodeIds ,
1120+ displayResult . graph ,
1121+ displayState ,
1122+ selectedNodeId ,
1123+ structuralSelectedNodeId ,
1124+ viewMode ,
1125+ ] ) ;
10411126 const focusedSummary = useMemo ( ( ) => {
10421127 if ( ! selectedNodeId || ! graph . hasNode ( selectedNodeId ) ) {
10431128 if ( viewMode === "grouped" ) {
@@ -1473,25 +1558,13 @@ export function GraphWorkspace() {
14731558 id : "zoom-in" ,
14741559 label : "+ Zoom In" ,
14751560 title : "Zoom in (or scroll up on the canvas)" ,
1476- onClick : ( ) => {
1477- const runtime = sceneRef . current ?. getRuntime ( ) ;
1478- if ( runtime ?. renderer === "sigma" ) {
1479- const camera = ( runtime . scene as import ( "sigma" ) . default ) . getCamera ( ) ;
1480- camera . animatedZoom ( { duration : 200 } ) ;
1481- }
1482- } ,
1561+ onClick : ( ) => sceneRef . current ?. zoomIn ( ) ,
14831562 } ,
14841563 {
14851564 id : "zoom-out" ,
14861565 label : "- Zoom Out" ,
14871566 title : "Zoom out (or scroll down on the canvas)" ,
1488- onClick : ( ) => {
1489- const runtime = sceneRef . current ?. getRuntime ( ) ;
1490- if ( runtime ?. renderer === "sigma" ) {
1491- const camera = ( runtime . scene as import ( "sigma" ) . default ) . getCamera ( ) ;
1492- camera . animatedUnzoom ( { duration : 200 } ) ;
1493- }
1494- } ,
1567+ onClick : ( ) => sceneRef . current ?. zoomOut ( ) ,
14951568 } ,
14961569 {
14971570 id : "fit-view" ,
@@ -1550,16 +1623,20 @@ export function GraphWorkspace() {
15501623 const sceneAdapterProps = {
15511624 onNodeSelect : focusNode ,
15521625 onEdgeSelect : handleEdgeSelect ,
1626+ graphVersion,
1627+ graphReady,
1628+ displayGraph : displayResult . graph ,
1629+ displayMeta,
1630+ displayState,
15531631 selectedNodeId,
15541632 selectedEdgeId,
15551633 activePath,
15561634 activePathEdgeIds,
15571635 effectsState,
15581636 temporalState,
15591637 isLayoutRunning,
1638+ layoutSource : graphSummary ?. layoutSource ,
15601639 viewMode,
1561- aggregationEnabled,
1562- collapsedNeighborhoodNodeIds,
15631640 showFitViewButton : false ,
15641641 pluginOverlays : pluginOverlays . map ( ( overlay ) => overlay . element ) ,
15651642 onRuntimeChange : handleSceneRuntimeChange ,
@@ -1580,7 +1657,7 @@ export function GraphWorkspace() {
15801657 < div style = { { display : "flex" , flexDirection : "column" , gap : 12 } } >
15811658 < div className = "explore-toolbar" >
15821659 < div style = { { display : "flex" , gap : 8 , flexWrap : "wrap" , alignItems : "center" } } >
1583- { showLoadingOverlay && loadingProgress ? (
1660+ { ( showLoadingOverlay || showSettlingStatus ) && loadingProgress ? (
15841661 < MetricChip > { getGraphLoadTitle ( loadingProgress . phase ) } </ MetricChip >
15851662 ) : null }
15861663 { summary ? (
0 commit comments