Skip to content

Commit ec69a28

Browse files
committed
[gephi-lite] Refactors layout logic
Details: - Renames shadowGraph as clearer layoutGraph - Extracts layoutGraph building from createLayoutSupervisor - Now restarts worker layouts when "graphImported" events are emitted
1 parent 15db57b commit ec69a28

File tree

1 file changed

+174
-98
lines changed
  • packages/gephi-lite/src/core/layouts

1 file changed

+174
-98
lines changed

packages/gephi-lite/src/core/layouts/index.ts

Lines changed: 174 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
1-
import { CoordinateGetter } from "@gephi/gephi-lite-sdk";
1+
import { CoordinateGetter, DEFAULT_EDGE_SIZE, DEFAULT_NODE_SIZE, StaticDynamicItemData } from "@gephi/gephi-lite-sdk";
22
import { Producer, asyncAction, atom, derivedAtom, producerToAction } from "@ouestware/atoms";
3-
import Graph from "graphology";
3+
import Graph, { MultiGraph } from "graphology";
44
import { connectedCloseness } from "graphology-metrics/layout-quality";
55
import { debounce, identity, pick } from "lodash";
66
import seedRandom from "seedrandom";
77

88
import { localStorage } from "../../utils/storage";
9+
import { VisualGetters } from "../appearance/types";
910
import { 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";
1127
import { dataGraphToFullGraph } from "../graph/utils";
1228
import { sessionAtom } from "../session";
1329
import { 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

190264
export const restartLastLayout = asyncAction(async () => {
191265
// Get the algo and its parameters
@@ -257,9 +331,11 @@ gridEnabledAtom.bindEffect((connectedClosenessSettings) => {
257331
layoutStateAtom.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

Comments
 (0)