@aiready/components 0.1.3 → 0.1.4
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 +2 -0
- package/dist/charts/ForceDirectedGraph.js +489 -162
- package/dist/charts/ForceDirectedGraph.js.map +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/hooks/useForceSimulation.d.ts +5 -0
- package/dist/hooks/useForceSimulation.js +181 -19
- package/dist/hooks/useForceSimulation.js.map +1 -1
- package/dist/index.js +489 -162
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/src/__tests__/smoke.test.js +4 -0
- package/src/charts/ForceDirectedGraph.tsx +281 -277
- package/src/charts/LinkItem.tsx +74 -0
- package/src/charts/NodeItem.tsx +70 -0
- package/src/hooks/useForceSimulation.ts +220 -36
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
|
|
1
|
+
import React, { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
|
|
2
2
|
import * as d32 from 'd3';
|
|
3
3
|
import { clsx } from 'clsx';
|
|
4
4
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -17,46 +17,183 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
17
17
|
height,
|
|
18
18
|
alphaDecay = 0.0228,
|
|
19
19
|
velocityDecay = 0.4,
|
|
20
|
-
|
|
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
|
|
21
32
|
} = options;
|
|
22
33
|
const [nodes, setNodes] = useState(initialNodes);
|
|
23
34
|
const [links, setLinks] = useState(initialLinks);
|
|
24
35
|
const [isRunning, setIsRunning] = useState(false);
|
|
25
36
|
const [alpha, setAlpha] = useState(1);
|
|
26
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("|");
|
|
27
45
|
useEffect(() => {
|
|
28
46
|
const nodesCopy = initialNodes.map((node) => ({ ...node }));
|
|
29
47
|
const linksCopy = initialLinks.map((link) => ({ ...link }));
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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) => {
|
|
36
80
|
const nodeSize = d && d.size ? d.size : 10;
|
|
37
81
|
return nodeSize + collisionRadius;
|
|
38
|
-
}).strength(collisionStrength)
|
|
39
|
-
|
|
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
|
+
}
|
|
40
99
|
simulationRef.current = simulation;
|
|
41
|
-
|
|
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 = () => {
|
|
42
130
|
try {
|
|
43
131
|
if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
|
|
44
132
|
} catch (e) {
|
|
45
133
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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);
|
|
51
170
|
simulation.on("end", () => {
|
|
52
171
|
setIsRunning(false);
|
|
53
172
|
});
|
|
54
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
|
+
}
|
|
55
192
|
simulation.stop();
|
|
56
193
|
};
|
|
57
194
|
}, [
|
|
58
|
-
|
|
59
|
-
|
|
195
|
+
nodesKey,
|
|
196
|
+
linksKey,
|
|
60
197
|
chargeStrength,
|
|
61
198
|
linkDistance,
|
|
62
199
|
linkStrength,
|
|
@@ -67,12 +204,37 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
67
204
|
height,
|
|
68
205
|
alphaDecay,
|
|
69
206
|
velocityDecay,
|
|
70
|
-
|
|
207
|
+
alphaTarget,
|
|
208
|
+
alphaMin,
|
|
209
|
+
stabilizeOnStop,
|
|
210
|
+
tickThrottleMs,
|
|
211
|
+
maxSimulationTimeMs
|
|
71
212
|
]);
|
|
72
213
|
const restart = () => {
|
|
73
214
|
if (simulationRef.current) {
|
|
74
|
-
|
|
215
|
+
try {
|
|
216
|
+
simulationRef.current.alphaTarget(warmAlpha).restart();
|
|
217
|
+
} catch (e) {
|
|
218
|
+
simulationRef.current.restart();
|
|
219
|
+
}
|
|
75
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
|
+
}
|
|
76
238
|
}
|
|
77
239
|
};
|
|
78
240
|
const stop = () => {
|
|
@@ -113,6 +275,107 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
113
275
|
function cn(...inputs) {
|
|
114
276
|
return twMerge(clsx(inputs));
|
|
115
277
|
}
|
|
278
|
+
var NodeItem = ({
|
|
279
|
+
node,
|
|
280
|
+
isSelected,
|
|
281
|
+
isHovered,
|
|
282
|
+
pinned,
|
|
283
|
+
defaultNodeSize,
|
|
284
|
+
defaultNodeColor,
|
|
285
|
+
showLabel = true,
|
|
286
|
+
onClick,
|
|
287
|
+
onDoubleClick,
|
|
288
|
+
onMouseEnter,
|
|
289
|
+
onMouseLeave,
|
|
290
|
+
onMouseDown
|
|
291
|
+
}) => {
|
|
292
|
+
const nodeSize = node.size || defaultNodeSize;
|
|
293
|
+
const nodeColor = node.color || defaultNodeColor;
|
|
294
|
+
const x = node.x ?? 0;
|
|
295
|
+
const y = node.y ?? 0;
|
|
296
|
+
return /* @__PURE__ */ jsxs(
|
|
297
|
+
"g",
|
|
298
|
+
{
|
|
299
|
+
className: "cursor-pointer node",
|
|
300
|
+
"data-id": node.id,
|
|
301
|
+
transform: `translate(${x},${y})`,
|
|
302
|
+
onClick: () => onClick?.(node),
|
|
303
|
+
onDoubleClick: (e) => onDoubleClick?.(e, node),
|
|
304
|
+
onMouseEnter: () => onMouseEnter?.(node),
|
|
305
|
+
onMouseLeave: () => onMouseLeave?.(),
|
|
306
|
+
onMouseDown: (e) => onMouseDown?.(e, node),
|
|
307
|
+
children: [
|
|
308
|
+
/* @__PURE__ */ jsx(
|
|
309
|
+
"circle",
|
|
310
|
+
{
|
|
311
|
+
r: nodeSize,
|
|
312
|
+
fill: nodeColor,
|
|
313
|
+
stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
|
|
314
|
+
strokeWidth: pinned ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5,
|
|
315
|
+
opacity: isHovered || isSelected ? 1 : 0.9
|
|
316
|
+
}
|
|
317
|
+
),
|
|
318
|
+
pinned && /* @__PURE__ */ jsx("circle", { r: nodeSize + 4, fill: "none", stroke: "#ff6b6b", strokeWidth: 1, opacity: 0.5, className: "pointer-events-none" }),
|
|
319
|
+
showLabel && node.label && /* @__PURE__ */ jsx("text", { y: nodeSize + 15, fill: "#333", fontSize: "12", textAnchor: "middle", dominantBaseline: "middle", pointerEvents: "none", className: "select-none", children: node.label })
|
|
320
|
+
]
|
|
321
|
+
},
|
|
322
|
+
node.id
|
|
323
|
+
);
|
|
324
|
+
};
|
|
325
|
+
var NodeItem_default = NodeItem;
|
|
326
|
+
var LinkItem = ({ link, onClick, defaultWidth, showLabel = true, nodes = [] }) => {
|
|
327
|
+
const src = link.source?.id ?? (typeof link.source === "string" ? link.source : void 0);
|
|
328
|
+
const tgt = link.target?.id ?? (typeof link.target === "string" ? link.target : void 0);
|
|
329
|
+
const getNodePosition = (nodeOrId) => {
|
|
330
|
+
if (typeof nodeOrId === "object" && nodeOrId !== null) {
|
|
331
|
+
const node = nodeOrId;
|
|
332
|
+
return { x: node.x ?? 0, y: node.y ?? 0 };
|
|
333
|
+
} else if (typeof nodeOrId === "string") {
|
|
334
|
+
const found = nodes.find((n) => n.id === nodeOrId);
|
|
335
|
+
if (found) return { x: found.x ?? 0, y: found.y ?? 0 };
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
};
|
|
339
|
+
const sourcePos = getNodePosition(link.source);
|
|
340
|
+
const targetPos = getNodePosition(link.target);
|
|
341
|
+
if (!sourcePos || !targetPos) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const midX = (sourcePos.x + targetPos.x) / 2;
|
|
345
|
+
const midY = (sourcePos.y + targetPos.y) / 2;
|
|
346
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
347
|
+
/* @__PURE__ */ jsx(
|
|
348
|
+
"line",
|
|
349
|
+
{
|
|
350
|
+
x1: sourcePos.x,
|
|
351
|
+
y1: sourcePos.y,
|
|
352
|
+
x2: targetPos.x,
|
|
353
|
+
y2: targetPos.y,
|
|
354
|
+
"data-source": src,
|
|
355
|
+
"data-target": tgt,
|
|
356
|
+
stroke: link.color,
|
|
357
|
+
strokeWidth: link.width ?? defaultWidth ?? 1,
|
|
358
|
+
opacity: 0.6,
|
|
359
|
+
className: "cursor-pointer transition-opacity hover:opacity-100",
|
|
360
|
+
onClick: () => onClick?.(link)
|
|
361
|
+
}
|
|
362
|
+
),
|
|
363
|
+
showLabel && link.label && /* @__PURE__ */ jsx(
|
|
364
|
+
"text",
|
|
365
|
+
{
|
|
366
|
+
x: midX,
|
|
367
|
+
y: midY,
|
|
368
|
+
fill: "#666",
|
|
369
|
+
fontSize: "10",
|
|
370
|
+
textAnchor: "middle",
|
|
371
|
+
dominantBaseline: "middle",
|
|
372
|
+
pointerEvents: "none",
|
|
373
|
+
children: link.label
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
] });
|
|
377
|
+
};
|
|
378
|
+
var LinkItem_default = LinkItem;
|
|
116
379
|
var ForceDirectedGraph = forwardRef(
|
|
117
380
|
({
|
|
118
381
|
nodes: initialNodes,
|
|
@@ -141,6 +404,7 @@ var ForceDirectedGraph = forwardRef(
|
|
|
141
404
|
const svgRef = useRef(null);
|
|
142
405
|
const gRef = useRef(null);
|
|
143
406
|
const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
|
|
407
|
+
const transformRef = useRef(transform);
|
|
144
408
|
const dragNodeRef = useRef(null);
|
|
145
409
|
const dragActiveRef = useRef(false);
|
|
146
410
|
const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
|
|
@@ -148,73 +412,177 @@ var ForceDirectedGraph = forwardRef(
|
|
|
148
412
|
useEffect(() => {
|
|
149
413
|
internalDragEnabledRef.current = enableDrag;
|
|
150
414
|
}, [enableDrag]);
|
|
151
|
-
const onTick = (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
415
|
+
const onTick = (_nodesCopy, _linksCopy, _sim) => {
|
|
416
|
+
try {
|
|
417
|
+
const boundsToUse = clusterBounds?.bounds ?? packageBounds;
|
|
418
|
+
const nodeClusterMap = clusterBounds?.nodeToCluster ?? {};
|
|
419
|
+
if (boundsToUse) {
|
|
420
|
+
Object.values(nodesById).forEach((n) => {
|
|
421
|
+
if (!n) return;
|
|
422
|
+
const group = n.group ?? n.packageGroup;
|
|
423
|
+
const clusterKey = nodeClusterMap[n.id];
|
|
424
|
+
const key = clusterKey ?? (group ? `pkg:${group}` : void 0);
|
|
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;
|
|
161
435
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const pack2 = d32.pack().size([width, height]).padding(30);
|
|
167
|
-
const packed = pack2(root);
|
|
168
|
-
const map = {};
|
|
169
|
-
if (packed.children) {
|
|
170
|
-
packed.children.forEach((c) => {
|
|
171
|
-
map[`pkg:${c.data.name}`] = { x: c.x, y: c.y, r: c.r * 0.95 };
|
|
172
|
-
});
|
|
173
|
-
effectiveBounds = map;
|
|
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;
|
|
174
440
|
}
|
|
175
|
-
}
|
|
176
|
-
} catch (e) {
|
|
441
|
+
});
|
|
177
442
|
}
|
|
443
|
+
} catch (e) {
|
|
178
444
|
}
|
|
179
|
-
|
|
445
|
+
};
|
|
446
|
+
const { packageAreas, localPositions } = React.useMemo(() => {
|
|
180
447
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
if (!
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
448
|
+
if (!initialNodes || !initialNodes.length) return { packageAreas: {}, localPositions: {} };
|
|
449
|
+
const groups = /* @__PURE__ */ new Map();
|
|
450
|
+
initialNodes.forEach((n) => {
|
|
451
|
+
const key = n.packageGroup || n.group || "root";
|
|
452
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
453
|
+
groups.get(key).push(n);
|
|
454
|
+
});
|
|
455
|
+
const groupKeys = Array.from(groups.keys());
|
|
456
|
+
const children = groupKeys.map((k) => ({ name: k, value: Math.max(1, groups.get(k).length) }));
|
|
457
|
+
const root = d32.hierarchy({ children });
|
|
458
|
+
root.sum((d) => d.value);
|
|
459
|
+
const pack2 = d32.pack().size([width, height]).padding(Math.max(20, Math.min(width, height) * 0.03));
|
|
460
|
+
const packed = pack2(root);
|
|
461
|
+
const packageAreas2 = {};
|
|
462
|
+
if (packed.children) {
|
|
463
|
+
packed.children.forEach((c) => {
|
|
464
|
+
const name = c.data.name;
|
|
465
|
+
packageAreas2[name] = { x: c.x, y: c.y, r: Math.max(40, c.r) };
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const localPositions2 = {};
|
|
469
|
+
groups.forEach((nodesInGroup, _key) => {
|
|
470
|
+
if (!nodesInGroup || nodesInGroup.length === 0) return;
|
|
471
|
+
const localNodes = nodesInGroup.map((n) => ({ id: n.id, x: Math.random() * 10 - 5, y: Math.random() * 10 - 5, size: n.size || 10 }));
|
|
472
|
+
const localLinks = (initialLinks || []).filter((l) => {
|
|
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;
|
|
199
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 };
|
|
486
|
+
});
|
|
200
487
|
});
|
|
488
|
+
return { packageAreas: packageAreas2, localPositions: localPositions2 };
|
|
201
489
|
} catch (e) {
|
|
490
|
+
return { packageAreas: {}, localPositions: {} };
|
|
202
491
|
}
|
|
203
|
-
};
|
|
204
|
-
const
|
|
492
|
+
}, [initialNodes, initialLinks, width, height]);
|
|
493
|
+
const seededNodes = React.useMemo(() => {
|
|
494
|
+
if (!initialNodes || !Object.keys(packageAreas || {}).length) return initialNodes;
|
|
495
|
+
return initialNodes.map((n) => {
|
|
496
|
+
const key = n.packageGroup || n.group || "root";
|
|
497
|
+
const area = packageAreas[key];
|
|
498
|
+
const lp = localPositions[n.id];
|
|
499
|
+
if (!area || !lp) return n;
|
|
500
|
+
const scale = Math.max(0.5, area.r * 0.6 / (Math.max(1, Math.sqrt(lp.x * lp.x + lp.y * lp.y)) || 1));
|
|
501
|
+
return { ...n, x: area.x + lp.x * scale, y: area.y + lp.y * scale };
|
|
502
|
+
});
|
|
503
|
+
}, [initialNodes, packageAreas, localPositions]);
|
|
504
|
+
const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(seededNodes || initialNodes, initialLinks, {
|
|
205
505
|
width,
|
|
206
506
|
height,
|
|
207
507
|
chargeStrength: manualLayout ? 0 : void 0,
|
|
208
508
|
onTick,
|
|
209
509
|
...simulationOptions
|
|
210
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
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
comps.push(comp);
|
|
554
|
+
}
|
|
555
|
+
if (comps.length <= 1) return null;
|
|
556
|
+
const children = comps.map((c, i) => ({ name: String(i), value: Math.max(1, c.length) }));
|
|
557
|
+
d32.hierarchy({ children }).sum((d) => d.value).sort((a, b) => b.value - a.value);
|
|
558
|
+
const num = comps.length;
|
|
559
|
+
const cx = width / 2;
|
|
560
|
+
const cy = height / 2;
|
|
561
|
+
const base = Math.max(width, height);
|
|
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]);
|
|
211
579
|
useEffect(() => {
|
|
212
|
-
if (!packageBounds) return;
|
|
580
|
+
if (!packageBounds && !clusterBounds && (!packageAreas || Object.keys(packageAreas).length === 0)) return;
|
|
213
581
|
try {
|
|
214
582
|
restart();
|
|
215
583
|
} catch (e) {
|
|
216
584
|
}
|
|
217
|
-
}, [packageBounds, restart]);
|
|
585
|
+
}, [packageBounds, clusterBounds, packageAreas, restart]);
|
|
218
586
|
useEffect(() => {
|
|
219
587
|
try {
|
|
220
588
|
if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
|
|
@@ -302,6 +670,7 @@ var ForceDirectedGraph = forwardRef(
|
|
|
302
670
|
const g = d32.select(gRef.current);
|
|
303
671
|
const zoom2 = d32.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
|
|
304
672
|
g.attr("transform", event.transform);
|
|
673
|
+
transformRef.current = event.transform;
|
|
305
674
|
setTransform(event.transform);
|
|
306
675
|
});
|
|
307
676
|
svg.call(zoom2);
|
|
@@ -309,6 +678,26 @@ var ForceDirectedGraph = forwardRef(
|
|
|
309
678
|
svg.on(".zoom", null);
|
|
310
679
|
};
|
|
311
680
|
}, [enableZoom]);
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
if (!gRef.current) return;
|
|
683
|
+
try {
|
|
684
|
+
const g = d32.select(gRef.current);
|
|
685
|
+
g.selectAll("g.node").each(function() {
|
|
686
|
+
const datum = d32.select(this).datum();
|
|
687
|
+
if (!datum) return;
|
|
688
|
+
d32.select(this).attr("transform", `translate(${datum.x || 0},${datum.y || 0})`);
|
|
689
|
+
});
|
|
690
|
+
g.selectAll("line").each(function() {
|
|
691
|
+
const l = d32.select(this).datum();
|
|
692
|
+
if (!l) return;
|
|
693
|
+
const s = typeof l.source === "object" ? l.source : nodes.find((n) => n.id === l.source) || l.source;
|
|
694
|
+
const t = typeof l.target === "object" ? l.target : nodes.find((n) => n.id === l.target) || l.target;
|
|
695
|
+
if (!s || !t) return;
|
|
696
|
+
d32.select(this).attr("x1", s.x).attr("y1", s.y).attr("x2", t.x).attr("y2", t.y);
|
|
697
|
+
});
|
|
698
|
+
} catch (e) {
|
|
699
|
+
}
|
|
700
|
+
}, [nodes, links]);
|
|
312
701
|
const handleDragStart = useCallback(
|
|
313
702
|
(event, node) => {
|
|
314
703
|
if (!enableDrag) return;
|
|
@@ -333,8 +722,9 @@ var ForceDirectedGraph = forwardRef(
|
|
|
333
722
|
const svg = svgRef.current;
|
|
334
723
|
if (!svg) return;
|
|
335
724
|
const rect = svg.getBoundingClientRect();
|
|
336
|
-
const
|
|
337
|
-
const
|
|
725
|
+
const t = transformRef.current;
|
|
726
|
+
const x = (event.clientX - rect.left - t.x) / t.k;
|
|
727
|
+
const y = (event.clientY - rect.top - t.y) / t.k;
|
|
338
728
|
dragNodeRef.current.fx = x;
|
|
339
729
|
dragNodeRef.current.fy = y;
|
|
340
730
|
};
|
|
@@ -361,7 +751,7 @@ var ForceDirectedGraph = forwardRef(
|
|
|
361
751
|
window.removeEventListener("mouseout", handleWindowLeave);
|
|
362
752
|
window.removeEventListener("blur", handleWindowUp);
|
|
363
753
|
};
|
|
364
|
-
}, [enableDrag
|
|
754
|
+
}, [enableDrag]);
|
|
365
755
|
useEffect(() => {
|
|
366
756
|
if (!gRef.current || !enableDrag) return;
|
|
367
757
|
const g = d32.select(gRef.current);
|
|
@@ -484,98 +874,35 @@ var ForceDirectedGraph = forwardRef(
|
|
|
484
874
|
}
|
|
485
875
|
) }),
|
|
486
876
|
/* @__PURE__ */ jsxs("g", { ref: gRef, children: [
|
|
487
|
-
links.map((link, i) =>
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
children: link.label
|
|
517
|
-
}
|
|
518
|
-
)
|
|
519
|
-
] }, `link-${i}`);
|
|
520
|
-
}),
|
|
521
|
-
nodes.map((node) => {
|
|
522
|
-
if (node.x == null || node.y == null) return null;
|
|
523
|
-
const isSelected = selectedNodeId === node.id;
|
|
524
|
-
const isHovered = hoveredNodeId === node.id;
|
|
525
|
-
const nodeSize = node.size || defaultNodeSize;
|
|
526
|
-
const nodeColor = node.color || defaultNodeColor;
|
|
527
|
-
return /* @__PURE__ */ jsxs(
|
|
528
|
-
"g",
|
|
529
|
-
{
|
|
530
|
-
transform: `translate(${node.x},${node.y})`,
|
|
531
|
-
className: "cursor-pointer node",
|
|
532
|
-
"data-id": node.id,
|
|
533
|
-
onClick: () => handleNodeClick(node),
|
|
534
|
-
onDoubleClick: (event) => handleNodeDoubleClick(event, node),
|
|
535
|
-
onMouseEnter: () => handleNodeMouseEnter(node),
|
|
536
|
-
onMouseLeave: handleNodeMouseLeave,
|
|
537
|
-
onMouseDown: (e) => handleDragStart(e, node),
|
|
538
|
-
children: [
|
|
539
|
-
/* @__PURE__ */ jsx(
|
|
540
|
-
"circle",
|
|
541
|
-
{
|
|
542
|
-
r: nodeSize,
|
|
543
|
-
fill: nodeColor,
|
|
544
|
-
stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
|
|
545
|
-
strokeWidth: pinnedNodes.has(node.id) ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5,
|
|
546
|
-
opacity: isHovered || isSelected ? 1 : 0.9,
|
|
547
|
-
className: "transition-all"
|
|
548
|
-
}
|
|
549
|
-
),
|
|
550
|
-
pinnedNodes.has(node.id) && /* @__PURE__ */ jsx(
|
|
551
|
-
"circle",
|
|
552
|
-
{
|
|
553
|
-
r: nodeSize + 4,
|
|
554
|
-
fill: "none",
|
|
555
|
-
stroke: "#ff6b6b",
|
|
556
|
-
strokeWidth: 1,
|
|
557
|
-
opacity: 0.5,
|
|
558
|
-
className: "pointer-events-none"
|
|
559
|
-
}
|
|
560
|
-
),
|
|
561
|
-
showNodeLabels && node.label && /* @__PURE__ */ jsx(
|
|
562
|
-
"text",
|
|
563
|
-
{
|
|
564
|
-
y: nodeSize + 15,
|
|
565
|
-
fill: "#333",
|
|
566
|
-
fontSize: "12",
|
|
567
|
-
textAnchor: "middle",
|
|
568
|
-
dominantBaseline: "middle",
|
|
569
|
-
pointerEvents: "none",
|
|
570
|
-
className: "select-none",
|
|
571
|
-
children: node.label
|
|
572
|
-
}
|
|
573
|
-
)
|
|
574
|
-
]
|
|
575
|
-
},
|
|
576
|
-
node.id
|
|
577
|
-
);
|
|
578
|
-
}),
|
|
877
|
+
links.map((link, i) => /* @__PURE__ */ jsx(
|
|
878
|
+
LinkItem_default,
|
|
879
|
+
{
|
|
880
|
+
link,
|
|
881
|
+
onClick: handleLinkClick,
|
|
882
|
+
defaultWidth: defaultLinkWidth,
|
|
883
|
+
showLabel: showLinkLabels,
|
|
884
|
+
nodes
|
|
885
|
+
},
|
|
886
|
+
`link-${i}`
|
|
887
|
+
)),
|
|
888
|
+
nodes.map((node) => /* @__PURE__ */ jsx(
|
|
889
|
+
NodeItem_default,
|
|
890
|
+
{
|
|
891
|
+
node,
|
|
892
|
+
isSelected: selectedNodeId === node.id,
|
|
893
|
+
isHovered: hoveredNodeId === node.id,
|
|
894
|
+
pinned: pinnedNodes.has(node.id),
|
|
895
|
+
defaultNodeSize,
|
|
896
|
+
defaultNodeColor,
|
|
897
|
+
showLabel: showNodeLabels,
|
|
898
|
+
onClick: handleNodeClick,
|
|
899
|
+
onDoubleClick: handleNodeDoubleClick,
|
|
900
|
+
onMouseEnter: handleNodeMouseEnter,
|
|
901
|
+
onMouseLeave: handleNodeMouseLeave,
|
|
902
|
+
onMouseDown: handleDragStart
|
|
903
|
+
},
|
|
904
|
+
node.id
|
|
905
|
+
)),
|
|
579
906
|
packageBounds && Object.keys(packageBounds).length > 0 && /* @__PURE__ */ jsx("g", { className: "package-boundaries", pointerEvents: "none", children: Object.entries(packageBounds).map(([pid, b]) => /* @__PURE__ */ jsxs("g", { children: [
|
|
580
907
|
/* @__PURE__ */ jsx(
|
|
581
908
|
"circle",
|