1- import { CoordinateGetter } from "@gephi/gephi-lite-sdk" ;
1+ import { CoordinateGetter , DEFAULT_EDGE_SIZE , DEFAULT_NODE_SIZE , StaticDynamicItemData } from "@gephi/gephi-lite-sdk" ;
22import { Producer , asyncAction , atom , derivedAtom , producerToAction } from "@ouestware/atoms" ;
3- import Graph from "graphology" ;
3+ import Graph , { MultiGraph } from "graphology" ;
44import { connectedCloseness } from "graphology-metrics/layout-quality" ;
55import { debounce , identity , pick } from "lodash" ;
66import seedRandom from "seedrandom" ;
77
88import { localStorage } from "../../utils/storage" ;
9+ import { VisualGetters } from "../appearance/types" ;
910import { EVENTS , emitter } from "../context/eventsContext" ;
10- import { graphDatasetActions , graphDatasetAtom , sigmaGraphAtom , visualGettersAtom } from "../graph" ;
11+ import {
12+ dynamicItemDataAtom ,
13+ filteredGraphAtom ,
14+ graphDatasetActions ,
15+ graphDatasetAtom ,
16+ sigmaGraphAtom ,
17+ visualGettersAtom ,
18+ } from "../graph" ;
19+ import {
20+ DatalessGraph ,
21+ DynamicItemData ,
22+ EdgeRenderingData ,
23+ GraphDataset ,
24+ NodeRenderingData ,
25+ SigmaGraph ,
26+ } from "../graph/types" ;
1127import { dataGraphToFullGraph } from "../graph/utils" ;
1228import { sessionAtom } from "../session" ;
1329import { resetCamera } from "../sigma" ;
@@ -21,63 +37,121 @@ import {
2137} from "./types" ;
2238
2339/**
24- * Creates a layout supervisor that runs on a cloned graph. In map mode,
25- * positions are projected between graph space and Mercator space; otherwise
26- * positions are copied as-is.
40+ * Builds a lightweight layout graph from dataset sources with only what layouts
41+ * need: x, y, size, fixed for nodes; weight for edges.
2742 */
28- function createLayoutSupervisor (
43+ export function buildLayoutGraph ( {
44+ dataset,
45+ filteredGraph,
46+ visualGetters,
47+ dynamicItemData,
48+ sigmaGraph,
49+ params,
50+ useSigmaPositions,
51+ } : {
52+ dataset : GraphDataset ;
53+ filteredGraph : DatalessGraph ;
54+ visualGetters : VisualGetters ;
55+ dynamicItemData : DynamicItemData ;
56+ sigmaGraph : SigmaGraph ;
57+ params : Record < string , unknown > ;
58+ useSigmaPositions : boolean ;
59+ } ) : SigmaGraph {
60+ const layoutGraph = sigmaGraph . nullCopy ( ) ;
61+ const reversePos = visualGetters . reverseNodePosition ;
62+ const fixedAttr =
63+ "getNodeFixedAttribut" in params && params . getNodeFixedAttribut ? `${ params . getNodeFixedAttribut } ` : null ;
64+
65+ filteredGraph . forEachNode ( ( node ) => {
66+ let x : number , y : number ;
67+ if ( useSigmaPositions ) {
68+ const sx = sigmaGraph . getNodeAttribute ( node , "x" ) ;
69+ const sy = sigmaGraph . getNodeAttribute ( node , "y" ) ;
70+ if ( reversePos ) {
71+ const p = reversePos ( { x : sx , y : sy } ) ;
72+ x = p . x ;
73+ y = p . y ;
74+ } else {
75+ x = sx ;
76+ y = sy ;
77+ }
78+ } else {
79+ const pos = dataset . layout [ node ] ;
80+ x = pos ?. x ?? 0 ;
81+ y = pos ?. y ?? 0 ;
82+ }
83+
84+ const data : StaticDynamicItemData = {
85+ static : dataset . nodeData [ node ] || { } ,
86+ dynamic : dynamicItemData . dynamicNodeData [ node ] || { } ,
87+ } ;
88+ const size = visualGetters . getNodeSize ? visualGetters . getNodeSize ( data ) : DEFAULT_NODE_SIZE ;
89+ const fixed =
90+ sigmaGraph . getNodeAttribute ( node , "dragging" ) === true ||
91+ ( fixedAttr !== null && dataset . nodeData [ node ] ?. [ fixedAttr ] === true ) ;
92+
93+ layoutGraph . addNode ( node , { x, y, size, fixed } ) ;
94+ } ) ;
95+
96+ filteredGraph . forEachEdge ( ( edge , _attrs , source , target ) => {
97+ const data : StaticDynamicItemData = {
98+ static : dataset . edgeData [ edge ] || { } ,
99+ dynamic : dynamicItemData . dynamicEdgeData [ edge ] || { } ,
100+ } ;
101+ const weight = visualGetters . getEdgeSize ? visualGetters . getEdgeSize ( data ) : DEFAULT_EDGE_SIZE ;
102+ if ( filteredGraph . isDirected ( edge ) ) {
103+ layoutGraph . addDirectedEdgeWithKey ( edge , source , target , { weight } ) ;
104+ } else {
105+ layoutGraph . addUndirectedEdgeWithKey ( edge , source , target , { weight } ) ;
106+ }
107+ } ) ;
108+
109+ return layoutGraph ;
110+ }
111+
112+ /**
113+ * Creates a layout supervisor that runs on a pre-built layout graph and syncs
114+ * positions back to the sigma graph on each tick.
115+ */
116+ export function createLayoutSupervisor (
29117 SupervisorClass : WorkerSupervisorConstructor ,
118+ layoutGraph : MultiGraph ,
30119 sigmaGraph : Graph ,
31120 options : unknown ,
32- transforms ?: {
33- getNodePosition ?: CoordinateGetter ;
34- reverseNodePosition ?: CoordinateGetter ;
35- } ,
121+ toSigma ?: CoordinateGetter ,
36122) : { supervisor : WorkerSupervisorInterface ; getPositions : ( ) => LayoutMapping } {
37- const passthrough : CoordinateGetter = ( pos ) => pos ;
38- const toGraph = transforms ?. reverseNodePosition ?? passthrough ;
39- const toSigma = transforms ?. getNodePosition ?? passthrough ;
40-
41- const shadowGraph = sigmaGraph . copy ( ) ;
42- shadowGraph . forEachNode ( ( node , { x, y } ) => {
43- const pos = toGraph ( { x, y } ) ;
44- shadowGraph . setNodeAttribute ( node , "x" , pos . x ) ;
45- shadowGraph . setNodeAttribute ( node , "y" , pos . y ) ;
46- } ) ;
47-
48123 const syncToSigma = ( ) => {
49- if ( transforms ?. getNodePosition ) {
50- let minY = Infinity ,
51- maxY = - Infinity ;
52- shadowGraph . forEachNode ( ( _ , { y } ) => {
53- if ( y < minY ) minY = y ;
54- if ( y > maxY ) maxY = y ;
55- } ) ;
56- }
57-
58- shadowGraph . forEachNode ( ( node , { x, y } ) => {
59- const pos = toSigma ( { x, y } ) ;
60- sigmaGraph . setNodeAttribute ( node , "x" , pos . x ) ;
61- sigmaGraph . setNodeAttribute ( node , "y" , pos . y ) ;
124+ sigmaGraph . updateEachNodeAttributes ( ( node , attrs ) => {
125+ if ( ! layoutGraph . hasNode ( node ) ) return attrs ;
126+ const { x, y } = layoutGraph . getNodeAttributes ( node ) ;
127+ if ( toSigma ) {
128+ const pos = toSigma ( { x, y } ) ;
129+ attrs . x = pos . x ;
130+ attrs . y = pos . y ;
131+ } else {
132+ attrs . x = x ;
133+ attrs . y = y ;
134+ }
135+ return attrs ;
62136 } ) ;
63137 } ;
64- shadowGraph . on ( "eachNodeAttributesUpdated" , syncToSigma ) ;
138+ layoutGraph . on ( "eachNodeAttributesUpdated" , syncToSigma ) ;
65139
66- const inner = new SupervisorClass ( shadowGraph , { settings : options } ) ;
140+ const inner = new SupervisorClass ( layoutGraph , { settings : options } ) ;
67141
68142 return {
69143 supervisor : {
70144 start : ( ) => inner . start ( ) ,
71145 stop : ( ) => inner . stop ( ) ,
72146 kill : ( ) => {
73147 inner . kill ( ) ;
74- shadowGraph . off ( "eachNodeAttributesUpdated" , syncToSigma ) ;
148+ layoutGraph . off ( "eachNodeAttributesUpdated" , syncToSigma ) ;
75149 } ,
76150 isRunning : ( ) => inner . isRunning ( ) ,
77151 } ,
78152 getPositions : ( ) => {
79153 const positions : LayoutMapping = { } ;
80- shadowGraph . forEachNode ( ( node , { x, y } ) => {
154+ layoutGraph . forEachNode ( ( node , { x, y } ) => {
81155 positions [ node ] = { x, y } ;
82156 } ) ;
83157 return positions ;
@@ -126,66 +200,66 @@ export const stopLayout = asyncAction(async (isForRestart = false) => {
126200 if ( ! isForRestart ) layoutStateAtom . set ( ( prev ) => ( { ...prev , type : "idle" } ) ) ;
127201} ) ;
128202
129- export const startLayout = asyncAction ( async ( id : string , params : Record < string , unknown > , isForRestart = false ) => {
130- // Stop the previous algo (the "if needed" is done in the function itself)
131- await stopLayout ( isForRestart ) ;
132-
133- const dataset = graphDatasetAtom . get ( ) ;
134- const { setNodePositions } = graphDatasetActions ;
135-
136- // search the layout
137- const layout = LAYOUTS . find ( ( l ) => l . id === id ) ;
138-
139- if ( layout ) {
140- // Sync layout
141- if ( layout . type === "sync" ) {
142- layoutStateAtom . set ( ( prev ) => ( { ...prev , type : "running" , layoutId : id , supervisor : undefined } ) ) ;
143-
144- // generate positions
145- const fullGraph = dataGraphToFullGraph ( dataset ) ;
146- const positions = layout . run ( fullGraph , { settings : params } ) ;
203+ export const startLayout = asyncAction (
204+ async ( id : string , params : Record < string , unknown > , isForRestart : boolean = false ) => {
205+ // Stop the previous algo (the "if needed" is done in the function itself)
206+ await stopLayout ( isForRestart ) ;
147207
148- // Save it
149- setNodePositions ( positions ) ;
208+ const dataset = graphDatasetAtom . get ( ) ;
209+ const { setNodePositions } = graphDatasetActions ;
150210
151- // To prevent resetting the camera before sigma receives new data, we
152- // need to wait a frame, and also wait for it to trigger a refresh:
153- setTimeout ( ( ) => {
154- layoutStateAtom . set ( ( prev ) => ( { ...prev , type : "idle" } ) ) ;
155- resetCamera ( { forceRefresh : true } ) ;
156- } , 0 ) ;
157- }
158-
159- // Async layout
160- if ( layout . type === "worker" ) {
161- const sigmaGraph = sigmaGraphAtom . get ( ) ;
162- const visualGetters = visualGettersAtom . get ( ) ;
163-
164- // Fixed node management
165- // ---------------------
166- // If layout parameter has a `getNodeFixedAttribut`, then we have to set the 'fixed' attribut in sigma's graph
167- // On a layout restart, if parameter has been removed, we need to set to false
168- // We also use the 'fixed' attribut for drag'n'drop
169- sigmaGraph . updateEachNodeAttributes ( ( id , attrs ) => {
170- let fixed = attrs . dragging === true ;
171- if ( "getNodeFixedAttribut" in params && params . getNodeFixedAttribut ) {
172- const fixedAttribut = `${ params . getNodeFixedAttribut } ` ;
173- if ( dataset . nodeData [ id ] [ fixedAttribut ] === true ) {
174- fixed = true ;
175- }
176- }
177- return { ...attrs , fixed } ;
178- } ) ;
211+ // search the layout
212+ const layout = LAYOUTS . find ( ( l ) => l . id === id ) ;
179213
180- const { supervisor, getPositions } = createLayoutSupervisor ( layout . supervisor , sigmaGraph , params , {
181- getNodePosition : visualGetters . getNodePosition ?? undefined ,
182- reverseNodePosition : visualGetters . reverseNodePosition ?? undefined ,
183- } ) ;
184- supervisor . start ( ) ;
185- layoutStateAtom . set ( ( prev ) => ( { ...prev , type : "running" , layoutId : id , supervisor, getPositions } ) ) ;
214+ if ( layout ) {
215+ // Sync layout
216+ if ( layout . type === "sync" ) {
217+ layoutStateAtom . set ( ( prev ) => ( { ...prev , type : "running" , layoutId : id , supervisor : undefined } ) ) ;
218+
219+ // generate positions
220+ const fullGraph = dataGraphToFullGraph ( dataset ) ;
221+ const positions = layout . run ( fullGraph , { settings : params } ) ;
222+
223+ // Save it
224+ setNodePositions ( positions ) ;
225+
226+ // To prevent resetting the camera before sigma receives new data, we
227+ // need to wait a frame, and also wait for it to trigger a refresh:
228+ setTimeout ( ( ) => {
229+ layoutStateAtom . set ( ( prev ) => ( { ...prev , type : "idle" } ) ) ;
230+ resetCamera ( { forceRefresh : true } ) ;
231+ } , 0 ) ;
232+ }
233+
234+ // Async layout
235+ if ( layout . type === "worker" ) {
236+ const sigmaGraph = sigmaGraphAtom . get ( ) ;
237+ const visualGetters = visualGettersAtom . get ( ) ;
238+ const filteredGraph = filteredGraphAtom . get ( ) ;
239+ const dynamicItemData = dynamicItemDataAtom . get ( ) ;
240+
241+ const layoutGraph = buildLayoutGraph ( {
242+ dataset,
243+ filteredGraph,
244+ visualGetters,
245+ dynamicItemData,
246+ sigmaGraph,
247+ params,
248+ useSigmaPositions : isForRestart ,
249+ } ) ;
250+ const { supervisor, getPositions } = createLayoutSupervisor (
251+ layout . supervisor ,
252+ layoutGraph ,
253+ sigmaGraph ,
254+ params ,
255+ visualGetters . getNodePosition ?? undefined ,
256+ ) ;
257+ supervisor . start ( ) ;
258+ layoutStateAtom . set ( ( prev ) => ( { ...prev , type : "running" , layoutId : id , supervisor, getPositions } ) ) ;
259+ }
186260 }
187- }
188- } ) ;
261+ } ,
262+ ) ;
189263
190264export const restartLastLayout = asyncAction ( async ( ) => {
191265 // Get the algo and its parameters
@@ -257,9 +331,11 @@ gridEnabledAtom.bindEffect((connectedClosenessSettings) => {
257331layoutStateAtom . bindEffect ( ( state ) => {
258332 if ( state . type !== "running" ) return ;
259333
260- const fnHandleDraggin = debounce ( restartLastLayout , 100 , { leading : true , trailing : true , maxWait : 100 } ) ;
261- emitter . on ( EVENTS . nodesDragged , fnHandleDraggin ) ;
334+ const fnRestart = debounce ( restartLastLayout , 100 , { leading : true , trailing : true , maxWait : 100 } ) ;
335+ emitter . on ( EVENTS . nodesDragged , fnRestart ) ;
336+ emitter . on ( EVENTS . graphImported , fnRestart ) ;
262337 return ( ) => {
263- emitter . off ( EVENTS . nodesDragged , fnHandleDraggin ) ;
338+ emitter . off ( EVENTS . nodesDragged , fnRestart ) ;
339+ emitter . off ( EVENTS . graphImported , fnRestart ) ;
264340 } ;
265341} ) ;
0 commit comments