@data-navigator/inspector 1.0.2 → 1.1.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/src/tree-graph.js CHANGED
@@ -22,7 +22,8 @@ export function TreeGraph(
22
22
  height = 400,
23
23
  dimensions,
24
24
  description,
25
- hide
25
+ hide,
26
+ idPrefix = ''
26
27
  } = {}
27
28
  ) {
28
29
  // Compute values (same pattern as ForceGraph).
@@ -55,15 +56,24 @@ export function TreeGraph(
55
56
  if (G && nodeGroups === undefined) nodeGroups = sort(G);
56
57
  const color = nodeGroup == null ? null : scaleOrdinal(nodeGroups, colors);
57
58
 
58
- // Build set of parent-child navigation rule names.
59
+ // Build set of parent-child and sibling navigation rule names.
59
60
  const parentChildRules = new Set(['parent', 'child']);
61
+ const siblingRulePairs = [['left', 'right']]; // default pair
60
62
  if (dimensions) {
61
63
  Object.values(dimensions).forEach(dim => {
62
64
  (dim.navigationRules?.parent_child || []).forEach(r => parentChildRules.add(r));
65
+ const ss = dim.navigationRules?.sibling_sibling;
66
+ if (ss && ss.length === 2) siblingRulePairs.push(ss);
63
67
  });
64
68
  }
69
+ // Map each sibling rule name to its pair for splitting merged edges.
70
+ const ruleToPair = {};
71
+ siblingRulePairs.forEach(pair => {
72
+ pair.forEach(r => { ruleToPair[r] = pair.join(','); });
73
+ });
65
74
 
66
- // Classify links.
75
+ // Classify links. Sibling links with merged rules (multiple pairs)
76
+ // are split into one entry per distinct pair so each gets its own arc.
67
77
  const parentChildLinks = [];
68
78
  const siblingLinks = [];
69
79
  linkData.forEach(l => {
@@ -75,7 +85,15 @@ export function TreeGraph(
75
85
  if (isParentChild) {
76
86
  parentChildLinks.push(l);
77
87
  } else {
78
- siblingLinks.push(l);
88
+ // Split by distinct sibling rule pairs.
89
+ const seenPairs = new Set();
90
+ rules.forEach(r => {
91
+ const pairKey = ruleToPair[r] || r;
92
+ if (!seenPairs.has(pairKey)) {
93
+ seenPairs.add(pairKey);
94
+ siblingLinks.push({ source: l.source, target: l.target, navigationRules: [r] });
95
+ }
96
+ });
79
97
  }
80
98
  });
81
99
 
@@ -138,10 +156,11 @@ export function TreeGraph(
138
156
 
139
157
  const hasLevel0 = level0.length > 0;
140
158
  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;
159
+ // Upper hierarchy gets 25% of height, leaves get the rest after a gap
160
+ const upperHeight = usableHeight * 0.25;
143
161
  const upperSpacing = upperRows > 1 ? upperHeight / (upperRows - 1) : upperHeight;
144
- const leafYPos = padding.top + usableHeight * 0.85;
162
+ const leafGap = usableHeight * 0.05; // small gap between hierarchy and leaves
163
+ const leafYPos = padding.top + upperHeight + leafGap;
145
164
 
146
165
  let currentRow = 0;
147
166
  const yFor = (row) => padding.top + row * upperSpacing;
@@ -234,17 +253,18 @@ export function TreeGraph(
234
253
  const cols = div1Ids.length || 1;
235
254
  const rows = div2Ids.length || 1;
236
255
 
237
- // Grid occupies the leaf area but may expand vertically
238
- const gridTop = leafY - (leafY - yFor(currentRow - 1)) * 0.15;
256
+ // Grid occupies the full leaf area
257
+ const gridTop = leafY;
239
258
  const gridBottom = height - padding.bottom;
240
259
  const gridHeight = gridBottom - gridTop;
241
260
  const colSpacing = usableWidth / (cols + 1);
242
261
  const rowSpacingGrid = gridHeight / (rows + 1);
243
262
 
263
+ // Group leaves by their grid cell so we can spread co-located nodes.
264
+ const cellGroups = {};
244
265
  leafNodes.forEach(n => {
245
266
  const ld = leafDivisions[n.id];
246
267
  if (!ld) {
247
- // Unaffiliated leaf — place to the right
248
268
  n.x = width - padding.right;
249
269
  n.y = leafY;
250
270
  return;
@@ -253,8 +273,27 @@ export function TreeGraph(
253
273
  const rowDivId = ld[dim2Key];
254
274
  const colIdx = div1Ids.indexOf(colDivId);
255
275
  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);
276
+ const cellKey = `${colIdx},${rowIdx}`;
277
+ if (!cellGroups[cellKey]) cellGroups[cellKey] = { colIdx, rowIdx, nodes: [] };
278
+ cellGroups[cellKey].nodes.push(n);
279
+ });
280
+ // Position each group, spreading nodes within shared cells.
281
+ const cellPad = Math.min(colSpacing, rowSpacingGrid) * 0.6;
282
+ Object.values(cellGroups).forEach(({ colIdx, rowIdx, nodes: cellNodes }) => {
283
+ const cx = padding.left + colSpacing * (colIdx + 1);
284
+ const cy = gridTop + rowSpacingGrid * (rowIdx + 1);
285
+ if (cellNodes.length === 1) {
286
+ cellNodes[0].x = cx;
287
+ cellNodes[0].y = cy;
288
+ } else {
289
+ // Spread vertically within the cell (matches row-based dim2)
290
+ const spread = cellPad;
291
+ const step = spread / (cellNodes.length - 1);
292
+ cellNodes.forEach((n, i) => {
293
+ n.x = cx;
294
+ n.y = cy - spread / 2 + step * i;
295
+ });
296
+ }
258
297
  });
259
298
  }
260
299
 
@@ -276,6 +315,8 @@ export function TreeGraph(
276
315
  // Draw sibling links as arced paths (dashed, behind parent-child links).
277
316
  // Regular siblings arc above (horizontal) or left (vertical).
278
317
  // Wrap-around (circular) siblings arc below/right with a larger curve.
318
+ // When multiple edges share the same node pair, each gets a larger arc.
319
+ const edgePairCounts = {};
279
320
  const siblingArc = (d) => {
280
321
  const s = nodeObjById[d.source];
281
322
  const t = nodeObjById[d.target];
@@ -287,8 +328,14 @@ export function TreeGraph(
287
328
  // Detect wrap-around: source is to the right of target (horizontal)
288
329
  // or below target (vertical) — these are circular edges.
289
330
  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);
331
+ // Track regular and wrap arcs separately they curve in opposite
332
+ // directions so only same-direction arcs need stacking.
333
+ const pairKey = [d.source, d.target].sort().join('::') + (isWrap ? '::wrap' : '::reg');
334
+ const edgeIndex = edgePairCounts[pairKey] || 0;
335
+ edgePairCounts[pairKey] = edgeIndex + 1;
336
+ // Arc offset scales with distance; increase by 20% for each additional same-direction edge
337
+ const arcFraction = 0.3 + edgeIndex * 0.2;
338
+ const arcAmount = Math.min(dist * arcFraction, 40 + edgeIndex * 25);
292
339
  const midX = (sx + tx) / 2;
293
340
  const midY = (sy + ty) / 2;
294
341
  let cx, cy;
@@ -307,6 +354,7 @@ export function TreeGraph(
307
354
  .selectAll('path')
308
355
  .data(siblingLinks)
309
356
  .join('path')
357
+ .attr('id', d => idPrefix + 'svgedge' + d.source + '-' + d.target + '-' + (d.navigationRules?.[0] || ''))
310
358
  .attr('d', siblingArc)
311
359
  .attr('fill', 'none')
312
360
  .attr('stroke', '#888')
@@ -320,6 +368,7 @@ export function TreeGraph(
320
368
  .selectAll('line')
321
369
  .data(parentChildLinks)
322
370
  .join('line')
371
+ .attr('id', d => idPrefix + 'svgedge' + d.source + '-' + d.target)
323
372
  .attr('x1', d => nodeObjById[d.source]?.x || 0)
324
373
  .attr('y1', d => nodeObjById[d.source]?.y || 0)
325
374
  .attr('x2', d => nodeObjById[d.target]?.x || 0)
package/style.css ADDED
@@ -0,0 +1,295 @@
1
+ /* Inspector default styles */
2
+
3
+ .dn-inspector-wrapper {
4
+ position: relative;
5
+ }
6
+
7
+ .dn-inspector-hidden {
8
+ display: none;
9
+ }
10
+
11
+ .dn-inspector-tooltip {
12
+ position: absolute;
13
+ padding: 10px;
14
+ background-color: white;
15
+ border: 1px solid black;
16
+ width: 200px;
17
+ top: 0;
18
+ left: 0;
19
+ font-size: 0.85em;
20
+ pointer-events: none;
21
+ z-index: 10;
22
+ }
23
+
24
+ .dn-inspector-focus-indicator {
25
+ pointer-events: none;
26
+ }
27
+
28
+ .dn-inspector-node {
29
+ position: absolute;
30
+ padding: 0px;
31
+ margin: 0px;
32
+ overflow: visible;
33
+ pointer-events: none;
34
+ background: transparent;
35
+ border: none;
36
+ }
37
+
38
+ .dn-inspector-node:focus {
39
+ outline: 2px solid #1e3369;
40
+ }
41
+
42
+ /* Dynamic edges drawn on-demand for virtual/excluded edges */
43
+ .dn-inspector-dynamic-edge {
44
+ pointer-events: none;
45
+ }
46
+
47
+ /* ==== Console Menu ====
48
+ *
49
+ * All selectors below use `.dn-inspector-menu` as a prefix for two reasons:
50
+ * 1. Scoping — these styles only affect the inspector menu
51
+ * 2. Specificity — overrides global UI framework styles (e.g. VitePress sets
52
+ * `summary { min-height: 44px }` and `button { min-width: 44px }`)
53
+ */
54
+
55
+ .dn-inspector-menu {
56
+ min-width: 500px;
57
+ max-height: 450px;
58
+ overflow-y: auto;
59
+ border: 1px solid #ccc;
60
+ font-size: 12px;
61
+ font-family: monospace;
62
+ line-height: 1.3;
63
+ padding: 2px;
64
+ margin-top: 4px;
65
+ background: #fff;
66
+ }
67
+
68
+ /* Hidden utility */
69
+ .dn-inspector-menu .dn-menu-hidden {
70
+ display: none !important;
71
+ }
72
+
73
+ /* ---- Caret summaries ---- */
74
+
75
+ .dn-inspector-menu summary {
76
+ cursor: pointer;
77
+ user-select: none;
78
+ list-style: none;
79
+ display: flex;
80
+ align-items: center;
81
+ min-height: 24px;
82
+ padding: 0 2px;
83
+ border: none;
84
+ border-radius: 0;
85
+ }
86
+
87
+ .dn-inspector-menu summary::-webkit-details-marker {
88
+ display: none;
89
+ }
90
+
91
+ .dn-inspector-menu summary:focus {
92
+ outline: 2px solid #1e3369;
93
+ outline-offset: 0;
94
+ }
95
+
96
+ /* Group checkbox inside summary */
97
+ .dn-inspector-menu .dn-menu-group-checkbox {
98
+ margin: 0 2px 0 0;
99
+ flex-shrink: 0;
100
+ width: 13px;
101
+ height: 13px;
102
+ min-width: 13px;
103
+ min-height: 13px;
104
+ padding: 0;
105
+ }
106
+
107
+ /* Caret indicator */
108
+ .dn-inspector-menu summary::before {
109
+ content: '\25B8'; /* right-pointing triangle */
110
+ display: inline-flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ width: 24px;
114
+ min-width: 24px;
115
+ height: 24px;
116
+ font-size: 10px;
117
+ flex-shrink: 0;
118
+ }
119
+
120
+ .dn-inspector-menu details[open] > summary::before {
121
+ content: '\25BE'; /* down-pointing triangle */
122
+ }
123
+
124
+ /* Top-level section summaries */
125
+ .dn-inspector-menu .dn-menu-summary-top {
126
+ font-weight: bold;
127
+ font-size: 12px;
128
+ }
129
+
130
+ /* Nested indentation: each details level indents its contents */
131
+ .dn-inspector-menu details details {
132
+ padding-left: 12px;
133
+ }
134
+
135
+ .dn-inspector-menu details details details {
136
+ padding-left: 12px;
137
+ }
138
+
139
+ /* ---- Menu items (checkbox + label + log btn) ---- */
140
+
141
+ .dn-inspector-menu .dn-menu-item {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 3px;
145
+ padding: 0 2px 0 6px;
146
+ min-height: 20px;
147
+ cursor: default;
148
+ margin-left: 32px;
149
+ }
150
+
151
+ .dn-inspector-menu .dn-menu-item:hover {
152
+ background: #f0f0f0;
153
+ }
154
+
155
+ .dn-inspector-menu .dn-menu-item input[type="checkbox"] {
156
+ margin: 0;
157
+ flex-shrink: 0;
158
+ width: 13px;
159
+ height: 13px;
160
+ min-width: 13px;
161
+ min-height: 13px;
162
+ padding: 0;
163
+ }
164
+
165
+ .dn-inspector-menu .dn-menu-item-label {
166
+ flex: 1;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ font-size: 11px;
171
+ }
172
+
173
+ .dn-inspector-menu .dn-menu-log-btn {
174
+ font-size: 10px;
175
+ font-family: monospace;
176
+ padding: 0 4px;
177
+ line-height: 16px;
178
+ min-width: auto;
179
+ min-height: auto;
180
+ cursor: pointer;
181
+ border: 1px solid #bbb;
182
+ background: #f5f5f5;
183
+ border-radius: 2px;
184
+ flex-shrink: 0;
185
+ color: #555;
186
+ }
187
+
188
+ .dn-inspector-menu .dn-menu-log-btn:hover {
189
+ background: #e0e0e0;
190
+ color: #222;
191
+ }
192
+
193
+ .dn-inspector-menu .dn-menu-log-btn:focus {
194
+ outline: 2px solid #1e3369;
195
+ outline-offset: 0;
196
+ }
197
+
198
+ /* Console clear button */
199
+ .dn-inspector-menu .dn-menu-clear-btn {
200
+ margin-left: auto;
201
+ }
202
+
203
+ /* ---- Informational items (no checkbox) ---- */
204
+
205
+ .dn-inspector-menu .dn-menu-info {
206
+ padding: 0 2px 0 30px;
207
+ font-size: 11px;
208
+ min-height: 18px;
209
+ display: flex;
210
+ align-items: center;
211
+ }
212
+
213
+ .dn-inspector-menu .dn-menu-empty {
214
+ color: #999;
215
+ font-style: italic;
216
+ }
217
+
218
+ /* ---- Data / Props pre blocks ---- */
219
+
220
+ .dn-inspector-menu .dn-menu-pre {
221
+ max-height: 150px;
222
+ overflow: auto;
223
+ font-size: 10px;
224
+ margin: 2px 0 2px 30px;
225
+ padding: 3px;
226
+ background: #fafafa;
227
+ border: 1px solid #eee;
228
+ white-space: pre-wrap;
229
+ word-break: break-word;
230
+ line-height: 1.25;
231
+ }
232
+
233
+ /* ---- Console section ---- */
234
+
235
+ .dn-inspector-menu .dn-menu-console-list {
236
+ max-height: 180px;
237
+ overflow-y: auto;
238
+ }
239
+
240
+ .dn-inspector-menu .dn-menu-console-list:empty::after {
241
+ content: "(empty)";
242
+ color: #999;
243
+ padding: 2px 30px;
244
+ display: block;
245
+ font-style: italic;
246
+ font-size: 11px;
247
+ }
248
+
249
+ .dn-inspector-menu .dn-menu-console-entry {
250
+ border-bottom: 1px solid #f0f0f0;
251
+ }
252
+
253
+ /* ---- Inline expandable arrays ---- */
254
+
255
+ .dn-inspector-menu .dn-menu-inline-array {
256
+ font-size: 11px;
257
+ padding: 0 2px;
258
+ min-height: 18px;
259
+ display: flex;
260
+ align-items: baseline;
261
+ flex-wrap: wrap;
262
+ }
263
+
264
+ .dn-inspector-menu .dn-menu-array-toggle {
265
+ color: #666;
266
+ cursor: pointer;
267
+ flex-shrink: 0;
268
+ }
269
+
270
+ .dn-inspector-menu .dn-menu-array-collapsed {
271
+ color: #08c;
272
+ cursor: pointer;
273
+ text-decoration: underline;
274
+ text-decoration-style: dotted;
275
+ }
276
+
277
+ .dn-inspector-menu .dn-menu-array-collapsed:hover {
278
+ color: #005fa3;
279
+ }
280
+
281
+ .dn-inspector-menu .dn-menu-array-expanded {
282
+ color: #333;
283
+ }
284
+
285
+ .dn-inspector-menu .dn-menu-array-chip {
286
+ color: #08c;
287
+ cursor: default;
288
+ padding: 0 1px;
289
+ border-radius: 2px;
290
+ }
291
+
292
+ .dn-inspector-menu .dn-menu-array-chip:hover {
293
+ background: #e8f4fd;
294
+ color: #005fa3;
295
+ }