@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/package.json +4 -2
- package/src/console-menu.js +85 -0
- package/src/force-graph.js +4 -2
- package/src/inspector.js +56 -9
- package/src/menu-events.js +26 -0
- package/src/menu-sections.js +751 -0
- package/src/menu-state.js +89 -0
- package/src/svg-highlight.js +457 -0
- package/src/tree-graph.js +63 -14
- package/style.css +295 -0
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
|
-
|
|
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
|
|
142
|
-
const upperHeight = usableHeight * 0.
|
|
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
|
|
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
|
|
238
|
-
const gridTop = leafY
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
//
|
|
291
|
-
|
|
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
|
+
}
|