@data-navigator/inspector 1.0.1 → 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 +98 -23
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
|
|
|
@@ -131,17 +148,21 @@ export function TreeGraph(
|
|
|
131
148
|
});
|
|
132
149
|
|
|
133
150
|
// Compute layout positions.
|
|
151
|
+
// Upper levels (dimensions, divisions) are compressed; leaves get more room.
|
|
134
152
|
const padding = { top: 30, bottom: 20, left: 30, right: 30 };
|
|
135
153
|
const usableWidth = width - padding.left - padding.right;
|
|
136
154
|
const usableHeight = height - padding.top - padding.bottom;
|
|
137
155
|
|
|
138
|
-
// Determine rows: level0 (optional), level1, level2, leaves
|
|
139
156
|
const hasLevel0 = level0.length > 0;
|
|
140
|
-
const
|
|
141
|
-
|
|
157
|
+
const upperRows = (hasLevel0 ? 1 : 0) + 1 + 1; // l0?, l1, l2
|
|
158
|
+
// Upper hierarchy gets 25% of height, leaves get the rest after a gap
|
|
159
|
+
const upperHeight = usableHeight * 0.25;
|
|
160
|
+
const upperSpacing = upperRows > 1 ? upperHeight / (upperRows - 1) : upperHeight;
|
|
161
|
+
const leafGap = usableHeight * 0.05; // small gap between hierarchy and leaves
|
|
162
|
+
const leafYPos = padding.top + upperHeight + leafGap;
|
|
142
163
|
|
|
143
164
|
let currentRow = 0;
|
|
144
|
-
const yFor = (row) => padding.top + row *
|
|
165
|
+
const yFor = (row) => padding.top + row * upperSpacing;
|
|
145
166
|
|
|
146
167
|
// Level 0
|
|
147
168
|
if (hasLevel0) {
|
|
@@ -192,7 +213,7 @@ export function TreeGraph(
|
|
|
192
213
|
currentRow++;
|
|
193
214
|
|
|
194
215
|
// Leaf nodes
|
|
195
|
-
const leafY =
|
|
216
|
+
const leafY = leafYPos;
|
|
196
217
|
if (dimOrder.length === 0 || !dimensions) {
|
|
197
218
|
// No dimensions — flat row
|
|
198
219
|
leafNodes.forEach((n, i) => {
|
|
@@ -231,17 +252,18 @@ export function TreeGraph(
|
|
|
231
252
|
const cols = div1Ids.length || 1;
|
|
232
253
|
const rows = div2Ids.length || 1;
|
|
233
254
|
|
|
234
|
-
// Grid occupies the leaf area
|
|
235
|
-
const gridTop = leafY
|
|
255
|
+
// Grid occupies the full leaf area
|
|
256
|
+
const gridTop = leafY;
|
|
236
257
|
const gridBottom = height - padding.bottom;
|
|
237
258
|
const gridHeight = gridBottom - gridTop;
|
|
238
259
|
const colSpacing = usableWidth / (cols + 1);
|
|
239
260
|
const rowSpacingGrid = gridHeight / (rows + 1);
|
|
240
261
|
|
|
262
|
+
// Group leaves by their grid cell so we can spread co-located nodes.
|
|
263
|
+
const cellGroups = {};
|
|
241
264
|
leafNodes.forEach(n => {
|
|
242
265
|
const ld = leafDivisions[n.id];
|
|
243
266
|
if (!ld) {
|
|
244
|
-
// Unaffiliated leaf — place to the right
|
|
245
267
|
n.x = width - padding.right;
|
|
246
268
|
n.y = leafY;
|
|
247
269
|
return;
|
|
@@ -250,8 +272,27 @@ export function TreeGraph(
|
|
|
250
272
|
const rowDivId = ld[dim2Key];
|
|
251
273
|
const colIdx = div1Ids.indexOf(colDivId);
|
|
252
274
|
const rowIdx = div2Ids.indexOf(rowDivId);
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
296
|
});
|
|
256
297
|
}
|
|
257
298
|
|
|
@@ -270,20 +311,54 @@ export function TreeGraph(
|
|
|
270
311
|
.attr('aria-label', hide ? null : description)
|
|
271
312
|
.attr('style', 'max-width: 100%; height: auto; height: intrinsic;');
|
|
272
313
|
|
|
273
|
-
// Draw sibling links (dashed, behind parent-child links).
|
|
314
|
+
// Draw sibling links as arced paths (dashed, behind parent-child links).
|
|
315
|
+
// Regular siblings arc above (horizontal) or left (vertical).
|
|
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 = {};
|
|
319
|
+
const siblingArc = (d) => {
|
|
320
|
+
const s = nodeObjById[d.source];
|
|
321
|
+
const t = nodeObjById[d.target];
|
|
322
|
+
if (!s || !t) return '';
|
|
323
|
+
const sx = s.x, sy = s.y, tx = t.x, ty = t.y;
|
|
324
|
+
const dx = tx - sx, dy = ty - sy;
|
|
325
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
326
|
+
const isHorizontal = Math.abs(dx) > Math.abs(dy);
|
|
327
|
+
// Detect wrap-around: source is to the right of target (horizontal)
|
|
328
|
+
// or below target (vertical) — these are circular edges.
|
|
329
|
+
const isWrap = isHorizontal ? (sx > tx) : (sy > ty);
|
|
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);
|
|
338
|
+
const midX = (sx + tx) / 2;
|
|
339
|
+
const midY = (sy + ty) / 2;
|
|
340
|
+
let cx, cy;
|
|
341
|
+
if (isHorizontal) {
|
|
342
|
+
cx = midX;
|
|
343
|
+
cy = isWrap ? midY + arcAmount : midY - arcAmount;
|
|
344
|
+
} else {
|
|
345
|
+
cx = isWrap ? midX + arcAmount : midX - arcAmount;
|
|
346
|
+
cy = midY;
|
|
347
|
+
}
|
|
348
|
+
return `M ${sx} ${sy} Q ${cx} ${cy} ${tx} ${ty}`;
|
|
349
|
+
};
|
|
350
|
+
|
|
274
351
|
svg.append('g')
|
|
275
352
|
.attr('class', 'tree-sibling-links')
|
|
276
|
-
.selectAll('
|
|
353
|
+
.selectAll('path')
|
|
277
354
|
.data(siblingLinks)
|
|
278
|
-
.join('
|
|
279
|
-
.attr('
|
|
280
|
-
.attr('
|
|
281
|
-
.attr('
|
|
282
|
-
.attr('
|
|
283
|
-
.attr('stroke', '#bbb')
|
|
284
|
-
.attr('stroke-opacity', 0.25)
|
|
355
|
+
.join('path')
|
|
356
|
+
.attr('d', siblingArc)
|
|
357
|
+
.attr('fill', 'none')
|
|
358
|
+
.attr('stroke', '#888')
|
|
359
|
+
.attr('stroke-opacity', 0.6)
|
|
285
360
|
.attr('stroke-width', 1)
|
|
286
|
-
.attr('stroke-dasharray', '3
|
|
361
|
+
.attr('stroke-dasharray', '4,3');
|
|
287
362
|
|
|
288
363
|
// Draw parent-child links (solid).
|
|
289
364
|
svg.append('g')
|