@data-navigator/inspector 1.0.2 → 1.0.3
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/package.json +1 -1
- package/src/tree-graph.js +59 -13
package/package.json
CHANGED
package/src/tree-graph.js
CHANGED
|
@@ -55,15 +55,24 @@ export function TreeGraph(
|
|
|
55
55
|
if (G && nodeGroups === undefined) nodeGroups = sort(G);
|
|
56
56
|
const color = nodeGroup == null ? null : scaleOrdinal(nodeGroups, colors);
|
|
57
57
|
|
|
58
|
-
// Build set of parent-child navigation rule names.
|
|
58
|
+
// Build set of parent-child and sibling navigation rule names.
|
|
59
59
|
const parentChildRules = new Set(['parent', 'child']);
|
|
60
|
+
const siblingRulePairs = [['left', 'right']]; // default pair
|
|
60
61
|
if (dimensions) {
|
|
61
62
|
Object.values(dimensions).forEach(dim => {
|
|
62
63
|
(dim.navigationRules?.parent_child || []).forEach(r => parentChildRules.add(r));
|
|
64
|
+
const ss = dim.navigationRules?.sibling_sibling;
|
|
65
|
+
if (ss && ss.length === 2) siblingRulePairs.push(ss);
|
|
63
66
|
});
|
|
64
67
|
}
|
|
68
|
+
// Map each sibling rule name to its pair for splitting merged edges.
|
|
69
|
+
const ruleToPair = {};
|
|
70
|
+
siblingRulePairs.forEach(pair => {
|
|
71
|
+
pair.forEach(r => { ruleToPair[r] = pair.join(','); });
|
|
72
|
+
});
|
|
65
73
|
|
|
66
|
-
// Classify links.
|
|
74
|
+
// Classify links. Sibling links with merged rules (multiple pairs)
|
|
75
|
+
// are split into one entry per distinct pair so each gets its own arc.
|
|
67
76
|
const parentChildLinks = [];
|
|
68
77
|
const siblingLinks = [];
|
|
69
78
|
linkData.forEach(l => {
|
|
@@ -75,7 +84,15 @@ export function TreeGraph(
|
|
|
75
84
|
if (isParentChild) {
|
|
76
85
|
parentChildLinks.push(l);
|
|
77
86
|
} else {
|
|
78
|
-
|
|
87
|
+
// Split by distinct sibling rule pairs.
|
|
88
|
+
const seenPairs = new Set();
|
|
89
|
+
rules.forEach(r => {
|
|
90
|
+
const pairKey = ruleToPair[r] || r;
|
|
91
|
+
if (!seenPairs.has(pairKey)) {
|
|
92
|
+
seenPairs.add(pairKey);
|
|
93
|
+
siblingLinks.push({ source: l.source, target: l.target, navigationRules: [r] });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
79
96
|
}
|
|
80
97
|
});
|
|
81
98
|
|
|
@@ -138,10 +155,11 @@ export function TreeGraph(
|
|
|
138
155
|
|
|
139
156
|
const hasLevel0 = level0.length > 0;
|
|
140
157
|
const upperRows = (hasLevel0 ? 1 : 0) + 1 + 1; // l0?, l1, l2
|
|
141
|
-
// Upper hierarchy gets
|
|
142
|
-
const upperHeight = usableHeight * 0.
|
|
158
|
+
// Upper hierarchy gets 25% of height, leaves get the rest after a gap
|
|
159
|
+
const upperHeight = usableHeight * 0.25;
|
|
143
160
|
const upperSpacing = upperRows > 1 ? upperHeight / (upperRows - 1) : upperHeight;
|
|
144
|
-
const
|
|
161
|
+
const leafGap = usableHeight * 0.05; // small gap between hierarchy and leaves
|
|
162
|
+
const leafYPos = padding.top + upperHeight + leafGap;
|
|
145
163
|
|
|
146
164
|
let currentRow = 0;
|
|
147
165
|
const yFor = (row) => padding.top + row * upperSpacing;
|
|
@@ -234,17 +252,18 @@ export function TreeGraph(
|
|
|
234
252
|
const cols = div1Ids.length || 1;
|
|
235
253
|
const rows = div2Ids.length || 1;
|
|
236
254
|
|
|
237
|
-
// Grid occupies the leaf area
|
|
238
|
-
const gridTop = leafY
|
|
255
|
+
// Grid occupies the full leaf area
|
|
256
|
+
const gridTop = leafY;
|
|
239
257
|
const gridBottom = height - padding.bottom;
|
|
240
258
|
const gridHeight = gridBottom - gridTop;
|
|
241
259
|
const colSpacing = usableWidth / (cols + 1);
|
|
242
260
|
const rowSpacingGrid = gridHeight / (rows + 1);
|
|
243
261
|
|
|
262
|
+
// Group leaves by their grid cell so we can spread co-located nodes.
|
|
263
|
+
const cellGroups = {};
|
|
244
264
|
leafNodes.forEach(n => {
|
|
245
265
|
const ld = leafDivisions[n.id];
|
|
246
266
|
if (!ld) {
|
|
247
|
-
// Unaffiliated leaf — place to the right
|
|
248
267
|
n.x = width - padding.right;
|
|
249
268
|
n.y = leafY;
|
|
250
269
|
return;
|
|
@@ -253,8 +272,27 @@ export function TreeGraph(
|
|
|
253
272
|
const rowDivId = ld[dim2Key];
|
|
254
273
|
const colIdx = div1Ids.indexOf(colDivId);
|
|
255
274
|
const rowIdx = div2Ids.indexOf(rowDivId);
|
|
256
|
-
|
|
257
|
-
|
|
275
|
+
const cellKey = `${colIdx},${rowIdx}`;
|
|
276
|
+
if (!cellGroups[cellKey]) cellGroups[cellKey] = { colIdx, rowIdx, nodes: [] };
|
|
277
|
+
cellGroups[cellKey].nodes.push(n);
|
|
278
|
+
});
|
|
279
|
+
// Position each group, spreading nodes within shared cells.
|
|
280
|
+
const cellPad = Math.min(colSpacing, rowSpacingGrid) * 0.6;
|
|
281
|
+
Object.values(cellGroups).forEach(({ colIdx, rowIdx, nodes: cellNodes }) => {
|
|
282
|
+
const cx = padding.left + colSpacing * (colIdx + 1);
|
|
283
|
+
const cy = gridTop + rowSpacingGrid * (rowIdx + 1);
|
|
284
|
+
if (cellNodes.length === 1) {
|
|
285
|
+
cellNodes[0].x = cx;
|
|
286
|
+
cellNodes[0].y = cy;
|
|
287
|
+
} else {
|
|
288
|
+
// Spread vertically within the cell (matches row-based dim2)
|
|
289
|
+
const spread = cellPad;
|
|
290
|
+
const step = spread / (cellNodes.length - 1);
|
|
291
|
+
cellNodes.forEach((n, i) => {
|
|
292
|
+
n.x = cx;
|
|
293
|
+
n.y = cy - spread / 2 + step * i;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
258
296
|
});
|
|
259
297
|
}
|
|
260
298
|
|
|
@@ -276,6 +314,8 @@ export function TreeGraph(
|
|
|
276
314
|
// Draw sibling links as arced paths (dashed, behind parent-child links).
|
|
277
315
|
// Regular siblings arc above (horizontal) or left (vertical).
|
|
278
316
|
// Wrap-around (circular) siblings arc below/right with a larger curve.
|
|
317
|
+
// When multiple edges share the same node pair, each gets a larger arc.
|
|
318
|
+
const edgePairCounts = {};
|
|
279
319
|
const siblingArc = (d) => {
|
|
280
320
|
const s = nodeObjById[d.source];
|
|
281
321
|
const t = nodeObjById[d.target];
|
|
@@ -287,8 +327,14 @@ export function TreeGraph(
|
|
|
287
327
|
// Detect wrap-around: source is to the right of target (horizontal)
|
|
288
328
|
// or below target (vertical) — these are circular edges.
|
|
289
329
|
const isWrap = isHorizontal ? (sx > tx) : (sy > ty);
|
|
290
|
-
//
|
|
291
|
-
|
|
330
|
+
// Track regular and wrap arcs separately — they curve in opposite
|
|
331
|
+
// directions so only same-direction arcs need stacking.
|
|
332
|
+
const pairKey = [d.source, d.target].sort().join('::') + (isWrap ? '::wrap' : '::reg');
|
|
333
|
+
const edgeIndex = edgePairCounts[pairKey] || 0;
|
|
334
|
+
edgePairCounts[pairKey] = edgeIndex + 1;
|
|
335
|
+
// Arc offset scales with distance; increase by 20% for each additional same-direction edge
|
|
336
|
+
const arcFraction = 0.3 + edgeIndex * 0.2;
|
|
337
|
+
const arcAmount = Math.min(dist * arcFraction, 40 + edgeIndex * 25);
|
|
292
338
|
const midX = (sx + tx) / 2;
|
|
293
339
|
const midY = (sy + ty) / 2;
|
|
294
340
|
let cx, cy;
|