@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tree-graph.js +59 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-navigator/inspector",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "author": "Frank Elavsky",
6
6
  "license": "MIT",
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
- siblingLinks.push(l);
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 40% of height, leaves get 60%
142
- const upperHeight = usableHeight * 0.4;
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 leafYPos = padding.top + usableHeight * 0.85;
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 but may expand vertically
238
- const gridTop = leafY - (leafY - yFor(currentRow - 1)) * 0.15;
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
- n.x = padding.left + colSpacing * ((colIdx >= 0 ? colIdx : 0) + 1);
257
- n.y = gridTop + rowSpacingGrid * ((rowIdx >= 0 ? rowIdx : 0) + 1);
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
- // Arc offset scales with distance but stays subtle
291
- const arcAmount = Math.min(dist * 0.3, 40);
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;