@carlonicora/nextjs-jsonapi 1.97.2 → 1.99.0
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/{BlockNoteEditor-LRJUTHTW.mjs → BlockNoteEditor-IBV3KBQM.mjs} +2 -2
- package/dist/{BlockNoteEditor-OOZGXU6E.js → BlockNoteEditor-LYJUF5N4.js} +9 -9
- package/dist/{BlockNoteEditor-OOZGXU6E.js.map → BlockNoteEditor-LYJUF5N4.js.map} +1 -1
- package/dist/billing/index.js +299 -299
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-XYGK26YG.mjs → chunk-CDNVUON3.mjs} +374 -148
- package/dist/chunk-CDNVUON3.mjs.map +1 -0
- package/dist/{chunk-HCOX3PKM.js → chunk-TRTKIQUB.js} +608 -382
- package/dist/chunk-TRTKIQUB.js.map +1 -0
- package/dist/client/index.d.mts +32 -3
- package/dist/client/index.d.ts +32 -3
- package/dist/client/index.js +4 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -1
- package/dist/components/index.d.mts +5 -2
- package/dist/components/index.d.ts +5 -2
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/{content.fields-hzZvhlBd.d.mts → content.fields-xH3TGvVk.d.mts} +2 -0
- package/dist/{content.fields-hzZvhlBd.d.ts → content.fields-xH3TGvVk.d.ts} +2 -0
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/core/index.d.mts +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +3 -1
- package/src/components/forms/FormDate.tsx +27 -3
- package/src/components/forms/FormDateTime.tsx +40 -8
- package/src/hooks/__tests__/computeLayeredLayout.spec.ts +152 -0
- package/src/hooks/computeLayeredLayout.ts +96 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useCustomD3Graph.tsx +310 -148
- package/src/interfaces/d3.node.interface.ts +2 -0
- package/dist/chunk-HCOX3PKM.js.map +0 -1
- package/dist/chunk-XYGK26YG.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-LRJUTHTW.mjs.map → BlockNoteEditor-IBV3KBQM.mjs.map} +0 -0
|
@@ -5,6 +5,7 @@ import { Loader2 } from "lucide-react";
|
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
6
6
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
7
7
|
import { D3Link, D3Node } from "../interfaces";
|
|
8
|
+
import { computeLayeredLayout, type LayeredRankDir } from "./computeLayeredLayout";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Custom hook for D3 graph visualization with larger circles and more interactive features
|
|
@@ -14,6 +15,15 @@ export function useCustomD3Graph(
|
|
|
14
15
|
links: D3Link[],
|
|
15
16
|
onNodeClick: (nodeId: string) => void,
|
|
16
17
|
visibleNodeIds?: Set<string>,
|
|
18
|
+
options?: {
|
|
19
|
+
directed?: boolean;
|
|
20
|
+
layout?: "radial" | "layered";
|
|
21
|
+
layered?: {
|
|
22
|
+
rankdir?: LayeredRankDir;
|
|
23
|
+
nodesep?: number;
|
|
24
|
+
ranksep?: number;
|
|
25
|
+
};
|
|
26
|
+
},
|
|
17
27
|
loadingNodeIds?: Set<string>,
|
|
18
28
|
containerKey?: string | number,
|
|
19
29
|
) {
|
|
@@ -154,6 +164,9 @@ export function useCustomD3Graph(
|
|
|
154
164
|
|
|
155
165
|
const getNodeColor = useCallback(
|
|
156
166
|
(node: D3Node) => {
|
|
167
|
+
if (node.color) {
|
|
168
|
+
return node.washedOut ? washOutColor(node.color) : node.color;
|
|
169
|
+
}
|
|
157
170
|
const baseColor = colorScale.get(node.instanceType) || "gray";
|
|
158
171
|
if (node.washedOut) {
|
|
159
172
|
return washOutColor(baseColor);
|
|
@@ -190,6 +203,26 @@ export function useCustomD3Graph(
|
|
|
190
203
|
|
|
191
204
|
const graphGroup = svg.append("g").attr("class", "graph-content");
|
|
192
205
|
|
|
206
|
+
const nodeRadius = 40;
|
|
207
|
+
const directed = options?.directed === true;
|
|
208
|
+
|
|
209
|
+
if (directed) {
|
|
210
|
+
const defs = svg.append("defs");
|
|
211
|
+
defs
|
|
212
|
+
.append("marker")
|
|
213
|
+
.attr("id", "narr8-arrow")
|
|
214
|
+
.attr("viewBox", "-10 -10 20 20")
|
|
215
|
+
.attr("markerWidth", 14)
|
|
216
|
+
.attr("markerHeight", 14)
|
|
217
|
+
.attr("markerUnits", "userSpaceOnUse")
|
|
218
|
+
.attr("orient", "auto")
|
|
219
|
+
.attr("refX", 0)
|
|
220
|
+
.attr("refY", 0)
|
|
221
|
+
.append("path")
|
|
222
|
+
.attr("d", "M-10,-10 L0,0 L-10,10 z")
|
|
223
|
+
.attr("fill", "#999");
|
|
224
|
+
}
|
|
225
|
+
|
|
193
226
|
const zoom = d3
|
|
194
227
|
.zoom<SVGSVGElement, unknown>()
|
|
195
228
|
.scaleExtent([0.1, 4])
|
|
@@ -206,170 +239,245 @@ export function useCustomD3Graph(
|
|
|
206
239
|
.on("wheel.zoom", null)
|
|
207
240
|
.on("dblclick.zoom", null);
|
|
208
241
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
242
|
+
const layoutMode = options?.layout ?? "radial";
|
|
243
|
+
let layeredPositionsApplied = false;
|
|
244
|
+
|
|
245
|
+
if (layoutMode === "layered") {
|
|
246
|
+
const layeredOpts = options?.layered ?? {};
|
|
247
|
+
const positions = computeLayeredLayout(visibleNodes, visibleLinks, {
|
|
248
|
+
rankdir: layeredOpts.rankdir ?? "LR",
|
|
249
|
+
nodesep: layeredOpts.nodesep,
|
|
250
|
+
ranksep: layeredOpts.ranksep,
|
|
251
|
+
minNodeWidth: nodeRadius * 2,
|
|
252
|
+
minNodeHeight: nodeRadius * 2,
|
|
253
|
+
});
|
|
215
254
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
255
|
+
if (positions) {
|
|
256
|
+
visibleNodes.forEach((node) => {
|
|
257
|
+
const saved = nodePositionsRef.current.get(node.id);
|
|
258
|
+
if (saved) {
|
|
259
|
+
node.fx = saved.x;
|
|
260
|
+
node.fy = saved.y;
|
|
261
|
+
node.x = saved.x;
|
|
262
|
+
node.y = saved.y;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const pos = positions.get(node.id);
|
|
266
|
+
if (pos) {
|
|
267
|
+
node.fx = pos.x;
|
|
268
|
+
node.fy = pos.y;
|
|
269
|
+
node.x = pos.x;
|
|
270
|
+
node.y = pos.y;
|
|
271
|
+
nodePositionsRef.current.set(node.id, { x: pos.x, y: pos.y });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
// d3.forceLink normally mutates link.source/link.target from string
|
|
275
|
+
// IDs to D3Node references when the simulation initializes. The
|
|
276
|
+
// layered branch skips the simulation, so we resolve those refs
|
|
277
|
+
// ourselves — otherwise the downstream link rendering reads
|
|
278
|
+
// `(d.source as D3Node).x` against a string and draws every line
|
|
279
|
+
// at (0,0)→(0,0).
|
|
280
|
+
const nodeById = new Map<string, D3Node>();
|
|
281
|
+
visibleNodes.forEach((n) => nodeById.set(n.id, n));
|
|
282
|
+
visibleLinks.forEach((link) => {
|
|
283
|
+
if (typeof link.source === "string") {
|
|
284
|
+
const src = nodeById.get(link.source);
|
|
285
|
+
if (src) link.source = src;
|
|
286
|
+
}
|
|
287
|
+
if (typeof link.target === "string") {
|
|
288
|
+
const tgt = nodeById.get(link.target);
|
|
289
|
+
if (tgt) link.target = tgt;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
layeredPositionsApplied = true;
|
|
293
|
+
} else {
|
|
294
|
+
console.warn("[useCustomD3Graph] Layered layout failed; falling back to radial.");
|
|
225
295
|
}
|
|
226
|
-
|
|
296
|
+
}
|
|
227
297
|
|
|
228
|
-
|
|
229
|
-
depth: 0,
|
|
230
|
-
parent: null,
|
|
231
|
-
children: [],
|
|
232
|
-
});
|
|
298
|
+
let simulation: d3.Simulation<D3Node, D3Link> | null = null;
|
|
233
299
|
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const
|
|
300
|
+
if (!layeredPositionsApplied) {
|
|
301
|
+
const childDistanceFromRoot = Math.min(width, height) * 0.4;
|
|
302
|
+
const grandchildDistanceFromChild = nodeRadius * 10;
|
|
303
|
+
|
|
304
|
+
const centralNodeId = nodes[0].id;
|
|
237
305
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
306
|
+
const nodeHierarchy = new Map<
|
|
307
|
+
string,
|
|
308
|
+
{
|
|
309
|
+
depth: number;
|
|
310
|
+
parent: string | null;
|
|
311
|
+
children: string[];
|
|
312
|
+
angle?: number;
|
|
313
|
+
x?: number;
|
|
314
|
+
y?: number;
|
|
243
315
|
}
|
|
244
|
-
|
|
245
|
-
});
|
|
316
|
+
>();
|
|
246
317
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
318
|
+
nodeHierarchy.set(centralNodeId, {
|
|
319
|
+
depth: 0,
|
|
320
|
+
parent: null,
|
|
321
|
+
children: [],
|
|
322
|
+
});
|
|
250
323
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
sourceNode.children.push(targetId);
|
|
255
|
-
}
|
|
256
|
-
});
|
|
324
|
+
visibleLinks.forEach((link) => {
|
|
325
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
326
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
257
327
|
|
|
258
|
-
|
|
328
|
+
if (sourceId === centralNodeId) {
|
|
329
|
+
nodeHierarchy.set(targetId, { depth: 1, parent: centralNodeId, children: [] });
|
|
330
|
+
const rootNode = nodeHierarchy.get(centralNodeId);
|
|
331
|
+
if (rootNode) {
|
|
332
|
+
rootNode.children.push(targetId);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
259
336
|
|
|
260
|
-
|
|
337
|
+
visibleLinks.forEach((link) => {
|
|
338
|
+
const sourceId = typeof link.source === "string" ? link.source : link.source.id;
|
|
339
|
+
const targetId = typeof link.target === "string" ? link.target : link.target.id;
|
|
261
340
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
childNode.y = height / 2 + childDistanceFromRoot * Math.sin(angle);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
341
|
+
const sourceNode = nodeHierarchy.get(sourceId);
|
|
342
|
+
if (sourceNode && sourceNode.depth === 1 && !nodeHierarchy.has(targetId)) {
|
|
343
|
+
nodeHierarchy.set(targetId, { depth: 2, parent: sourceId, children: [] });
|
|
344
|
+
sourceNode.children.push(targetId);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
271
347
|
|
|
272
|
-
|
|
273
|
-
if (node.depth === 1 && node.angle !== undefined && node.x !== undefined && node.y !== undefined) {
|
|
274
|
-
const childAngle = node.angle;
|
|
275
|
-
const childX = node.x;
|
|
276
|
-
const childY = node.y;
|
|
277
|
-
const grandchildren = node.children;
|
|
278
|
-
|
|
279
|
-
if (grandchildren.length === 0) continue;
|
|
280
|
-
|
|
281
|
-
const dirX = childX - width / 2;
|
|
282
|
-
const dirY = childY - height / 2;
|
|
283
|
-
const dirLength = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
284
|
-
|
|
285
|
-
const normDirX = dirX / dirLength;
|
|
286
|
-
const normDirY = dirY / dirLength;
|
|
287
|
-
|
|
288
|
-
if (grandchildren.length === 1) {
|
|
289
|
-
const grandchildId = grandchildren[0];
|
|
290
|
-
const grandchildNode = nodeHierarchy.get(grandchildId);
|
|
291
|
-
if (grandchildNode) {
|
|
292
|
-
grandchildNode.x = childX + normDirX * grandchildDistanceFromChild;
|
|
293
|
-
grandchildNode.y = childY + normDirY * grandchildDistanceFromChild;
|
|
294
|
-
grandchildNode.angle = childAngle;
|
|
295
|
-
}
|
|
296
|
-
} else {
|
|
297
|
-
// Multiple grandchildren - arrange in semicircular arc
|
|
298
|
-
const numChildren = grandchildren.length;
|
|
348
|
+
const rootChildren = nodeHierarchy.get(centralNodeId)?.children || [];
|
|
299
349
|
|
|
300
|
-
|
|
301
|
-
const minArc = Math.PI / 3; // 60 degrees
|
|
302
|
-
const maxArc = Math.PI; // 180 degrees
|
|
303
|
-
const arcProgress = Math.min(1, (numChildren - 2) / 5);
|
|
304
|
-
const arcSpan = minArc + arcProgress * (maxArc - minArc);
|
|
350
|
+
const childAngleStep = (2 * Math.PI) / Math.max(rootChildren.length, 1);
|
|
305
351
|
|
|
306
|
-
|
|
307
|
-
|
|
352
|
+
rootChildren.forEach((childId, index) => {
|
|
353
|
+
const childNode = nodeHierarchy.get(childId);
|
|
354
|
+
if (childNode) {
|
|
355
|
+
const angle = index * childAngleStep;
|
|
356
|
+
childNode.angle = angle;
|
|
357
|
+
childNode.x = width / 2 + childDistanceFromRoot * Math.cos(angle);
|
|
358
|
+
childNode.y = height / 2 + childDistanceFromRoot * Math.sin(angle);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
308
361
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
362
|
+
for (const [_nodeId, node] of nodeHierarchy.entries()) {
|
|
363
|
+
if (node.depth === 1 && node.angle !== undefined && node.x !== undefined && node.y !== undefined) {
|
|
364
|
+
const childAngle = node.angle;
|
|
365
|
+
const childX = node.x;
|
|
366
|
+
const childY = node.y;
|
|
367
|
+
const grandchildren = node.children;
|
|
312
368
|
|
|
313
|
-
|
|
314
|
-
const angleOffset = numChildren > 1 ? (index / (numChildren - 1)) * arcSpan : 0;
|
|
315
|
-
const angle = startAngle + angleOffset;
|
|
369
|
+
if (grandchildren.length === 0) continue;
|
|
316
370
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
grandchildNode.angle = angle;
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
371
|
+
const dirX = childX - width / 2;
|
|
372
|
+
const dirY = childY - height / 2;
|
|
373
|
+
const dirLength = Math.sqrt(dirX * dirX + dirY * dirY);
|
|
325
374
|
|
|
326
|
-
|
|
327
|
-
|
|
375
|
+
const normDirX = dirX / dirLength;
|
|
376
|
+
const normDirY = dirY / dirLength;
|
|
328
377
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
378
|
+
if (grandchildren.length === 1) {
|
|
379
|
+
const grandchildId = grandchildren[0];
|
|
380
|
+
const grandchildNode = nodeHierarchy.get(grandchildId);
|
|
381
|
+
if (grandchildNode) {
|
|
382
|
+
grandchildNode.x = childX + normDirX * grandchildDistanceFromChild;
|
|
383
|
+
grandchildNode.y = childY + normDirY * grandchildDistanceFromChild;
|
|
384
|
+
grandchildNode.angle = childAngle;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// Multiple grandchildren - arrange in semicircular arc
|
|
388
|
+
const numChildren = grandchildren.length;
|
|
389
|
+
|
|
390
|
+
// Dynamic arc span: scale from 60° (2 children) to 180° (7+ children)
|
|
391
|
+
const minArc = Math.PI / 3; // 60 degrees
|
|
392
|
+
const maxArc = Math.PI; // 180 degrees
|
|
393
|
+
const arcProgress = Math.min(1, (numChildren - 2) / 5);
|
|
394
|
+
const arcSpan = minArc + arcProgress * (maxArc - minArc);
|
|
395
|
+
|
|
396
|
+
// Calculate starting angle (center the arc around the radial direction)
|
|
397
|
+
const startAngle = childAngle - arcSpan / 2;
|
|
398
|
+
|
|
399
|
+
grandchildren.forEach((grandchildId, index) => {
|
|
400
|
+
const grandchildNode = nodeHierarchy.get(grandchildId);
|
|
401
|
+
if (!grandchildNode) return;
|
|
402
|
+
|
|
403
|
+
// Calculate angle for this child
|
|
404
|
+
const angleOffset = numChildren > 1 ? (index / (numChildren - 1)) * arcSpan : 0;
|
|
405
|
+
const angle = startAngle + angleOffset;
|
|
406
|
+
|
|
407
|
+
// Position at constant radius from parent
|
|
408
|
+
grandchildNode.x = childX + grandchildDistanceFromChild * Math.cos(angle);
|
|
409
|
+
grandchildNode.y = childY + grandchildDistanceFromChild * Math.sin(angle);
|
|
410
|
+
grandchildNode.angle = angle;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
344
413
|
}
|
|
345
414
|
}
|
|
346
|
-
});
|
|
347
415
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.force(
|
|
351
|
-
"link",
|
|
352
|
-
d3
|
|
353
|
-
.forceLink<D3Node, D3Link>(visibleLinks)
|
|
354
|
-
.id((d) => d.id)
|
|
355
|
-
.distance(nodeRadius * 3)
|
|
356
|
-
.strength(0.1),
|
|
357
|
-
)
|
|
358
|
-
.force("charge", d3.forceManyBody().strength(-500).distanceMax(300))
|
|
359
|
-
.force("collision", d3.forceCollide().radius(nodeRadius * 1.2))
|
|
360
|
-
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.1));
|
|
416
|
+
visibleNodes.forEach((node) => {
|
|
417
|
+
const savedPosition = nodePositionsRef.current.get(node.id);
|
|
361
418
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
419
|
+
if (savedPosition) {
|
|
420
|
+
node.fx = savedPosition.x;
|
|
421
|
+
node.fy = savedPosition.y;
|
|
422
|
+
} else {
|
|
423
|
+
const hierarchyNode = nodeHierarchy.get(node.id);
|
|
424
|
+
if (hierarchyNode && hierarchyNode.x !== undefined && hierarchyNode.y !== undefined) {
|
|
425
|
+
node.fx = hierarchyNode.x;
|
|
426
|
+
node.fy = hierarchyNode.y;
|
|
427
|
+
// Save the calculated position so it persists across re-renders
|
|
428
|
+
nodePositionsRef.current.set(node.id, { x: hierarchyNode.x, y: hierarchyNode.y });
|
|
429
|
+
} else if (node.id === centralNodeId) {
|
|
430
|
+
node.fx = width / 2;
|
|
431
|
+
node.fy = height / 2;
|
|
432
|
+
// Save the center position
|
|
433
|
+
nodePositionsRef.current.set(node.id, { x: width / 2, y: height / 2 });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
366
437
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
438
|
+
simulation = d3
|
|
439
|
+
.forceSimulation<D3Node>(visibleNodes)
|
|
440
|
+
.force(
|
|
441
|
+
"link",
|
|
442
|
+
d3
|
|
443
|
+
.forceLink<D3Node, D3Link>(visibleLinks)
|
|
444
|
+
.id((d) => d.id)
|
|
445
|
+
.distance(nodeRadius * 3)
|
|
446
|
+
.strength(0.1),
|
|
447
|
+
)
|
|
448
|
+
.force("charge", d3.forceManyBody().strength(-500).distanceMax(300))
|
|
449
|
+
.force("collision", d3.forceCollide().radius(nodeRadius * 1.2))
|
|
450
|
+
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.1));
|
|
451
|
+
|
|
452
|
+
simulation.stop();
|
|
453
|
+
for (let i = 0; i < 100; i++) {
|
|
454
|
+
simulation.tick();
|
|
371
455
|
}
|
|
372
|
-
|
|
456
|
+
|
|
457
|
+
visibleNodes.forEach((node) => {
|
|
458
|
+
if (node.fx === undefined) {
|
|
459
|
+
node.fx = node.x;
|
|
460
|
+
node.fy = node.y;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
} // end if (!layeredPositionsApplied)
|
|
464
|
+
|
|
465
|
+
// When directed, stop the line at the target node boundary so the
|
|
466
|
+
// arrowhead tip sits just outside the circle rather than inside it.
|
|
467
|
+
const linkX2 = (sx: number, sy: number, tx: number, ty: number): number => {
|
|
468
|
+
if (!directed) return tx;
|
|
469
|
+
const dx = tx - sx;
|
|
470
|
+
const dy = ty - sy;
|
|
471
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
472
|
+
return tx - (dx / dist) * nodeRadius;
|
|
473
|
+
};
|
|
474
|
+
const linkY2 = (sx: number, sy: number, tx: number, ty: number): number => {
|
|
475
|
+
if (!directed) return ty;
|
|
476
|
+
const dx = tx - sx;
|
|
477
|
+
const dy = ty - sy;
|
|
478
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
479
|
+
return ty - (dy / dist) * nodeRadius;
|
|
480
|
+
};
|
|
373
481
|
|
|
374
482
|
const link = graphGroup
|
|
375
483
|
.append("g")
|
|
@@ -380,9 +488,18 @@ export function useCustomD3Graph(
|
|
|
380
488
|
.join("line")
|
|
381
489
|
.attr("x1", (d) => (d.source as D3Node).x || 0)
|
|
382
490
|
.attr("y1", (d) => (d.source as D3Node).y || 0)
|
|
383
|
-
.attr("x2", (d) =>
|
|
384
|
-
|
|
385
|
-
|
|
491
|
+
.attr("x2", (d) => {
|
|
492
|
+
const s = d.source as D3Node;
|
|
493
|
+
const t = d.target as D3Node;
|
|
494
|
+
return linkX2(s.x || 0, s.y || 0, t.x || 0, t.y || 0);
|
|
495
|
+
})
|
|
496
|
+
.attr("y2", (d) => {
|
|
497
|
+
const s = d.source as D3Node;
|
|
498
|
+
const t = d.target as D3Node;
|
|
499
|
+
return linkY2(s.x || 0, s.y || 0, t.x || 0, t.y || 0);
|
|
500
|
+
})
|
|
501
|
+
.attr("stroke-width", 1.5)
|
|
502
|
+
.attr("marker-end", directed ? "url(#narr8-arrow)" : null);
|
|
386
503
|
|
|
387
504
|
const node = graphGroup
|
|
388
505
|
.append("g")
|
|
@@ -423,12 +540,22 @@ export function useCustomD3Graph(
|
|
|
423
540
|
return source.fy || source.y || 0;
|
|
424
541
|
})
|
|
425
542
|
.attr("x2", (l) => {
|
|
543
|
+
const source = l.source as D3Node;
|
|
426
544
|
const target = l.target as D3Node;
|
|
427
|
-
|
|
545
|
+
const sx = source.fx || source.x || 0;
|
|
546
|
+
const sy = source.fy || source.y || 0;
|
|
547
|
+
const tx = target.fx || target.x || 0;
|
|
548
|
+
const ty = target.fy || target.y || 0;
|
|
549
|
+
return linkX2(sx, sy, tx, ty);
|
|
428
550
|
})
|
|
429
551
|
.attr("y2", (l) => {
|
|
552
|
+
const source = l.source as D3Node;
|
|
430
553
|
const target = l.target as D3Node;
|
|
431
|
-
|
|
554
|
+
const sx = source.fx || source.x || 0;
|
|
555
|
+
const sy = source.fy || source.y || 0;
|
|
556
|
+
const tx = target.fx || target.x || 0;
|
|
557
|
+
const ty = target.fy || target.y || 0;
|
|
558
|
+
return linkY2(sx, sy, tx, ty);
|
|
432
559
|
});
|
|
433
560
|
})
|
|
434
561
|
.on("end", function (event, d) {
|
|
@@ -489,8 +616,13 @@ export function useCustomD3Graph(
|
|
|
489
616
|
.attr("r", nodeRadius)
|
|
490
617
|
.attr("filter", null);
|
|
491
618
|
|
|
492
|
-
// Return text to normal size with smooth transform
|
|
493
|
-
|
|
619
|
+
// Return text to normal size with smooth transform. Two-line labels
|
|
620
|
+
// (when `d.subtitle` is set) sit one line-gap higher than the single
|
|
621
|
+
// line case — restoring to the bare `nodeRadius + 5` offset would
|
|
622
|
+
// shift the title down by that gap (visibly jumping after the first
|
|
623
|
+
// hover). The 16 here matches the `lineGap` used when the two-line
|
|
624
|
+
// text is first rendered (see the `d.subtitle` branch below).
|
|
625
|
+
const normalOffset = nodeRadius + 5 + (d.subtitle ? 16 : 0);
|
|
494
626
|
currentNode
|
|
495
627
|
.select("text")
|
|
496
628
|
.transition()
|
|
@@ -607,6 +739,24 @@ export function useCustomD3Graph(
|
|
|
607
739
|
.attr("dy", index === 0 ? `${startY}em` : `${lineHeight}em`)
|
|
608
740
|
.text(word);
|
|
609
741
|
});
|
|
742
|
+
} else if (d.subtitle) {
|
|
743
|
+
// Two-line label: name (bigger, bolder) on line 1, subtitle
|
|
744
|
+
// (smaller, dimmed) on line 2. Used by the scene graph to show
|
|
745
|
+
// the HAPPENS_AT location name beneath the scene title. The
|
|
746
|
+
// whole text block is lifted above the circle so the lower line
|
|
747
|
+
// sits at the same baseline the single-line case uses.
|
|
748
|
+
const titleSize = 16;
|
|
749
|
+
const subtitleSize = 11;
|
|
750
|
+
const lineGap = titleSize; // px between baselines
|
|
751
|
+
textElement.attr("dy", -nodeRadius - 5 - lineGap).attr("fill", "currentColor");
|
|
752
|
+
textElement.append("tspan").attr("x", 0).attr("font-size", titleSize).attr("font-weight", 700).text(d.name);
|
|
753
|
+
textElement
|
|
754
|
+
.append("tspan")
|
|
755
|
+
.attr("x", 0)
|
|
756
|
+
.attr("dy", lineGap)
|
|
757
|
+
.attr("font-size", subtitleSize)
|
|
758
|
+
.attr("fill-opacity", 0.7)
|
|
759
|
+
.text(d.subtitle);
|
|
610
760
|
} else {
|
|
611
761
|
// Non-root nodes: single line text above the circle. The
|
|
612
762
|
// optional `bold` flag lets the consumer highlight a focal
|
|
@@ -623,9 +773,21 @@ export function useCustomD3Graph(
|
|
|
623
773
|
});
|
|
624
774
|
|
|
625
775
|
return () => {
|
|
626
|
-
simulation
|
|
776
|
+
simulation?.stop();
|
|
627
777
|
};
|
|
628
|
-
}, [
|
|
778
|
+
}, [
|
|
779
|
+
nodes,
|
|
780
|
+
links,
|
|
781
|
+
colorScale,
|
|
782
|
+
visibleNodeIds,
|
|
783
|
+
options?.directed,
|
|
784
|
+
options?.layout,
|
|
785
|
+
options?.layered?.rankdir,
|
|
786
|
+
options?.layered?.nodesep,
|
|
787
|
+
options?.layered?.ranksep,
|
|
788
|
+
loadingNodeIds,
|
|
789
|
+
onNodeClick,
|
|
790
|
+
]);
|
|
629
791
|
|
|
630
792
|
const zoomIn = useCallback(() => {
|
|
631
793
|
if (!svgRef.current || !zoomBehaviorRef.current) return;
|