@data-navigator/inspector 1.0.3 → 1.1.1

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
@@ -8,10 +8,7 @@ import { schemeTableau10 } from 'd3-scale-chromatic';
8
8
  import { create } from 'd3-selection';
9
9
 
10
10
  export function TreeGraph(
11
- {
12
- nodes,
13
- links
14
- },
11
+ { nodes, links },
15
12
  {
16
13
  nodeId = d => d.id,
17
14
  nodeGroup,
@@ -22,7 +19,8 @@ export function TreeGraph(
22
19
  height = 400,
23
20
  dimensions,
24
21
  description,
25
- hide
22
+ hide,
23
+ idPrefix = ''
26
24
  } = {}
27
25
  ) {
28
26
  // Compute values (same pattern as ForceGraph).
@@ -31,15 +29,19 @@ export function TreeGraph(
31
29
 
32
30
  // Build a lookup from node id to original node data.
33
31
  const nodeById = {};
34
- nodes.forEach((n, i) => { nodeById[N[i]] = n; });
32
+ nodes.forEach((n, i) => {
33
+ nodeById[N[i]] = n;
34
+ });
35
35
 
36
36
  // Replace input nodes with positioned objects.
37
37
  nodes = map(nodes, (_, i) => ({ id: N[i], x: 0, y: 0 }));
38
38
  const nodeObjById = {};
39
- nodes.forEach(n => { nodeObjById[n.id] = n; });
39
+ nodes.forEach(n => {
40
+ nodeObjById[n.id] = n;
41
+ });
40
42
 
41
43
  // Map links to source/target ids.
42
- const linkData = map(links, (l) => ({
44
+ const linkData = map(links, l => ({
43
45
  source: intern(l.source),
44
46
  target: intern(l.target),
45
47
  navigationRules: l.navigationRules || []
@@ -68,7 +70,9 @@ export function TreeGraph(
68
70
  // Map each sibling rule name to its pair for splitting merged edges.
69
71
  const ruleToPair = {};
70
72
  siblingRulePairs.forEach(pair => {
71
- pair.forEach(r => { ruleToPair[r] = pair.join(','); });
73
+ pair.forEach(r => {
74
+ ruleToPair[r] = pair.join(',');
75
+ });
72
76
  });
73
77
 
74
78
  // Classify links. Sibling links with merged rules (multiple pairs)
@@ -105,7 +109,10 @@ export function TreeGraph(
105
109
 
106
110
  nodes.forEach(n => {
107
111
  const orig = nodeById[n.id];
108
- if (!orig) { otherNodes.push(n); return; }
112
+ if (!orig) {
113
+ otherNodes.push(n);
114
+ return;
115
+ }
109
116
  const dl = orig.dimensionLevel;
110
117
  const dn = orig.derivedNode;
111
118
  if (dl === 0) level0.push(n);
@@ -139,7 +146,9 @@ export function TreeGraph(
139
146
 
140
147
  // Organize level2 nodes by their parent dimension.
141
148
  const divisionsByDim = {};
142
- dimOrder.forEach(key => { divisionsByDim[key] = []; });
149
+ dimOrder.forEach(key => {
150
+ divisionsByDim[key] = [];
151
+ });
143
152
  level2.forEach(n => {
144
153
  const orig = nodeById[n.id];
145
154
  if (orig && orig.derivedNode && divisionsByDim[orig.derivedNode]) {
@@ -160,9 +169,10 @@ export function TreeGraph(
160
169
  const upperSpacing = upperRows > 1 ? upperHeight / (upperRows - 1) : upperHeight;
161
170
  const leafGap = usableHeight * 0.05; // small gap between hierarchy and leaves
162
171
  const leafYPos = padding.top + upperHeight + leafGap;
172
+ const leafAreaCenter = (leafYPos + height - padding.bottom) / 2;
163
173
 
164
174
  let currentRow = 0;
165
- const yFor = (row) => padding.top + row * upperSpacing;
175
+ const yFor = row => padding.top + row * upperSpacing;
166
176
 
167
177
  // Level 0
168
178
  if (hasLevel0) {
@@ -215,13 +225,13 @@ export function TreeGraph(
215
225
  // Leaf nodes
216
226
  const leafY = leafYPos;
217
227
  if (dimOrder.length === 0 || !dimensions) {
218
- // No dimensions — flat row
228
+ // No dimensions — flat row, centered in leaf area
219
229
  leafNodes.forEach((n, i) => {
220
230
  n.x = padding.left + (usableWidth / (leafNodes.length + 1)) * (i + 1);
221
- n.y = leafY;
231
+ n.y = leafAreaCenter;
222
232
  });
223
233
  } else if (dimOrder.length === 1) {
224
- // Single dimension — spread leaves under their division parent
234
+ // Single dimension — spread leaves under their division parent, centered in leaf area
225
235
  const key = dimOrder[0];
226
236
  const dim = dimensions[key];
227
237
  const divIds = Object.keys(dim.divisions || {});
@@ -235,22 +245,30 @@ export function TreeGraph(
235
245
  const n = nodeObjById[leafId];
236
246
  if (n) {
237
247
  n.x = parentX - spreadWidth / 2 + leafSpacing * (i + 1);
238
- n.y = leafY;
248
+ n.y = leafAreaCenter;
239
249
  }
240
250
  });
241
251
  });
242
252
  } else {
243
253
  // 2+ dimensions — grid layout
244
- // First dimension controls columns, second controls rows
245
- const dim1Key = dimOrder[0];
246
- const dim2Key = dimOrder[1];
247
- const dim1 = dimensions[dim1Key];
248
- const dim2 = dimensions[dim2Key];
249
- const div1Ids = Object.keys(dim1.divisions || {});
250
- const div2Ids = Object.keys(dim2.divisions || {});
251
-
252
- const cols = div1Ids.length || 1;
253
- const rows = div2Ids.length || 1;
254
+ // Prefer horizontal spread: ensure cols >= rows by swapping dims if needed.
255
+ // This prevents single-column layouts (e.g. when dim1 is compressed to 1 division).
256
+ let dim1Key = dimOrder[0];
257
+ let dim2Key = dimOrder[1];
258
+ let dim1 = dimensions[dim1Key];
259
+ let dim2 = dimensions[dim2Key];
260
+ let div1Ids = Object.keys(dim1.divisions || {});
261
+ let div2Ids = Object.keys(dim2.divisions || {});
262
+
263
+ let cols = div1Ids.length || 1;
264
+ let rows = div2Ids.length || 1;
265
+
266
+ if (cols < rows) {
267
+ [dim1Key, dim2Key] = [dim2Key, dim1Key];
268
+ [dim1, dim2] = [dim2, dim1];
269
+ [div1Ids, div2Ids] = [div2Ids, div1Ids];
270
+ [cols, rows] = [rows, cols];
271
+ }
254
272
 
255
273
  // Grid occupies the full leaf area
256
274
  const gridTop = leafY;
@@ -298,7 +316,7 @@ export function TreeGraph(
298
316
 
299
317
  // Other nodes (exit, etc.) — placed bottom-right
300
318
  otherNodes.forEach((n, i) => {
301
- n.x = width - padding.right - (i * nodeRadius * 3);
319
+ n.x = width - padding.right - i * nodeRadius * 3;
302
320
  n.y = height - padding.bottom;
303
321
  });
304
322
 
@@ -316,17 +334,21 @@ export function TreeGraph(
316
334
  // Wrap-around (circular) siblings arc below/right with a larger curve.
317
335
  // When multiple edges share the same node pair, each gets a larger arc.
318
336
  const edgePairCounts = {};
319
- const siblingArc = (d) => {
337
+ const siblingArc = d => {
320
338
  const s = nodeObjById[d.source];
321
339
  const t = nodeObjById[d.target];
322
340
  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;
341
+ const sx = s.x,
342
+ sy = s.y,
343
+ tx = t.x,
344
+ ty = t.y;
345
+ const dx = tx - sx,
346
+ dy = ty - sy;
325
347
  const dist = Math.sqrt(dx * dx + dy * dy);
326
348
  const isHorizontal = Math.abs(dx) > Math.abs(dy);
327
349
  // Detect wrap-around: source is to the right of target (horizontal)
328
350
  // or below target (vertical) — these are circular edges.
329
- const isWrap = isHorizontal ? (sx > tx) : (sy > ty);
351
+ const isWrap = isHorizontal ? sx > tx : sy > ty;
330
352
  // Track regular and wrap arcs separately — they curve in opposite
331
353
  // directions so only same-direction arcs need stacking.
332
354
  const pairKey = [d.source, d.target].sort().join('::') + (isWrap ? '::wrap' : '::reg');
@@ -353,6 +375,7 @@ export function TreeGraph(
353
375
  .selectAll('path')
354
376
  .data(siblingLinks)
355
377
  .join('path')
378
+ .attr('id', d => idPrefix + 'svgedge' + d.source + '-' + d.target + '-' + (d.navigationRules?.[0] || ''))
356
379
  .attr('d', siblingArc)
357
380
  .attr('fill', 'none')
358
381
  .attr('stroke', '#888')
@@ -366,6 +389,7 @@ export function TreeGraph(
366
389
  .selectAll('line')
367
390
  .data(parentChildLinks)
368
391
  .join('line')
392
+ .attr('id', d => idPrefix + 'svgedge' + d.source + '-' + d.target)
369
393
  .attr('x1', d => nodeObjById[d.source]?.x || 0)
370
394
  .attr('y1', d => nodeObjById[d.source]?.y || 0)
371
395
  .attr('x2', d => nodeObjById[d.target]?.x || 0)
@@ -389,10 +413,11 @@ export function TreeGraph(
389
413
  .attr('r', nodeRadius)
390
414
  .attr('role', 'presentation');
391
415
 
392
- if (G) node.attr('fill', ({ id }) => {
393
- const idx = N.indexOf(id);
394
- return idx >= 0 ? color(G[idx]) : '#999';
395
- });
416
+ if (G)
417
+ node.attr('fill', ({ id }) => {
418
+ const idx = N.indexOf(id);
419
+ return idx >= 0 ? color(G[idx]) : '#999';
420
+ });
396
421
 
397
422
  function intern(value) {
398
423
  return value !== null && typeof value === 'object' ? value.valueOf() : value;
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
+ }