@invana/graph-layout-d3-force 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +209 -0
- package/dist/index.js +255 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Layout } from '@invana/canvas';
|
|
2
|
+
import { GraphNode, GraphLayer } from '@invana/graph';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `D3ForceLayout` options. Every field maps 1:1 to a d3-force setter
|
|
6
|
+
* documented at https://d3js.org/d3-force.
|
|
7
|
+
*
|
|
8
|
+
* **All options default to `undefined`.** A force is only added to the
|
|
9
|
+
* simulation when its option is provided. A setter is only called when
|
|
10
|
+
* its sub-option is provided. Anything omitted falls through to
|
|
11
|
+
* d3-force's own defaults — or, for forces themselves, is not added at
|
|
12
|
+
* all.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* new D3ForceLayout({
|
|
16
|
+
* charge: {}, // adds forceManyBody at d3 defaults
|
|
17
|
+
* link: { distance: 80 }, // adds forceLink, override distance
|
|
18
|
+
* center: { x: 0, y: 0 }, // adds forceCenter at (0, 0)
|
|
19
|
+
* // no `collide` → no collision force
|
|
20
|
+
* // no `alphaDecay` → d3 default decay rate
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
interface D3ForceLayoutOptions {
|
|
24
|
+
/**
|
|
25
|
+
* When `true` (default), positions are written back to the store on
|
|
26
|
+
* every d3-force tick — the renderer animates the simulation as it
|
|
27
|
+
* settles.
|
|
28
|
+
*
|
|
29
|
+
* When `false`, per-tick writeback is suppressed and positions are
|
|
30
|
+
* flushed to the store exactly once when the simulation settles
|
|
31
|
+
* (`sim.on('end')`). The simulation still runs to completion; only the
|
|
32
|
+
* mirrored renderer updates are skipped. For large graphs (thousands
|
|
33
|
+
* of nodes / tens of thousands of edges) this avoids the ~hundreds of
|
|
34
|
+
* intermediate `setPositionsBulk` → `node:update` → renderer storms
|
|
35
|
+
* that dominate cost — the run finishes noticeably faster and the
|
|
36
|
+
* viewer just sees the settled picture appear.
|
|
37
|
+
*
|
|
38
|
+
* Lifecycle `tick` events are still emitted in both modes — only the
|
|
39
|
+
* store writeback is gated.
|
|
40
|
+
*
|
|
41
|
+
* Default `true`.
|
|
42
|
+
*/
|
|
43
|
+
animate?: boolean;
|
|
44
|
+
/** `simulation.alpha(alpha)`. */
|
|
45
|
+
alpha?: number;
|
|
46
|
+
/** `simulation.alphaMin(min)`. */
|
|
47
|
+
alphaMin?: number;
|
|
48
|
+
/** `simulation.alphaDecay(decay)`. */
|
|
49
|
+
alphaDecay?: number;
|
|
50
|
+
/** `simulation.alphaTarget(target)`. */
|
|
51
|
+
alphaTarget?: number;
|
|
52
|
+
/** `simulation.velocityDecay(decay)`. */
|
|
53
|
+
velocityDecay?: number;
|
|
54
|
+
/** `forceLink` — pulls connected nodes toward a target distance. */
|
|
55
|
+
link?: LinkForceOptions;
|
|
56
|
+
/** `forceManyBody` — n-body charge (negative = repulsion). */
|
|
57
|
+
charge?: ChargeForceOptions;
|
|
58
|
+
/** `forceCenter` — translates the cluster's centroid to `(x, y)`. */
|
|
59
|
+
center?: CenterForceOptions;
|
|
60
|
+
/** `forceCollide` — prevents overlap. */
|
|
61
|
+
collide?: CollideForceOptions;
|
|
62
|
+
/** `forceX` — positioning force along x. */
|
|
63
|
+
x?: PositionXForceOptions;
|
|
64
|
+
/** `forceY` — positioning force along y. */
|
|
65
|
+
y?: PositionYForceOptions;
|
|
66
|
+
/** `forceRadial` — pulls toward a circle of given radius. Requires `radius`. */
|
|
67
|
+
radial?: RadialForceOptions;
|
|
68
|
+
}
|
|
69
|
+
/** `forceLink` configuration. */
|
|
70
|
+
interface LinkForceOptions {
|
|
71
|
+
/** `link.distance(d)`. */
|
|
72
|
+
distance?: number;
|
|
73
|
+
/** `link.strength(s)`. */
|
|
74
|
+
strength?: number;
|
|
75
|
+
/** `link.iterations(n)`. */
|
|
76
|
+
iterations?: number;
|
|
77
|
+
}
|
|
78
|
+
/** `forceManyBody` configuration. */
|
|
79
|
+
interface ChargeForceOptions {
|
|
80
|
+
/** `manyBody.strength(s)` — negative repels, positive attracts. */
|
|
81
|
+
strength?: number;
|
|
82
|
+
/** `manyBody.theta(θ)` — Barnes–Hut accuracy threshold. */
|
|
83
|
+
theta?: number;
|
|
84
|
+
/** `manyBody.distanceMin(d)`. */
|
|
85
|
+
distanceMin?: number;
|
|
86
|
+
/** `manyBody.distanceMax(d)`. */
|
|
87
|
+
distanceMax?: number;
|
|
88
|
+
}
|
|
89
|
+
/** `forceCenter` configuration. */
|
|
90
|
+
interface CenterForceOptions {
|
|
91
|
+
/** `center.x(x)`. */
|
|
92
|
+
x?: number;
|
|
93
|
+
/** `center.y(y)`. */
|
|
94
|
+
y?: number;
|
|
95
|
+
/** `center.strength(s)`. */
|
|
96
|
+
strength?: number;
|
|
97
|
+
}
|
|
98
|
+
/** `forceCollide` configuration. */
|
|
99
|
+
interface CollideForceOptions {
|
|
100
|
+
/**
|
|
101
|
+
* `collide.radius(r)`. Either a constant, or a per-node function called
|
|
102
|
+
* once per node at `apply()` time with the underlying `GraphNode`. Use
|
|
103
|
+
* the function form when collision sizes vary per node (e.g. read
|
|
104
|
+
* `node.data.size`).
|
|
105
|
+
*/
|
|
106
|
+
radius?: number | ((node: GraphNode) => number);
|
|
107
|
+
/** `collide.strength(s)` in `[0, 1]`. */
|
|
108
|
+
strength?: number;
|
|
109
|
+
/** `collide.iterations(n)`. */
|
|
110
|
+
iterations?: number;
|
|
111
|
+
}
|
|
112
|
+
/** `forceX` configuration. */
|
|
113
|
+
interface PositionXForceOptions {
|
|
114
|
+
/** `forceX.x(x)`. */
|
|
115
|
+
x?: number;
|
|
116
|
+
/** `forceX.strength(s)`. */
|
|
117
|
+
strength?: number;
|
|
118
|
+
}
|
|
119
|
+
/** `forceY` configuration. */
|
|
120
|
+
interface PositionYForceOptions {
|
|
121
|
+
/** `forceY.y(y)`. */
|
|
122
|
+
y?: number;
|
|
123
|
+
/** `forceY.strength(s)`. */
|
|
124
|
+
strength?: number;
|
|
125
|
+
}
|
|
126
|
+
/** `forceRadial` configuration. `radius` is required. */
|
|
127
|
+
interface RadialForceOptions {
|
|
128
|
+
/** Target circle radius. */
|
|
129
|
+
radius: number;
|
|
130
|
+
/** Circle center x. */
|
|
131
|
+
x?: number;
|
|
132
|
+
/** Circle center y. */
|
|
133
|
+
y?: number;
|
|
134
|
+
/** `radial.strength(s)`. */
|
|
135
|
+
strength?: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `D3ForceLayout` — d3-force-directed `Layout` for `@invana/graph`.
|
|
140
|
+
*
|
|
141
|
+
* The flow is intentionally tiny:
|
|
142
|
+
* 1. Snapshot nodes + edges from `layer.store` into d3-force datums.
|
|
143
|
+
* 2. Build a simulation. d3 owns the tick loop.
|
|
144
|
+
* 3. On each tick, bulk-write positions back to the store; the store
|
|
145
|
+
* emits `node:update` events and the renderer reacts on its own.
|
|
146
|
+
* 4. Listen to external `node:update` (e.g. a drag) and mirror new
|
|
147
|
+
* positions onto the sim, reheating α so neighbours readjust.
|
|
148
|
+
*
|
|
149
|
+
* Every option defaults to `undefined`. A force is only added when its
|
|
150
|
+
* option is provided; a setter is only called when its sub-option is
|
|
151
|
+
* provided. d3-force's own defaults apply otherwise. See `./types`.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* const layout = new D3ForceLayout({
|
|
155
|
+
* charge: { strength: -300 },
|
|
156
|
+
* link: { distance: 80 },
|
|
157
|
+
* center: { x: 0, y: 0 },
|
|
158
|
+
* });
|
|
159
|
+
* await layout.apply(graphLayer);
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
declare class D3ForceLayout extends Layout<GraphLayer> {
|
|
163
|
+
private readonly opts;
|
|
164
|
+
private sim;
|
|
165
|
+
private nodes;
|
|
166
|
+
private ids;
|
|
167
|
+
private nodeById;
|
|
168
|
+
/** GraphNode snapshot indexed by id — used by per-node force callbacks
|
|
169
|
+
* (e.g. `collide.radius(d => ...)`) without coupling SimNode to GraphNode. */
|
|
170
|
+
private graphNodeById;
|
|
171
|
+
/** Ids of nodes whose `GraphNode.pinned === true` — permanent pins from
|
|
172
|
+
* user data. Driven via d3-force's `fx/fy` so the simulation keeps them
|
|
173
|
+
* fixed. Live: pin/unpin patches on `node:update` add/remove entries. */
|
|
174
|
+
private pinnedIds;
|
|
175
|
+
/** Ids of nodes currently being dragged by a user behaviour. Populated
|
|
176
|
+
* on `node:drag-start` from the layer, drained on `node:drag-end`. While
|
|
177
|
+
* an id is in this set, position updates mirror onto `fx/fy` so the
|
|
178
|
+
* simulation can't push the node away from the cursor. On drag-end the
|
|
179
|
+
* transient `fx/fy` clears (unless the node is also in `pinnedIds`,
|
|
180
|
+
* which is the permanent-pin path). Decoupled from `pinned` so a drag
|
|
181
|
+
* never mutates user-data semantics — pin-on-release is opt-in via a
|
|
182
|
+
* separate behaviour. */
|
|
183
|
+
private draggedIds;
|
|
184
|
+
private buffer;
|
|
185
|
+
/** True while our own bulk write is in-flight, so the `node:update`
|
|
186
|
+
* events it triggers don't bounce back into the sim. Relies on the
|
|
187
|
+
* store's default sync flush firing events inside the bulk call. */
|
|
188
|
+
private writing;
|
|
189
|
+
private unsubscribe;
|
|
190
|
+
private offDragStart;
|
|
191
|
+
private offDragEnd;
|
|
192
|
+
/** True while a run is active. Guards `stop()` so it only emits `end`
|
|
193
|
+
* once per run, even if called externally after a natural settle. */
|
|
194
|
+
private running;
|
|
195
|
+
constructor(opts?: D3ForceLayoutOptions);
|
|
196
|
+
/**
|
|
197
|
+
* Run the layout against `layer`. Resolves when the simulation settles
|
|
198
|
+
* naturally OR is cancelled via `stop()` / a second `apply()` call.
|
|
199
|
+
* Lifecycle events (`start` / `tick` / `end`) fire around the run.
|
|
200
|
+
*/
|
|
201
|
+
apply(layer: GraphLayer): Promise<void>;
|
|
202
|
+
/** Cancel an in-flight run. Positions stay in the store. No-op when idle. */
|
|
203
|
+
stop(): void;
|
|
204
|
+
private configureForces;
|
|
205
|
+
private configureSimulation;
|
|
206
|
+
private writeBack;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export { type CenterForceOptions, type ChargeForceOptions, type CollideForceOptions, D3ForceLayout, type D3ForceLayoutOptions, type LinkForceOptions, type PositionXForceOptions, type PositionYForceOptions, type RadialForceOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide, forceX, forceY, forceRadial } from 'd3-force';
|
|
2
|
+
import { Layout } from '@invana/canvas';
|
|
3
|
+
|
|
4
|
+
// src/D3ForceLayout.ts
|
|
5
|
+
var REHEAT_ALPHA = 0.3;
|
|
6
|
+
var D3ForceLayout = class extends Layout {
|
|
7
|
+
opts;
|
|
8
|
+
sim = null;
|
|
9
|
+
nodes = [];
|
|
10
|
+
ids = [];
|
|
11
|
+
nodeById = /* @__PURE__ */ new Map();
|
|
12
|
+
/** GraphNode snapshot indexed by id — used by per-node force callbacks
|
|
13
|
+
* (e.g. `collide.radius(d => ...)`) without coupling SimNode to GraphNode. */
|
|
14
|
+
graphNodeById = /* @__PURE__ */ new Map();
|
|
15
|
+
/** Ids of nodes whose `GraphNode.pinned === true` — permanent pins from
|
|
16
|
+
* user data. Driven via d3-force's `fx/fy` so the simulation keeps them
|
|
17
|
+
* fixed. Live: pin/unpin patches on `node:update` add/remove entries. */
|
|
18
|
+
pinnedIds = /* @__PURE__ */ new Set();
|
|
19
|
+
/** Ids of nodes currently being dragged by a user behaviour. Populated
|
|
20
|
+
* on `node:drag-start` from the layer, drained on `node:drag-end`. While
|
|
21
|
+
* an id is in this set, position updates mirror onto `fx/fy` so the
|
|
22
|
+
* simulation can't push the node away from the cursor. On drag-end the
|
|
23
|
+
* transient `fx/fy` clears (unless the node is also in `pinnedIds`,
|
|
24
|
+
* which is the permanent-pin path). Decoupled from `pinned` so a drag
|
|
25
|
+
* never mutates user-data semantics — pin-on-release is opt-in via a
|
|
26
|
+
* separate behaviour. */
|
|
27
|
+
draggedIds = /* @__PURE__ */ new Set();
|
|
28
|
+
buffer = new Float32Array(0);
|
|
29
|
+
/** True while our own bulk write is in-flight, so the `node:update`
|
|
30
|
+
* events it triggers don't bounce back into the sim. Relies on the
|
|
31
|
+
* store's default sync flush firing events inside the bulk call. */
|
|
32
|
+
writing = false;
|
|
33
|
+
unsubscribe = null;
|
|
34
|
+
offDragStart = null;
|
|
35
|
+
offDragEnd = null;
|
|
36
|
+
/** True while a run is active. Guards `stop()` so it only emits `end`
|
|
37
|
+
* once per run, even if called externally after a natural settle. */
|
|
38
|
+
running = false;
|
|
39
|
+
constructor(opts = {}) {
|
|
40
|
+
super();
|
|
41
|
+
this.opts = opts;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run the layout against `layer`. Resolves when the simulation settles
|
|
45
|
+
* naturally OR is cancelled via `stop()` / a second `apply()` call.
|
|
46
|
+
* Lifecycle events (`start` / `tick` / `end`) fire around the run.
|
|
47
|
+
*/
|
|
48
|
+
async apply(layer) {
|
|
49
|
+
this.stop();
|
|
50
|
+
const store = layer.store;
|
|
51
|
+
this.nodes = [];
|
|
52
|
+
this.ids = [];
|
|
53
|
+
this.nodeById.clear();
|
|
54
|
+
this.graphNodeById.clear();
|
|
55
|
+
this.pinnedIds.clear();
|
|
56
|
+
this.draggedIds.clear();
|
|
57
|
+
for (const n of store.nodes()) {
|
|
58
|
+
const pos = store.getPosition(n.id);
|
|
59
|
+
const node = { id: n.id };
|
|
60
|
+
if (n.pinned) {
|
|
61
|
+
const px = pos?.x ?? 0;
|
|
62
|
+
const py = pos?.y ?? 0;
|
|
63
|
+
node.fx = px;
|
|
64
|
+
node.fy = py;
|
|
65
|
+
node.x = px;
|
|
66
|
+
node.y = py;
|
|
67
|
+
this.pinnedIds.add(n.id);
|
|
68
|
+
} else if (pos && (pos.x !== 0 || pos.y !== 0)) {
|
|
69
|
+
node.x = pos.x;
|
|
70
|
+
node.y = pos.y;
|
|
71
|
+
}
|
|
72
|
+
this.nodes.push(node);
|
|
73
|
+
this.ids.push(n.id);
|
|
74
|
+
this.nodeById.set(n.id, node);
|
|
75
|
+
this.graphNodeById.set(n.id, n);
|
|
76
|
+
}
|
|
77
|
+
if (this.nodes.length === 0) return;
|
|
78
|
+
this.buffer = new Float32Array(this.nodes.length * 2);
|
|
79
|
+
const links = [];
|
|
80
|
+
for (const e of store.edges()) {
|
|
81
|
+
links.push({ source: e.source, target: e.target });
|
|
82
|
+
}
|
|
83
|
+
const sim = forceSimulation(this.nodes);
|
|
84
|
+
this.configureForces(sim, links);
|
|
85
|
+
this.configureSimulation(sim);
|
|
86
|
+
this.sim = sim;
|
|
87
|
+
const animate = this.opts.animate ?? true;
|
|
88
|
+
sim.on("tick", () => {
|
|
89
|
+
if (animate) this.writeBack(store);
|
|
90
|
+
this.events.emit("tick", {});
|
|
91
|
+
});
|
|
92
|
+
this.unsubscribe = store.events.on("node:update", ({ nodeId, patch }) => {
|
|
93
|
+
if (this.writing) return;
|
|
94
|
+
const node = this.nodeById.get(nodeId);
|
|
95
|
+
if (!node) return;
|
|
96
|
+
if ("pinned" in patch) {
|
|
97
|
+
if (patch.pinned) this.pinnedIds.add(nodeId);
|
|
98
|
+
else this.pinnedIds.delete(nodeId);
|
|
99
|
+
}
|
|
100
|
+
if (!patch.position) return;
|
|
101
|
+
const locked = this.pinnedIds.has(nodeId) || this.draggedIds.has(nodeId);
|
|
102
|
+
if (locked) {
|
|
103
|
+
node.fx = patch.position.x;
|
|
104
|
+
node.fy = patch.position.y;
|
|
105
|
+
node.x = patch.position.x;
|
|
106
|
+
node.y = patch.position.y;
|
|
107
|
+
} else {
|
|
108
|
+
if (node.fx !== void 0) node.fx = void 0;
|
|
109
|
+
if (node.fy !== void 0) node.fy = void 0;
|
|
110
|
+
node.x = patch.position.x;
|
|
111
|
+
node.y = patch.position.y;
|
|
112
|
+
}
|
|
113
|
+
if (sim.alpha() < REHEAT_ALPHA) sim.alpha(REHEAT_ALPHA).restart();
|
|
114
|
+
});
|
|
115
|
+
this.offDragStart = layer.events.on("node:drag-start", ({ nodeId, nodeIds }) => {
|
|
116
|
+
for (const id of nodeIds ?? [nodeId]) {
|
|
117
|
+
const node = this.nodeById.get(id);
|
|
118
|
+
if (!node) continue;
|
|
119
|
+
this.draggedIds.add(id);
|
|
120
|
+
const pos = store.getNode(id)?.position;
|
|
121
|
+
if (pos) {
|
|
122
|
+
node.fx = pos.x;
|
|
123
|
+
node.fy = pos.y;
|
|
124
|
+
node.x = pos.x;
|
|
125
|
+
node.y = pos.y;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (sim.alpha() < REHEAT_ALPHA) sim.alpha(REHEAT_ALPHA).restart();
|
|
129
|
+
});
|
|
130
|
+
this.offDragEnd = layer.events.on("node:drag-end", ({ nodeId, nodeIds }) => {
|
|
131
|
+
for (const id of nodeIds ?? [nodeId]) {
|
|
132
|
+
this.draggedIds.delete(id);
|
|
133
|
+
const node = this.nodeById.get(id);
|
|
134
|
+
if (!node) continue;
|
|
135
|
+
if (!this.pinnedIds.has(id)) {
|
|
136
|
+
node.fx = void 0;
|
|
137
|
+
node.fy = void 0;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
this.running = true;
|
|
142
|
+
this.events.emit("start", {});
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
sim.on("end", () => {
|
|
145
|
+
if (this.running) {
|
|
146
|
+
if (!animate) this.writeBack(store);
|
|
147
|
+
this.running = false;
|
|
148
|
+
this.events.emit("end", { reason: "completed" });
|
|
149
|
+
}
|
|
150
|
+
resolve();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** Cancel an in-flight run. Positions stay in the store. No-op when idle. */
|
|
155
|
+
stop() {
|
|
156
|
+
const wasRunning = this.running;
|
|
157
|
+
this.running = false;
|
|
158
|
+
this.sim?.stop();
|
|
159
|
+
this.sim = null;
|
|
160
|
+
this.unsubscribe?.();
|
|
161
|
+
this.unsubscribe = null;
|
|
162
|
+
this.offDragStart?.();
|
|
163
|
+
this.offDragStart = null;
|
|
164
|
+
this.offDragEnd?.();
|
|
165
|
+
this.offDragEnd = null;
|
|
166
|
+
this.nodes = [];
|
|
167
|
+
this.ids = [];
|
|
168
|
+
this.nodeById.clear();
|
|
169
|
+
this.graphNodeById.clear();
|
|
170
|
+
this.pinnedIds.clear();
|
|
171
|
+
this.draggedIds.clear();
|
|
172
|
+
if (wasRunning) this.events.emit("end", { reason: "stopped" });
|
|
173
|
+
}
|
|
174
|
+
// ─── Configuration ─────────────────────────────────────────────────────
|
|
175
|
+
configureForces(sim, links) {
|
|
176
|
+
const { link, charge, center, collide, x, y, radial } = this.opts;
|
|
177
|
+
if (link !== void 0) {
|
|
178
|
+
const force = forceLink(links).id((d) => d.id);
|
|
179
|
+
if (link.distance !== void 0) force.distance(link.distance);
|
|
180
|
+
if (link.strength !== void 0) force.strength(link.strength);
|
|
181
|
+
if (link.iterations !== void 0) force.iterations(link.iterations);
|
|
182
|
+
sim.force("link", force);
|
|
183
|
+
}
|
|
184
|
+
if (charge !== void 0) {
|
|
185
|
+
const force = forceManyBody();
|
|
186
|
+
if (charge.strength !== void 0) force.strength(charge.strength);
|
|
187
|
+
if (charge.theta !== void 0) force.theta(charge.theta);
|
|
188
|
+
if (charge.distanceMin !== void 0) force.distanceMin(charge.distanceMin);
|
|
189
|
+
if (charge.distanceMax !== void 0) force.distanceMax(charge.distanceMax);
|
|
190
|
+
sim.force("charge", force);
|
|
191
|
+
}
|
|
192
|
+
if (center !== void 0) {
|
|
193
|
+
const force = forceCenter(center.x ?? 0, center.y ?? 0);
|
|
194
|
+
if (center.strength !== void 0) force.strength(center.strength);
|
|
195
|
+
sim.force("center", force);
|
|
196
|
+
}
|
|
197
|
+
if (collide !== void 0) {
|
|
198
|
+
const force = forceCollide();
|
|
199
|
+
if (collide.radius !== void 0) {
|
|
200
|
+
if (typeof collide.radius === "function") {
|
|
201
|
+
const fn = collide.radius;
|
|
202
|
+
const refs = this.graphNodeById;
|
|
203
|
+
force.radius((d) => {
|
|
204
|
+
const node = refs.get(d.id);
|
|
205
|
+
return node ? fn(node) : 0;
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
force.radius(collide.radius);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (collide.strength !== void 0) force.strength(collide.strength);
|
|
212
|
+
if (collide.iterations !== void 0) force.iterations(collide.iterations);
|
|
213
|
+
sim.force("collide", force);
|
|
214
|
+
}
|
|
215
|
+
if (x !== void 0) {
|
|
216
|
+
const force = forceX();
|
|
217
|
+
if (x.x !== void 0) force.x(x.x);
|
|
218
|
+
if (x.strength !== void 0) force.strength(x.strength);
|
|
219
|
+
sim.force("x", force);
|
|
220
|
+
}
|
|
221
|
+
if (y !== void 0) {
|
|
222
|
+
const force = forceY();
|
|
223
|
+
if (y.y !== void 0) force.y(y.y);
|
|
224
|
+
if (y.strength !== void 0) force.strength(y.strength);
|
|
225
|
+
sim.force("y", force);
|
|
226
|
+
}
|
|
227
|
+
if (radial !== void 0) {
|
|
228
|
+
const force = forceRadial(radial.radius, radial.x ?? 0, radial.y ?? 0);
|
|
229
|
+
if (radial.strength !== void 0) force.strength(radial.strength);
|
|
230
|
+
sim.force("radial", force);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
configureSimulation(sim) {
|
|
234
|
+
const { alpha, alphaMin, alphaDecay, alphaTarget, velocityDecay } = this.opts;
|
|
235
|
+
if (alpha !== void 0) sim.alpha(alpha);
|
|
236
|
+
if (alphaMin !== void 0) sim.alphaMin(alphaMin);
|
|
237
|
+
if (alphaDecay !== void 0) sim.alphaDecay(alphaDecay);
|
|
238
|
+
if (alphaTarget !== void 0) sim.alphaTarget(alphaTarget);
|
|
239
|
+
if (velocityDecay !== void 0) sim.velocityDecay(velocityDecay);
|
|
240
|
+
}
|
|
241
|
+
writeBack(store) {
|
|
242
|
+
const { nodes, buffer } = this;
|
|
243
|
+
for (let i = 0, j = 0; i < nodes.length; i++, j += 2) {
|
|
244
|
+
buffer[j] = nodes[i].x;
|
|
245
|
+
buffer[j + 1] = nodes[i].y;
|
|
246
|
+
}
|
|
247
|
+
this.writing = true;
|
|
248
|
+
store.setPositionsBulk(this.ids, buffer);
|
|
249
|
+
this.writing = false;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export { D3ForceLayout };
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
255
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/D3ForceLayout.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `D3ForceLayout` — d3-force-directed `Layout` for `@invana/graph`.\n *\n * The flow is intentionally tiny:\n * 1. Snapshot nodes + edges from `layer.store` into d3-force datums.\n * 2. Build a simulation. d3 owns the tick loop.\n * 3. On each tick, bulk-write positions back to the store; the store\n * emits `node:update` events and the renderer reacts on its own.\n * 4. Listen to external `node:update` (e.g. a drag) and mirror new\n * positions onto the sim, reheating α so neighbours readjust.\n *\n * Every option defaults to `undefined`. A force is only added when its\n * option is provided; a setter is only called when its sub-option is\n * provided. d3-force's own defaults apply otherwise. See `./types`.\n *\n * @example\n * const layout = new D3ForceLayout({\n * charge: { strength: -300 },\n * link: { distance: 80 },\n * center: { x: 0, y: 0 },\n * });\n * await layout.apply(graphLayer);\n */\n\nimport {\n forceCenter,\n forceCollide,\n forceLink,\n forceManyBody,\n forceRadial,\n forceSimulation,\n forceX,\n forceY,\n type Simulation,\n type SimulationLinkDatum,\n type SimulationNodeDatum,\n} from 'd3-force';\n\nimport { Layout } from '@invana/canvas';\nimport type { GraphLayer, GraphNode } from '@invana/graph';\n\nimport type { D3ForceLayoutOptions } from './types';\n\ninterface SimNode extends SimulationNodeDatum {\n id: string;\n}\ninterface SimLink extends SimulationLinkDatum<SimNode> {}\n\n/** α target re-applied when an external `node:update` arrives. */\nconst REHEAT_ALPHA = 0.3;\n\nexport class D3ForceLayout extends Layout<GraphLayer> {\n private readonly opts: D3ForceLayoutOptions;\n private sim: Simulation<SimNode, SimLink> | null = null;\n private nodes: SimNode[] = [];\n private ids: string[] = [];\n private nodeById = new Map<string, SimNode>();\n /** GraphNode snapshot indexed by id — used by per-node force callbacks\n * (e.g. `collide.radius(d => ...)`) without coupling SimNode to GraphNode. */\n private graphNodeById = new Map<string, GraphNode>();\n /** Ids of nodes whose `GraphNode.pinned === true` — permanent pins from\n * user data. Driven via d3-force's `fx/fy` so the simulation keeps them\n * fixed. Live: pin/unpin patches on `node:update` add/remove entries. */\n private pinnedIds = new Set<string>();\n /** Ids of nodes currently being dragged by a user behaviour. Populated\n * on `node:drag-start` from the layer, drained on `node:drag-end`. While\n * an id is in this set, position updates mirror onto `fx/fy` so the\n * simulation can't push the node away from the cursor. On drag-end the\n * transient `fx/fy` clears (unless the node is also in `pinnedIds`,\n * which is the permanent-pin path). Decoupled from `pinned` so a drag\n * never mutates user-data semantics — pin-on-release is opt-in via a\n * separate behaviour. */\n private draggedIds = new Set<string>();\n private buffer = new Float32Array(0);\n /** True while our own bulk write is in-flight, so the `node:update`\n * events it triggers don't bounce back into the sim. Relies on the\n * store's default sync flush firing events inside the bulk call. */\n private writing = false;\n private unsubscribe: (() => void) | null = null;\n private offDragStart: (() => void) | null = null;\n private offDragEnd: (() => void) | null = null;\n /** True while a run is active. Guards `stop()` so it only emits `end`\n * once per run, even if called externally after a natural settle. */\n private running = false;\n\n constructor(opts: D3ForceLayoutOptions = {}) {\n super();\n this.opts = opts;\n }\n\n /**\n * Run the layout against `layer`. Resolves when the simulation settles\n * naturally OR is cancelled via `stop()` / a second `apply()` call.\n * Lifecycle events (`start` / `tick` / `end`) fire around the run.\n */\n async apply(layer: GraphLayer): Promise<void> {\n this.stop();\n const store = layer.store;\n\n // 1. Snapshot store → sim datums.\n this.nodes = [];\n this.ids = [];\n this.nodeById.clear();\n this.graphNodeById.clear();\n this.pinnedIds.clear();\n this.draggedIds.clear();\n for (const n of store.nodes()) {\n const pos = store.getPosition(n.id);\n const node: SimNode = { id: n.id };\n if (n.pinned) {\n // Pinned nodes use d3-force's `fx/fy` so the sim treats them as\n // immovable but still applies their forces to neighbours\n // (e.g. a cursor-follower that pushes other nodes via collide).\n const px = pos?.x ?? 0;\n const py = pos?.y ?? 0;\n node.fx = px;\n node.fy = py;\n node.x = px;\n node.y = py;\n this.pinnedIds.add(n.id);\n } else if (pos && (pos.x !== 0 || pos.y !== 0)) {\n // Only seed if the store has a real position. (0, 0) is treated as\n // the typed-array default; leaving x/y `undefined` lets\n // `forceSimulation` phyllotaxis-scatter the cluster — without it,\n // all-colocated nodes can't break their tie under d3 defaults.\n node.x = pos.x;\n node.y = pos.y;\n }\n this.nodes.push(node);\n this.ids.push(n.id);\n this.nodeById.set(n.id, node);\n this.graphNodeById.set(n.id, n);\n }\n if (this.nodes.length === 0) return;\n this.buffer = new Float32Array(this.nodes.length * 2);\n\n const links: SimLink[] = [];\n for (const e of store.edges()) {\n links.push({ source: e.source, target: e.target });\n }\n\n // 2. Build simulation + apply only options the user provided.\n const sim = forceSimulation<SimNode, SimLink>(this.nodes);\n this.configureForces(sim, links);\n this.configureSimulation(sim);\n this.sim = sim;\n\n // 3. Each d3 tick → optionally bulk write to store + emit lifecycle\n // `tick`. When `animate` is `false` we skip the per-tick store\n // writeback (and the renderer storm that follows) and defer to a\n // single writeback in the natural-end handler below — see\n // `D3ForceLayoutOptions.animate` for the rationale.\n const animate = this.opts.animate ?? true;\n sim.on('tick', () => {\n if (animate) this.writeBack(store);\n this.events.emit('tick', {});\n });\n\n // 4. External writes (drag, cursor-follower, etc.) → mirror onto sim,\n // reheat. Nodes that are either permanently pinned (`pinnedIds`,\n // user-data semantics) or transiently locked by an in-flight drag\n // (`draggedIds`, signalled by `node:drag-start` / `node:drag-end`\n // on the layer's events) write to `fx/fy` so the simulation can't\n // push them away from the supplied position. Otherwise the update\n // flows into `x/y` and the next force tick may move the node.\n //\n // Pin patches are tracked live so a mid-run flip (e.g. a feed\n // enabling `pinned: true` on a node) takes effect on the next\n // `node:update` without needing a fresh `apply()`.\n this.unsubscribe = store.events.on('node:update', ({ nodeId, patch }) => {\n if (this.writing) return;\n const node = this.nodeById.get(nodeId);\n if (!node) return;\n\n if ('pinned' in patch) {\n if (patch.pinned) this.pinnedIds.add(nodeId);\n else this.pinnedIds.delete(nodeId);\n }\n\n if (!patch.position) return;\n const locked =\n this.pinnedIds.has(nodeId) || this.draggedIds.has(nodeId);\n if (locked) {\n node.fx = patch.position.x;\n node.fy = patch.position.y;\n // Mirror onto `x/y` so rendered position matches before the next\n // force tick — `forceCenter`/`forceX`/`forceY` read `x/y`.\n node.x = patch.position.x;\n node.y = patch.position.y;\n } else {\n // Free node: ensure no stale `fx/fy` are holding it.\n if (node.fx !== undefined) node.fx = undefined as unknown as number;\n if (node.fy !== undefined) node.fy = undefined as unknown as number;\n node.x = patch.position.x;\n node.y = patch.position.y;\n }\n if (sim.alpha() < REHEAT_ALPHA) sim.alpha(REHEAT_ALPHA).restart();\n });\n\n // 4b. Subscribe to drag lifecycle on the layer. `node:drag-start` puts\n // the node in `draggedIds` (so subsequent position updates lock via\n // `fx/fy`) and reheats the sim. `node:drag-end` removes it and —\n // unless the node is also permanently pinned — clears `fx/fy` so\n // forces can move it again. The store's `pinned` flag is never\n // touched here; permanent pin-on-release is a separate behaviour's\n // concern.\n // `nodeIds` carries every primary being dragged (a multi-selection drag\n // moves them all); fall back to `[nodeId]` for safety. Clamp / release\n // each one's `fx/fy` independently.\n this.offDragStart = layer.events.on('node:drag-start', ({ nodeId, nodeIds }) => {\n for (const id of nodeIds ?? [nodeId]) {\n const node = this.nodeById.get(id);\n if (!node) continue;\n this.draggedIds.add(id);\n const pos = store.getNode(id)?.position;\n if (pos) {\n node.fx = pos.x;\n node.fy = pos.y;\n node.x = pos.x;\n node.y = pos.y;\n }\n }\n if (sim.alpha() < REHEAT_ALPHA) sim.alpha(REHEAT_ALPHA).restart();\n });\n this.offDragEnd = layer.events.on('node:drag-end', ({ nodeId, nodeIds }) => {\n for (const id of nodeIds ?? [nodeId]) {\n this.draggedIds.delete(id);\n const node = this.nodeById.get(id);\n if (!node) continue;\n if (!this.pinnedIds.has(id)) {\n node.fx = undefined as unknown as number;\n node.fy = undefined as unknown as number;\n }\n }\n });\n\n // 5. Mark run as active and announce `start` after wiring is in place\n // so handlers see a fully-initialised layout.\n this.running = true;\n this.events.emit('start', {});\n\n // 6. Resolve on natural settle. The `end` emission happens here (not in\n // `stop()`) so a natural settle reports `reason: 'completed'`;\n // external `stop()` flips `running` first and emits `'stopped'`.\n return new Promise<void>((resolve) => {\n sim.on('end', () => {\n if (this.running) {\n // `animate: false` runs deferred every per-tick writeback —\n // flush the final settled positions now so the renderer\n // actually sees the layout result.\n if (!animate) this.writeBack(store);\n this.running = false;\n this.events.emit('end', { reason: 'completed' });\n }\n resolve();\n });\n });\n }\n\n /** Cancel an in-flight run. Positions stay in the store. No-op when idle. */\n stop(): void {\n const wasRunning = this.running;\n this.running = false;\n this.sim?.stop();\n this.sim = null;\n this.unsubscribe?.();\n this.unsubscribe = null;\n this.offDragStart?.();\n this.offDragStart = null;\n this.offDragEnd?.();\n this.offDragEnd = null;\n this.nodes = [];\n this.ids = [];\n this.nodeById.clear();\n this.graphNodeById.clear();\n this.pinnedIds.clear();\n this.draggedIds.clear();\n if (wasRunning) this.events.emit('end', { reason: 'stopped' });\n }\n\n // ─── Configuration ─────────────────────────────────────────────────────\n\n private configureForces(sim: Simulation<SimNode, SimLink>, links: SimLink[]): void {\n const { link, charge, center, collide, x, y, radial } = this.opts;\n\n if (link !== undefined) {\n const force = forceLink<SimNode, SimLink>(links).id((d) => d.id);\n if (link.distance !== undefined) force.distance(link.distance);\n if (link.strength !== undefined) force.strength(link.strength);\n if (link.iterations !== undefined) force.iterations(link.iterations);\n sim.force('link', force);\n }\n\n if (charge !== undefined) {\n const force = forceManyBody<SimNode>();\n if (charge.strength !== undefined) force.strength(charge.strength);\n if (charge.theta !== undefined) force.theta(charge.theta);\n if (charge.distanceMin !== undefined) force.distanceMin(charge.distanceMin);\n if (charge.distanceMax !== undefined) force.distanceMax(charge.distanceMax);\n sim.force('charge', force);\n }\n\n if (center !== undefined) {\n const force = forceCenter<SimNode>(center.x ?? 0, center.y ?? 0);\n if (center.strength !== undefined) force.strength(center.strength);\n sim.force('center', force);\n }\n\n if (collide !== undefined) {\n const force = forceCollide<SimNode>();\n if (collide.radius !== undefined) {\n if (typeof collide.radius === 'function') {\n const fn = collide.radius;\n const refs = this.graphNodeById;\n force.radius((d) => {\n const node = refs.get(d.id);\n return node ? fn(node) : 0;\n });\n } else {\n force.radius(collide.radius);\n }\n }\n if (collide.strength !== undefined) force.strength(collide.strength);\n if (collide.iterations !== undefined) force.iterations(collide.iterations);\n sim.force('collide', force);\n }\n\n if (x !== undefined) {\n const force = forceX<SimNode>();\n if (x.x !== undefined) force.x(x.x);\n if (x.strength !== undefined) force.strength(x.strength);\n sim.force('x', force);\n }\n\n if (y !== undefined) {\n const force = forceY<SimNode>();\n if (y.y !== undefined) force.y(y.y);\n if (y.strength !== undefined) force.strength(y.strength);\n sim.force('y', force);\n }\n\n if (radial !== undefined) {\n const force = forceRadial<SimNode>(radial.radius, radial.x ?? 0, radial.y ?? 0);\n if (radial.strength !== undefined) force.strength(radial.strength);\n sim.force('radial', force);\n }\n }\n\n private configureSimulation(sim: Simulation<SimNode, SimLink>): void {\n const { alpha, alphaMin, alphaDecay, alphaTarget, velocityDecay } = this.opts;\n if (alpha !== undefined) sim.alpha(alpha);\n if (alphaMin !== undefined) sim.alphaMin(alphaMin);\n if (alphaDecay !== undefined) sim.alphaDecay(alphaDecay);\n if (alphaTarget !== undefined) sim.alphaTarget(alphaTarget);\n if (velocityDecay !== undefined) sim.velocityDecay(velocityDecay);\n }\n\n private writeBack(store: GraphLayer['store']): void {\n const { nodes, buffer } = this;\n for (let i = 0, j = 0; i < nodes.length; i++, j += 2) {\n buffer[j] = nodes[i]!.x!;\n buffer[j + 1] = nodes[i]!.y!;\n }\n this.writing = true;\n store.setPositionsBulk(this.ids, buffer);\n this.writing = false;\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@invana/graph-layout-d3-force",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "D3 force-directed Layout for @invana/graph.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"typedocOptions": {
|
|
9
|
+
"entryPoints": [
|
|
10
|
+
"src/index.ts"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"d3-force": "^3.0.0"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@invana/graph": "0.0.2",
|
|
18
|
+
"@invana/canvas": "0.0.2"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/d3-force": "^3.0.10",
|
|
22
|
+
"tsup": "^8.3.5",
|
|
23
|
+
"typescript": "5.9.2",
|
|
24
|
+
"vitest": "^2.1.8",
|
|
25
|
+
"@invana/canvas": "0.0.2",
|
|
26
|
+
"@invana/graph": "0.0.2",
|
|
27
|
+
"@repo/typescript-config": "0.0.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"canvas",
|
|
31
|
+
"graph",
|
|
32
|
+
"layout",
|
|
33
|
+
"d3",
|
|
34
|
+
"d3-force",
|
|
35
|
+
"force-directed"
|
|
36
|
+
],
|
|
37
|
+
"license": "Apache-2.0",
|
|
38
|
+
"module": "./dist/index.js",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./dist/index.js",
|
|
43
|
+
"default": "./dist/index.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"dev": "tsup --watch",
|
|
55
|
+
"lint": "eslint src/",
|
|
56
|
+
"check-types": "tsc --noEmit",
|
|
57
|
+
"clean": "rm -rf dist",
|
|
58
|
+
"test": "vitest run"
|
|
59
|
+
}
|
|
60
|
+
}
|