Skip to content

Commit d279f29

Browse files
committed
[gephi-lite] adding layout controller on graph
On the graph page, you can start/stop/run the last used layout, directly on the graph. The lastUsedLayout is store in the session using the corresponding atom. Now when you on a layout setting page : - you can close it even if the algo is running (the close button is on top of the loader) - you can't change the parameters when an algo is running Moreover, I review the node drag'n'drop feature to restart the algo while you drag.
1 parent 046fcf0 commit d279f29

15 files changed

Lines changed: 300 additions & 109 deletions

File tree

packages/gephi-lite/src/components/Loader.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ export const Loader: FC = () => (
2323
/**
2424
* Display a loader that takes the size of its parent container.
2525
*/
26-
export const LoaderFill: FC = () => (
26+
export const LoaderFill: FC<{ message?: string }> = ({ message }) => (
2727
<div className="loader-fill">
2828
<Spinner style={{ width: "3rem", height: " 3rem" }} />
29+
{message && <p className="text-center">{message}</p>}
2930
</div>
3031
);

packages/gephi-lite/src/components/common-icons.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import {
5858
PiPaintBrush,
5959
PiPalette,
6060
PiPaletteFill,
61+
PiPause,
62+
PiPauseFill,
6163
PiPencilSimpleLine,
6264
PiPencilSimpleLineFill,
6365
PiPlay,
@@ -71,6 +73,8 @@ import {
7173
PiSelection,
7274
PiSelectionBold,
7375
PiSignIn,
76+
PiSkipForwardFill,
77+
PiSkipForwardLight,
7478
PiSpinner,
7579
PiSquare,
7680
PiStop,
@@ -152,6 +156,10 @@ export const MouseIconFill = PiCursorFill;
152156
export const OpenInGraphIcon = PiCrosshair;
153157
export const PlayIcon = PiPlay;
154158
export const PlayIconFill = PiPlayFill;
159+
export const PlaySyncIcon = PiSkipForwardLight;
160+
export const PlaySyncIconFill = PiSkipForwardFill;
161+
export const PauseIcon = PiPause;
162+
export const PauseIconFill = PiPauseFill;
155163
export const ResetIcon = PiArrowCounterClockwise;
156164
export const RetryIcon = PiArrowClockwise;
157165
export const SearchIcon = PiMagnifyingGlass;

packages/gephi-lite/src/core/context/dataContexts.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export const useSearch = makeUseAtom(CONTEXTS.search);
147147
export const useLayoutState = makeUseAtom(CONTEXTS.layoutState);
148148
export const useUser = makeUseAtom(CONTEXTS.user);
149149
export const useDynamicItemData = makeUseAtom(CONTEXTS.dynamicItemData);
150+
export const useSessionData = makeUseAtom(CONTEXTS.session);
150151

151152
export const useSigmaActions = makeUseActions(sigmaActions);
152153
export const useFiltersActions = makeUseActions(filtersActions);
@@ -159,6 +160,7 @@ export const useSearchActions = makeUseActions(searchActions);
159160
export const useFileActions = makeUseActions(fileActions);
160161
export const useLayoutActions = makeUseActions(layoutActions);
161162
export const useUserActions = makeUseActions(userActions);
163+
export const useSessionActions = makeUseActions(sessionActions);
162164

163165
export const useResetStates = () => {
164166
return resetStates;

packages/gephi-lite/src/core/context/eventsContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const EVENTS = {
1111
nodeCreated: "nodeCreated",
1212
edgeCreated: "edgeCreated",
1313
searchResultsSelected: "searchResultsSelected",
14+
openMenu: "openMenu",
1415
} as const;
1516

1617
/**

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

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { localStorage } from "../../utils/storage";
77
import { EVENTS, emitter } from "../context/eventsContext";
88
import { graphDatasetActions, graphDatasetAtom, sigmaGraphAtom } from "../graph";
99
import { dataGraphToFullGraph } from "../graph/utils";
10+
import { sessionActions, sessionAtom } from "../session";
1011
import { resetCamera } from "../sigma";
1112
import { LAYOUTS } from "./collection";
1213
import { LayoutMapping, LayoutQuality, LayoutState } from "./types";
@@ -34,41 +35,46 @@ export const layoutStateAtom = atom<LayoutState>(getLocalStorageLayoutState());
3435
* Actions:
3536
* ********
3637
*/
37-
export const startLayout = asyncAction(async (id: string, params: unknown) => {
38+
export const startLayout = asyncAction(async (id: string, params: unknown, isForRestart = false) => {
3839
const { setNodePositions } = graphDatasetActions;
39-
const dataset = graphDatasetAtom.get();
40+
const { setLastLayoutUsed } = sessionActions;
4041

4142
// search the layout
4243
const layout = LAYOUTS.find((l) => l.id === id);
4344

44-
// Sync layout
45-
if (layout && layout.type === "sync") {
46-
layoutStateAtom.set((prev) => ({ ...prev, type: "running", layoutId: id }));
47-
48-
// generate positions
49-
const fullGraph = dataGraphToFullGraph(dataset);
50-
const positions = layout.run(fullGraph, { settings: params });
51-
52-
// Save it
53-
setNodePositions(positions);
54-
55-
// To prevent resetting the camera before sigma receives new data, we
56-
// need to wait a frame, and also wait for it to trigger a refresh:
57-
setTimeout(() => {
58-
layoutStateAtom.set((prev) => ({ ...prev, type: "idle" }));
59-
resetCamera({ forceRefresh: false });
60-
}, 0);
61-
}
62-
63-
// Async layout
64-
if (layout && layout.type === "worker") {
65-
const worker = new layout.supervisor(sigmaGraphAtom.get(), { settings: params });
66-
worker.start();
67-
layoutStateAtom.set((prev) => ({ ...prev, type: "running", layoutId: id, supervisor: worker }));
45+
if (layout) {
46+
if (!isForRestart) setLastLayoutUsed(layout.id);
47+
48+
// Sync layout
49+
if (layout.type === "sync") {
50+
layoutStateAtom.set((prev) => ({ ...prev, type: "running", layoutId: id }));
51+
52+
// generate positions
53+
const dataset = graphDatasetAtom.get();
54+
const fullGraph = dataGraphToFullGraph(dataset);
55+
const positions = layout.run(fullGraph, { settings: params });
56+
57+
// Save it
58+
setNodePositions(positions);
59+
60+
// To prevent resetting the camera before sigma receives new data, we
61+
// need to wait a frame, and also wait for it to trigger a refresh:
62+
setTimeout(() => {
63+
layoutStateAtom.set((prev) => ({ ...prev, type: "idle" }));
64+
resetCamera({ forceRefresh: false });
65+
}, 0);
66+
}
67+
68+
// Async layout
69+
if (layout.type === "worker") {
70+
const worker = new layout.supervisor(sigmaGraphAtom.get(), { settings: params });
71+
worker.start();
72+
layoutStateAtom.set((prev) => ({ ...prev, type: "running", layoutId: id, supervisor: worker }));
73+
}
6874
}
6975
});
7076

71-
export const stopLayout = asyncAction(async () => {
77+
export const stopLayout = asyncAction(async (isForRestart = false) => {
7278
const { setNodePositions } = graphDatasetActions;
7379
const layoutState = layoutStateAtom.get();
7480

@@ -77,15 +83,33 @@ export const stopLayout = asyncAction(async () => {
7783
layoutState.supervisor.stop();
7884
layoutState.supervisor.kill();
7985

80-
// Save data
81-
const positions: LayoutMapping = {};
82-
sigmaGraphAtom.get().forEachNode((node, { x, y }) => {
83-
positions[node] = { x, y };
84-
});
85-
setNodePositions(positions);
86+
// DOn't save position if it's for a restart
87+
if (!isForRestart) {
88+
// Save data
89+
const positions: LayoutMapping = {};
90+
sigmaGraphAtom.get().forEachNode((node, { x, y }) => {
91+
positions[node] = { x, y };
92+
});
93+
setNodePositions(positions);
94+
}
8695
}
8796

88-
layoutStateAtom.set((prev) => ({ ...prev, type: "idle" }));
97+
// Don't set the state if it's for restart
98+
if (!isForRestart) layoutStateAtom.set((prev) => ({ ...prev, type: "idle" }));
99+
});
100+
101+
export const restartLastLayout = asyncAction(async () => {
102+
// Get the algo and its parameters
103+
const session = sessionAtom.get();
104+
if (session.lastLayoutUsed) {
105+
const layoutId = session.lastLayoutUsed;
106+
const layout = LAYOUTS.find((e) => e.id === layoutId);
107+
const params = session.layoutsParameters[layoutId] || {};
108+
if (layout) {
109+
await stopLayout(true);
110+
await startLayout(layoutId, params, true);
111+
}
112+
}
89113
});
90114

91115
export const setQuality: Producer<LayoutState, [LayoutQuality]> = (quality) => {
@@ -105,8 +129,9 @@ const _computeLayoutQualityMetric: Producer<LayoutState> = () => {
105129
};
106130

107131
export const layoutActions = {
108-
startLayout,
109132
stopLayout,
133+
startLayout,
134+
restartLastLayout,
110135
setQuality: producerToAction(setQuality, layoutStateAtom),
111136
computeLayoutQualityMetric: producerToAction(_computeLayoutQualityMetric, layoutStateAtom),
112137
};
@@ -140,3 +165,13 @@ gridEnabledAtom.bindEffect((connectedClosenessSettings) => {
140165
sigmaGraph.off("eachNodeAttributesUpdated", fn);
141166
};
142167
});
168+
169+
layoutStateAtom.bindEffect((state) => {
170+
if (state.type !== "running") return;
171+
172+
const fnHandleDraggin = debounce(restartLastLayout, 100, { leading: true, trailing: true, maxWait: 100 });
173+
emitter.on(EVENTS.nodesDragged, fnHandleDraggin);
174+
return () => {
175+
emitter.off(EVENTS.nodesDragged, fnHandleDraggin);
176+
};
177+
});

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,30 @@ import { sessionStorage } from "../../utils/storage";
44
import { Session } from "./types";
55
import { getEmptySession, serializeSession } from "./utils";
66

7-
/**
8-
* Producers:
9-
* **********
10-
*/
11-
127
/**
138
* Public API:
149
* ***********
1510
*/
1611
export const sessionAtom = atom<Session>(getEmptySession());
1712

13+
/**
14+
* Producers:
15+
* **********
16+
*/
1817
export const reset: Producer<Session, []> = () => {
1918
return () => getEmptySession();
2019
};
2120

21+
const setLastLayoutUsed: Producer<Session, [Session["lastLayoutUsed"]]> = (lastLayoutUsed) => {
22+
return (session) => ({
23+
...session,
24+
lastLayoutUsed,
25+
});
26+
};
27+
2228
export const sessionActions = {
2329
reset: producerToAction(reset, sessionAtom),
30+
setLastLayoutUsed: producerToAction(setLastLayoutUsed, sessionAtom),
2431
};
2532

2633
/**

packages/gephi-lite/src/core/session/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface Session {
22
// for each layout, we save the parameters
3+
lastLayoutUsed?: string;
34
layoutsParameters: { [layout: string]: Record<string, unknown> };
45
// for each metrics, we save the parameters
56
metrics: {

packages/gephi-lite/src/locales/dev.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,11 @@
355355
"control": {
356356
"fullscreenEnter": "Enter fullscreen mode",
357357
"fullscreenExit": "Exit fullscreen",
358+
"layout-open-settings": "Open '{{name}}' layout settings",
359+
"layout-no-layout": "Disabled: there is no layout in your history",
360+
"layout-run-latest": "Run layout '{{name}}'",
361+
"layout-start-latest": "Start layout '{{name}}'",
362+
"layout-stop-latest": "Stop layout '{{name}}'",
358363
"zoomIn": "Zoom In",
359364
"zoomOut": "Zoom Out",
360365
"zoomReset": "See the whole graph"
@@ -506,9 +511,9 @@
506511
},
507512
"description": "This panel allows computing new coordinates to nodes of the graph. ",
508513
"exec": {
509-
"started": "Layout {{layout}} is running",
510-
"stopped": "Layout {{layout}} has been stopped",
511-
"success": "Layout \"{{layout}}\" has successfully been applied."
514+
"started": "Layout '{{layout}}' is running",
515+
"stopped": "Layout '{{layout}}' has been stopped",
516+
"success": "Layout '{{layout}}' has successfully been applied."
512517
},
513518
"fa2": {
514519
"buttons": {

packages/gephi-lite/src/locales/fr.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,10 @@
394394
"control": {
395395
"fullscreenEnter": "Activer le mode plein écran",
396396
"fullscreenExit": "Quitter le mode plein écran",
397+
"layout-run-latest": "Exécuter la spatialisation '{{name}}'",
398+
"layout-start-latest": "Démarrer la spatialisation '{{name}}'",
399+
"layout-stop-latest": "Arrêter la spatialisation '{{name}}'",
400+
"layout-open-settings": "Ouvrir le panneau de paramétrage du layout '{{name}}'",
397401
"zoomIn": "Zoom avant",
398402
"zoomOut": "Zoom arrière",
399403
"zoomReset": "Afficher tout le graphe"

packages/gephi-lite/src/styles/_loader.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,4 @@
2626

2727
.z-over-loader {
2828
z-index: 10;
29-
position: relative;
3029
}

0 commit comments

Comments
 (0)