@aiready/components 0.1.7 → 0.1.8
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/charts/ForceDirectedGraph.d.ts +15 -6
- package/dist/charts/ForceDirectedGraph.js +112 -452
- package/dist/charts/ForceDirectedGraph.js.map +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +98 -171
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/ForceDirectedGraph.tsx +148 -234
- package/src/index.ts +2 -0
|
@@ -1,277 +1,10 @@
|
|
|
1
|
-
import React, { forwardRef, useRef, useState, useEffect,
|
|
2
|
-
import * as
|
|
1
|
+
import React, { forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle } from 'react';
|
|
2
|
+
import * as d3 from 'd3';
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
4
|
import { twMerge } from 'tailwind-merge';
|
|
5
5
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
// src/charts/ForceDirectedGraph.tsx
|
|
8
|
-
function useForceSimulation(initialNodes, initialLinks, options) {
|
|
9
|
-
const {
|
|
10
|
-
chargeStrength = -300,
|
|
11
|
-
linkDistance = 100,
|
|
12
|
-
linkStrength = 1,
|
|
13
|
-
collisionStrength = 1,
|
|
14
|
-
collisionRadius = 10,
|
|
15
|
-
centerStrength = 0.1,
|
|
16
|
-
width,
|
|
17
|
-
height,
|
|
18
|
-
alphaDecay = 0.0228,
|
|
19
|
-
velocityDecay = 0.4,
|
|
20
|
-
alphaTarget = 0,
|
|
21
|
-
warmAlpha = 0.3,
|
|
22
|
-
alphaMin = 0.01,
|
|
23
|
-
// @ts-ignore allow extra option
|
|
24
|
-
stabilizeOnStop = true,
|
|
25
|
-
onTick,
|
|
26
|
-
// Optional throttle in milliseconds for tick updates (reduce React re-renders)
|
|
27
|
-
// Lower values = smoother but more CPU; default ~30ms (~33fps)
|
|
28
|
-
// @ts-ignore allow extra option
|
|
29
|
-
tickThrottleMs = 33,
|
|
30
|
-
// @ts-ignore allow extra option
|
|
31
|
-
maxSimulationTimeMs = 3e3
|
|
32
|
-
} = options;
|
|
33
|
-
const [nodes, setNodes] = useState(initialNodes);
|
|
34
|
-
const [links, setLinks] = useState(initialLinks);
|
|
35
|
-
const [isRunning, setIsRunning] = useState(false);
|
|
36
|
-
const [alpha, setAlpha] = useState(1);
|
|
37
|
-
const simulationRef = useRef(null);
|
|
38
|
-
const stopTimeoutRef = useRef(null);
|
|
39
|
-
const nodesKey = initialNodes.map((n) => n.id).join("|");
|
|
40
|
-
const linksKey = (initialLinks || []).map((l) => {
|
|
41
|
-
const s = typeof l.source === "string" ? l.source : l.source?.id;
|
|
42
|
-
const t = typeof l.target === "string" ? l.target : l.target?.id;
|
|
43
|
-
return `${s}->${t}:${l.type || ""}`;
|
|
44
|
-
}).join("|");
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
const nodesCopy = initialNodes.map((node) => ({ ...node }));
|
|
47
|
-
const linksCopy = initialLinks.map((link) => ({ ...link }));
|
|
48
|
-
try {
|
|
49
|
-
nodesCopy.forEach((n, i) => {
|
|
50
|
-
const angle = i * 2 * Math.PI / nodesCopy.length;
|
|
51
|
-
const radius = Math.min(width, height) * 0.45;
|
|
52
|
-
n.x = width / 2 + radius * Math.cos(angle);
|
|
53
|
-
n.y = height / 2 + radius * Math.sin(angle);
|
|
54
|
-
n.vx = (Math.random() - 0.5) * 2;
|
|
55
|
-
n.vy = (Math.random() - 0.5) * 2;
|
|
56
|
-
});
|
|
57
|
-
} catch (e) {
|
|
58
|
-
nodesCopy.forEach((n) => {
|
|
59
|
-
n.x = Math.random() * width;
|
|
60
|
-
n.y = Math.random() * height;
|
|
61
|
-
n.vx = (Math.random() - 0.5) * 10;
|
|
62
|
-
n.vy = (Math.random() - 0.5) * 10;
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
const simulation = d32.forceSimulation(nodesCopy);
|
|
66
|
-
try {
|
|
67
|
-
const linkForce = d32.forceLink(linksCopy);
|
|
68
|
-
linkForce.id((d) => d.id).distance((d) => d && d.distance != null ? d.distance : linkDistance).strength(linkStrength);
|
|
69
|
-
simulation.force("link", linkForce);
|
|
70
|
-
} catch (e) {
|
|
71
|
-
try {
|
|
72
|
-
simulation.force("link", d32.forceLink(linksCopy));
|
|
73
|
-
} catch (e2) {
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
simulation.force("charge", d32.forceManyBody().strength(chargeStrength));
|
|
78
|
-
simulation.force("center", d32.forceCenter(width / 2, height / 2).strength(centerStrength));
|
|
79
|
-
const collide = d32.forceCollide().radius((d) => {
|
|
80
|
-
const nodeSize = d && d.size ? d.size : 10;
|
|
81
|
-
return nodeSize + collisionRadius;
|
|
82
|
-
}).strength(collisionStrength);
|
|
83
|
-
simulation.force("collision", collide);
|
|
84
|
-
simulation.force("x", d32.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5)));
|
|
85
|
-
simulation.force("y", d32.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5)));
|
|
86
|
-
simulation.alphaDecay(alphaDecay);
|
|
87
|
-
simulation.velocityDecay(velocityDecay);
|
|
88
|
-
simulation.alphaMin(alphaMin);
|
|
89
|
-
try {
|
|
90
|
-
simulation.alphaTarget(alphaTarget);
|
|
91
|
-
} catch (e) {
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
simulation.alpha(warmAlpha);
|
|
95
|
-
} catch (e) {
|
|
96
|
-
}
|
|
97
|
-
} catch (e) {
|
|
98
|
-
}
|
|
99
|
-
simulationRef.current = simulation;
|
|
100
|
-
if (stopTimeoutRef.current != null) {
|
|
101
|
-
try {
|
|
102
|
-
globalThis.clearTimeout(stopTimeoutRef.current);
|
|
103
|
-
} catch (e) {
|
|
104
|
-
}
|
|
105
|
-
stopTimeoutRef.current = null;
|
|
106
|
-
}
|
|
107
|
-
if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
|
|
108
|
-
stopTimeoutRef.current = globalThis.setTimeout(() => {
|
|
109
|
-
try {
|
|
110
|
-
if (stabilizeOnStop) {
|
|
111
|
-
nodesCopy.forEach((n) => {
|
|
112
|
-
n.vx = 0;
|
|
113
|
-
n.vy = 0;
|
|
114
|
-
if (typeof n.x === "number") n.x = Number(n.x.toFixed(3));
|
|
115
|
-
if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
simulation.alpha(0);
|
|
119
|
-
simulation.stop();
|
|
120
|
-
} catch (e) {
|
|
121
|
-
}
|
|
122
|
-
setIsRunning(false);
|
|
123
|
-
setNodes([...nodesCopy]);
|
|
124
|
-
setLinks([...linksCopy]);
|
|
125
|
-
}, maxSimulationTimeMs);
|
|
126
|
-
}
|
|
127
|
-
let rafId = null;
|
|
128
|
-
let lastUpdate = 0;
|
|
129
|
-
const tickHandler = () => {
|
|
130
|
-
try {
|
|
131
|
-
if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
|
|
132
|
-
} catch (e) {
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
if (simulation.alpha() <= alphaMin) {
|
|
136
|
-
try {
|
|
137
|
-
if (stabilizeOnStop) {
|
|
138
|
-
nodesCopy.forEach((n) => {
|
|
139
|
-
n.vx = 0;
|
|
140
|
-
n.vy = 0;
|
|
141
|
-
if (typeof n.x === "number") n.x = Number(n.x.toFixed(3));
|
|
142
|
-
if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
simulation.stop();
|
|
146
|
-
} catch (e) {
|
|
147
|
-
}
|
|
148
|
-
setAlpha(simulation.alpha());
|
|
149
|
-
setIsRunning(false);
|
|
150
|
-
setNodes([...nodesCopy]);
|
|
151
|
-
setLinks([...linksCopy]);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
} catch (e) {
|
|
155
|
-
}
|
|
156
|
-
const now = Date.now();
|
|
157
|
-
const shouldUpdate = now - lastUpdate >= tickThrottleMs;
|
|
158
|
-
if (rafId == null && shouldUpdate) {
|
|
159
|
-
rafId = (globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 16)))(() => {
|
|
160
|
-
rafId = null;
|
|
161
|
-
lastUpdate = Date.now();
|
|
162
|
-
setNodes([...nodesCopy]);
|
|
163
|
-
setLinks([...linksCopy]);
|
|
164
|
-
setAlpha(simulation.alpha());
|
|
165
|
-
setIsRunning(simulation.alpha() > simulation.alphaMin());
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
simulation.on("tick", tickHandler);
|
|
170
|
-
simulation.on("end", () => {
|
|
171
|
-
setIsRunning(false);
|
|
172
|
-
});
|
|
173
|
-
return () => {
|
|
174
|
-
try {
|
|
175
|
-
simulation.on("tick", null);
|
|
176
|
-
} catch (e) {
|
|
177
|
-
}
|
|
178
|
-
if (stopTimeoutRef.current != null) {
|
|
179
|
-
try {
|
|
180
|
-
globalThis.clearTimeout(stopTimeoutRef.current);
|
|
181
|
-
} catch (e) {
|
|
182
|
-
}
|
|
183
|
-
stopTimeoutRef.current = null;
|
|
184
|
-
}
|
|
185
|
-
if (rafId != null) {
|
|
186
|
-
try {
|
|
187
|
-
(globalThis.cancelAnimationFrame || ((id) => clearTimeout(id)))(rafId);
|
|
188
|
-
} catch (e) {
|
|
189
|
-
}
|
|
190
|
-
rafId = null;
|
|
191
|
-
}
|
|
192
|
-
simulation.stop();
|
|
193
|
-
};
|
|
194
|
-
}, [
|
|
195
|
-
nodesKey,
|
|
196
|
-
linksKey,
|
|
197
|
-
chargeStrength,
|
|
198
|
-
linkDistance,
|
|
199
|
-
linkStrength,
|
|
200
|
-
collisionStrength,
|
|
201
|
-
collisionRadius,
|
|
202
|
-
centerStrength,
|
|
203
|
-
width,
|
|
204
|
-
height,
|
|
205
|
-
alphaDecay,
|
|
206
|
-
velocityDecay,
|
|
207
|
-
alphaTarget,
|
|
208
|
-
alphaMin,
|
|
209
|
-
stabilizeOnStop,
|
|
210
|
-
tickThrottleMs,
|
|
211
|
-
maxSimulationTimeMs
|
|
212
|
-
]);
|
|
213
|
-
const restart = () => {
|
|
214
|
-
if (simulationRef.current) {
|
|
215
|
-
try {
|
|
216
|
-
simulationRef.current.alphaTarget(warmAlpha).restart();
|
|
217
|
-
} catch (e) {
|
|
218
|
-
simulationRef.current.restart();
|
|
219
|
-
}
|
|
220
|
-
setIsRunning(true);
|
|
221
|
-
if (stopTimeoutRef.current != null) {
|
|
222
|
-
try {
|
|
223
|
-
globalThis.clearTimeout(stopTimeoutRef.current);
|
|
224
|
-
} catch (e) {
|
|
225
|
-
}
|
|
226
|
-
stopTimeoutRef.current = null;
|
|
227
|
-
}
|
|
228
|
-
if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
|
|
229
|
-
stopTimeoutRef.current = globalThis.setTimeout(() => {
|
|
230
|
-
try {
|
|
231
|
-
simulationRef.current?.alpha(0);
|
|
232
|
-
simulationRef.current?.stop();
|
|
233
|
-
} catch (e) {
|
|
234
|
-
}
|
|
235
|
-
setIsRunning(false);
|
|
236
|
-
}, maxSimulationTimeMs);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
const stop = () => {
|
|
241
|
-
if (simulationRef.current) {
|
|
242
|
-
simulationRef.current.stop();
|
|
243
|
-
setIsRunning(false);
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
const originalForcesRef = useRef({ charge: chargeStrength, link: linkStrength, collision: collisionStrength });
|
|
247
|
-
const forcesEnabledRef = useRef(true);
|
|
248
|
-
const setForcesEnabled = (enabled) => {
|
|
249
|
-
const sim = simulationRef.current;
|
|
250
|
-
if (!sim) return;
|
|
251
|
-
if (forcesEnabledRef.current === enabled) return;
|
|
252
|
-
forcesEnabledRef.current = enabled;
|
|
253
|
-
try {
|
|
254
|
-
const charge = sim.force("charge");
|
|
255
|
-
if (charge && typeof charge.strength === "function") {
|
|
256
|
-
charge.strength(enabled ? originalForcesRef.current.charge : 0);
|
|
257
|
-
}
|
|
258
|
-
const link = sim.force("link");
|
|
259
|
-
if (link && typeof link.strength === "function") {
|
|
260
|
-
link.strength(enabled ? originalForcesRef.current.link : 0);
|
|
261
|
-
}
|
|
262
|
-
} catch (e) {
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
return {
|
|
266
|
-
nodes,
|
|
267
|
-
links,
|
|
268
|
-
restart,
|
|
269
|
-
stop,
|
|
270
|
-
isRunning,
|
|
271
|
-
alpha,
|
|
272
|
-
setForcesEnabled
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
8
|
function cn(...inputs) {
|
|
276
9
|
return twMerge(clsx(inputs));
|
|
277
10
|
}
|
|
@@ -382,7 +115,6 @@ var ForceDirectedGraph = forwardRef(
|
|
|
382
115
|
links: initialLinks,
|
|
383
116
|
width,
|
|
384
117
|
height,
|
|
385
|
-
simulationOptions,
|
|
386
118
|
enableZoom = true,
|
|
387
119
|
enableDrag = true,
|
|
388
120
|
onNodeClick,
|
|
@@ -399,7 +131,9 @@ var ForceDirectedGraph = forwardRef(
|
|
|
399
131
|
className,
|
|
400
132
|
manualLayout = false,
|
|
401
133
|
onManualLayoutChange,
|
|
402
|
-
packageBounds
|
|
134
|
+
packageBounds,
|
|
135
|
+
layout: externalLayout,
|
|
136
|
+
onLayoutChange
|
|
403
137
|
}, ref) => {
|
|
404
138
|
const svgRef = useRef(null);
|
|
405
139
|
const gRef = useRef(null);
|
|
@@ -409,180 +143,102 @@ var ForceDirectedGraph = forwardRef(
|
|
|
409
143
|
const dragActiveRef = useRef(false);
|
|
410
144
|
const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
|
|
411
145
|
const internalDragEnabledRef = useRef(enableDrag);
|
|
146
|
+
const [layout, setLayout] = useState(externalLayout || "force");
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (externalLayout && externalLayout !== layout) {
|
|
149
|
+
setLayout(externalLayout);
|
|
150
|
+
}
|
|
151
|
+
}, [externalLayout]);
|
|
152
|
+
const handleLayoutChange = useCallback((newLayout) => {
|
|
153
|
+
setLayout(newLayout);
|
|
154
|
+
onLayoutChange?.(newLayout);
|
|
155
|
+
}, [onLayoutChange]);
|
|
412
156
|
useEffect(() => {
|
|
413
157
|
internalDragEnabledRef.current = enableDrag;
|
|
414
158
|
}, [enableDrag]);
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (!key) return;
|
|
426
|
-
const center = boundsToUse[key];
|
|
427
|
-
if (!center) return;
|
|
428
|
-
const dx = center.x - (n.x ?? 0);
|
|
429
|
-
const dy = center.y - (n.y ?? 0);
|
|
430
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
431
|
-
const pullStrength = Math.min(0.5, 0.15 * (dist / (center.r || 200)) + 0.06);
|
|
432
|
-
if (!isNaN(pullStrength) && isFinite(pullStrength)) {
|
|
433
|
-
n.vx = (n.vx ?? 0) + dx / (dist || 1) * pullStrength;
|
|
434
|
-
n.vy = (n.vy ?? 0) + dy / (dist || 1) * pullStrength;
|
|
435
|
-
}
|
|
436
|
-
if (center.r && dist > center.r) {
|
|
437
|
-
const excess = (dist - center.r) / (dist || 1);
|
|
438
|
-
n.vx = (n.vx ?? 0) - dx * 0.02 * excess;
|
|
439
|
-
n.vy = (n.vy ?? 0) - dy * 0.02 * excess;
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
} catch (e) {
|
|
159
|
+
const nodes = React.useMemo(() => {
|
|
160
|
+
if (!initialNodes || !initialNodes.length) return initialNodes;
|
|
161
|
+
const cx = width / 2;
|
|
162
|
+
const cy = height / 2;
|
|
163
|
+
if (layout === "force") {
|
|
164
|
+
return initialNodes.map((n) => ({
|
|
165
|
+
...n,
|
|
166
|
+
x: Math.random() * width,
|
|
167
|
+
y: Math.random() * height
|
|
168
|
+
}));
|
|
444
169
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
170
|
+
if (layout === "circular") {
|
|
171
|
+
const radius = Math.min(width, height) * 0.35;
|
|
172
|
+
return initialNodes.map((n, i) => ({
|
|
173
|
+
...n,
|
|
174
|
+
x: cx + Math.cos(2 * Math.PI * i / initialNodes.length) * radius,
|
|
175
|
+
y: cy + Math.sin(2 * Math.PI * i / initialNodes.length) * radius
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
if (layout === "hierarchical") {
|
|
179
|
+
const cols = Math.ceil(Math.sqrt(initialNodes.length));
|
|
180
|
+
const spacingX = width / (cols + 1);
|
|
181
|
+
const spacingY = height / (Math.ceil(initialNodes.length / cols) + 1);
|
|
182
|
+
return initialNodes.map((n, i) => ({
|
|
183
|
+
...n,
|
|
184
|
+
x: spacingX * (i % cols + 1),
|
|
185
|
+
y: spacingY * (Math.floor(i / cols) + 1)
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
return initialNodes;
|
|
189
|
+
}, [initialNodes, width, height, layout]);
|
|
190
|
+
const links = initialLinks;
|
|
191
|
+
const restart = React.useCallback(() => {
|
|
192
|
+
}, []);
|
|
193
|
+
const stop = React.useCallback(() => {
|
|
194
|
+
}, []);
|
|
195
|
+
const setForcesEnabled = React.useCallback((_enabled) => {
|
|
196
|
+
}, []);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!nodes || nodes.length === 0) return;
|
|
199
|
+
const applyLayout = () => {
|
|
200
|
+
const cx = width / 2;
|
|
201
|
+
const cy = height / 2;
|
|
202
|
+
if (layout === "circular") {
|
|
203
|
+
const radius = Math.min(width, height) * 0.35;
|
|
204
|
+
nodes.forEach((node, i) => {
|
|
205
|
+
const angle = 2 * Math.PI * i / nodes.length;
|
|
206
|
+
node.fx = cx + Math.cos(angle) * radius;
|
|
207
|
+
node.fy = cy + Math.sin(angle) * radius;
|
|
466
208
|
});
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const s = typeof l.source === "string" ? l.source : l.source && l.source.id;
|
|
474
|
-
const t = typeof l.target === "string" ? l.target : l.target && l.target.id;
|
|
475
|
-
return localNodes.some((ln) => ln.id === s) && localNodes.some((ln) => ln.id === t);
|
|
476
|
-
}).map((l) => ({ source: typeof l.source === "string" ? l.source : l.source.id, target: typeof l.target === "string" ? l.target : l.target.id }));
|
|
477
|
-
if (localNodes.length === 1) {
|
|
478
|
-
localPositions2[localNodes[0].id] = { x: 0, y: 0 };
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
const sim = d32.forceSimulation(localNodes).force("link", d32.forceLink(localLinks).id((d) => d.id).distance(30).strength(0.8)).force("charge", d32.forceManyBody().strength(-15)).force("collide", d32.forceCollide((d) => (d.size || 10) + 6).iterations(2)).stop();
|
|
482
|
-
const ticks = 300;
|
|
483
|
-
for (let i = 0; i < ticks; i++) sim.tick();
|
|
484
|
-
localNodes.forEach((ln) => {
|
|
485
|
-
localPositions2[ln.id] = { x: ln.x ?? 0, y: ln.y ?? 0 };
|
|
209
|
+
} else if (layout === "hierarchical") {
|
|
210
|
+
const groups = /* @__PURE__ */ new Map();
|
|
211
|
+
nodes.forEach((n) => {
|
|
212
|
+
const key = n.packageGroup || n.group || "root";
|
|
213
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
214
|
+
groups.get(key).push(n);
|
|
486
215
|
});
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}, [initialNodes, packageAreas, localPositions]);
|
|
504
|
-
const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(seededNodes || initialNodes, initialLinks, {
|
|
505
|
-
width,
|
|
506
|
-
height,
|
|
507
|
-
chargeStrength: manualLayout ? 0 : void 0,
|
|
508
|
-
onTick,
|
|
509
|
-
...simulationOptions
|
|
510
|
-
});
|
|
511
|
-
const nodesById = React.useMemo(() => {
|
|
512
|
-
const m = {};
|
|
513
|
-
(nodes || []).forEach((n) => {
|
|
514
|
-
if (n && n.id) m[n.id] = n;
|
|
515
|
-
});
|
|
516
|
-
return m;
|
|
517
|
-
}, [nodes]);
|
|
518
|
-
const clusterBounds = React.useMemo(() => {
|
|
519
|
-
try {
|
|
520
|
-
if (!links || !nodes) return null;
|
|
521
|
-
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
522
|
-
const adj = /* @__PURE__ */ new Map();
|
|
523
|
-
nodes.forEach((n) => adj.set(n.id, /* @__PURE__ */ new Set()));
|
|
524
|
-
links.forEach((l) => {
|
|
525
|
-
const type = l.type || "reference";
|
|
526
|
-
if (type !== "dependency") return;
|
|
527
|
-
const s = typeof l.source === "string" ? l.source : l.source && l.source.id || null;
|
|
528
|
-
const t = typeof l.target === "string" ? l.target : l.target && l.target.id || null;
|
|
529
|
-
if (!s || !t) return;
|
|
530
|
-
if (!nodeIds.has(s) || !nodeIds.has(t)) return;
|
|
531
|
-
adj.get(s)?.add(t);
|
|
532
|
-
adj.get(t)?.add(s);
|
|
533
|
-
});
|
|
534
|
-
const visited = /* @__PURE__ */ new Set();
|
|
535
|
-
const comps = [];
|
|
536
|
-
for (const nid of nodeIds) {
|
|
537
|
-
if (visited.has(nid)) continue;
|
|
538
|
-
const stack = [nid];
|
|
539
|
-
const comp = [];
|
|
540
|
-
visited.add(nid);
|
|
541
|
-
while (stack.length) {
|
|
542
|
-
const cur = stack.pop();
|
|
543
|
-
comp.push(cur);
|
|
544
|
-
const neigh = adj.get(cur);
|
|
545
|
-
if (!neigh) continue;
|
|
546
|
-
for (const nb of neigh) {
|
|
547
|
-
if (!visited.has(nb)) {
|
|
548
|
-
visited.add(nb);
|
|
549
|
-
stack.push(nb);
|
|
550
|
-
}
|
|
216
|
+
const groupArray = Array.from(groups.entries());
|
|
217
|
+
const cols = Math.ceil(Math.sqrt(groupArray.length));
|
|
218
|
+
const groupSpacingX = width * 0.8 / cols;
|
|
219
|
+
const groupSpacingY = height * 0.8 / Math.ceil(groupArray.length / cols);
|
|
220
|
+
groupArray.forEach(([groupKey, groupNodes], gi) => {
|
|
221
|
+
const col = gi % cols;
|
|
222
|
+
const row = Math.floor(gi / cols);
|
|
223
|
+
const groupX = (col + 0.5) * groupSpacingX;
|
|
224
|
+
const groupY = (row + 0.5) * groupSpacingY;
|
|
225
|
+
if (groupKey.startsWith("pkg:") || groupKey === groupKey) {
|
|
226
|
+
groupNodes.forEach((n, ni) => {
|
|
227
|
+
const angle = 2 * Math.PI * ni / groupNodes.length;
|
|
228
|
+
const r = Math.min(80, 20 + groupNodes.length * 8);
|
|
229
|
+
n.fx = groupX + Math.cos(angle) * r;
|
|
230
|
+
n.fy = groupY + Math.sin(angle) * r;
|
|
231
|
+
});
|
|
551
232
|
}
|
|
552
|
-
}
|
|
553
|
-
comps.push(comp);
|
|
233
|
+
});
|
|
554
234
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const circleRadius = base * Math.max(30, num * 20, Math.sqrt(num) * 12);
|
|
563
|
-
const map = {};
|
|
564
|
-
comps.forEach((c, i) => {
|
|
565
|
-
const angle = 2 * Math.PI * i / num;
|
|
566
|
-
const x = cx + Math.cos(angle) * circleRadius;
|
|
567
|
-
const y = cy + Math.sin(angle) * circleRadius;
|
|
568
|
-
const sizeBias = Math.sqrt(Math.max(1, c.length));
|
|
569
|
-
const r = Math.max(200, 100 * sizeBias);
|
|
570
|
-
map[`cluster:${i}`] = { x, y, r };
|
|
571
|
-
});
|
|
572
|
-
const nodeToCluster = {};
|
|
573
|
-
comps.forEach((c, i) => c.forEach((nid) => nodeToCluster[nid] = `cluster:${i}`));
|
|
574
|
-
return { bounds: map, nodeToCluster };
|
|
575
|
-
} catch (e) {
|
|
576
|
-
return null;
|
|
577
|
-
}
|
|
578
|
-
}, [nodes, links, width, height]);
|
|
579
|
-
useEffect(() => {
|
|
580
|
-
if (!packageBounds && !clusterBounds && (!packageAreas || Object.keys(packageAreas).length === 0)) return;
|
|
581
|
-
try {
|
|
582
|
-
restart();
|
|
583
|
-
} catch (e) {
|
|
584
|
-
}
|
|
585
|
-
}, [packageBounds, clusterBounds, packageAreas, restart]);
|
|
235
|
+
try {
|
|
236
|
+
restart();
|
|
237
|
+
} catch (e) {
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
applyLayout();
|
|
241
|
+
}, [layout, nodes, width, height, restart]);
|
|
586
242
|
useEffect(() => {
|
|
587
243
|
try {
|
|
588
244
|
if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
|
|
@@ -645,18 +301,22 @@ var ForceDirectedGraph = forwardRef(
|
|
|
645
301
|
const x = width / 2 - centerX * scale;
|
|
646
302
|
const y = height / 2 - centerY * scale;
|
|
647
303
|
if (gRef.current && svgRef.current) {
|
|
648
|
-
const svg =
|
|
649
|
-
const newTransform =
|
|
650
|
-
svg.transition().duration(300).call(
|
|
304
|
+
const svg = d3.select(svgRef.current);
|
|
305
|
+
const newTransform = d3.zoomIdentity.translate(x, y).scale(scale);
|
|
306
|
+
svg.transition().duration(300).call(d3.zoom().transform, newTransform);
|
|
651
307
|
setTransform(newTransform);
|
|
652
308
|
}
|
|
653
309
|
},
|
|
654
310
|
getPinnedNodes: () => Array.from(pinnedNodes),
|
|
655
311
|
setDragMode: (enabled) => {
|
|
656
312
|
internalDragEnabledRef.current = enabled;
|
|
657
|
-
}
|
|
313
|
+
},
|
|
314
|
+
setLayout: (newLayout) => {
|
|
315
|
+
handleLayoutChange(newLayout);
|
|
316
|
+
},
|
|
317
|
+
getLayout: () => layout
|
|
658
318
|
}),
|
|
659
|
-
[nodes, pinnedNodes, restart, width, height]
|
|
319
|
+
[nodes, pinnedNodes, restart, width, height, layout, handleLayoutChange]
|
|
660
320
|
);
|
|
661
321
|
useEffect(() => {
|
|
662
322
|
try {
|
|
@@ -666,9 +326,9 @@ var ForceDirectedGraph = forwardRef(
|
|
|
666
326
|
}, [manualLayout, onManualLayoutChange]);
|
|
667
327
|
useEffect(() => {
|
|
668
328
|
if (!enableZoom || !svgRef.current || !gRef.current) return;
|
|
669
|
-
const svg =
|
|
670
|
-
const g =
|
|
671
|
-
const zoom2 =
|
|
329
|
+
const svg = d3.select(svgRef.current);
|
|
330
|
+
const g = d3.select(gRef.current);
|
|
331
|
+
const zoom2 = d3.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
|
|
672
332
|
g.attr("transform", event.transform);
|
|
673
333
|
transformRef.current = event.transform;
|
|
674
334
|
setTransform(event.transform);
|
|
@@ -681,19 +341,19 @@ var ForceDirectedGraph = forwardRef(
|
|
|
681
341
|
useEffect(() => {
|
|
682
342
|
if (!gRef.current) return;
|
|
683
343
|
try {
|
|
684
|
-
const g =
|
|
344
|
+
const g = d3.select(gRef.current);
|
|
685
345
|
g.selectAll("g.node").each(function() {
|
|
686
|
-
const datum =
|
|
346
|
+
const datum = d3.select(this).datum();
|
|
687
347
|
if (!datum) return;
|
|
688
|
-
|
|
348
|
+
d3.select(this).attr("transform", `translate(${datum.x || 0},${datum.y || 0})`);
|
|
689
349
|
});
|
|
690
350
|
g.selectAll("line").each(function() {
|
|
691
|
-
const l =
|
|
351
|
+
const l = d3.select(this).datum();
|
|
692
352
|
if (!l) return;
|
|
693
353
|
const s = typeof l.source === "object" ? l.source : nodes.find((n) => n.id === l.source) || l.source;
|
|
694
354
|
const t = typeof l.target === "object" ? l.target : nodes.find((n) => n.id === l.target) || l.target;
|
|
695
355
|
if (!s || !t) return;
|
|
696
|
-
|
|
356
|
+
d3.select(this).attr("x1", s.x).attr("y1", s.y).attr("x2", t.x).attr("y2", t.y);
|
|
697
357
|
});
|
|
698
358
|
} catch (e) {
|
|
699
359
|
}
|
|
@@ -754,8 +414,8 @@ var ForceDirectedGraph = forwardRef(
|
|
|
754
414
|
}, [enableDrag]);
|
|
755
415
|
useEffect(() => {
|
|
756
416
|
if (!gRef.current || !enableDrag) return;
|
|
757
|
-
const g =
|
|
758
|
-
const dragBehavior =
|
|
417
|
+
const g = d3.select(gRef.current);
|
|
418
|
+
const dragBehavior = d3.drag().on("start", function(event) {
|
|
759
419
|
try {
|
|
760
420
|
const target = event.sourceEvent && event.sourceEvent.target || event.target;
|
|
761
421
|
const grp = target.closest?.("g.node");
|