@data-navigator/inspector 1.0.3 → 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 +4 -1
- package/style.css +295 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@data-navigator/inspector",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Frank Elavsky",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"module": "./src/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"src/**/*",
|
|
12
|
+
"style.css",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
@@ -16,7 +17,8 @@
|
|
|
16
17
|
".": {
|
|
17
18
|
"import": "./src/index.js",
|
|
18
19
|
"default": "./src/index.js"
|
|
19
|
-
}
|
|
20
|
+
},
|
|
21
|
+
"./style.css": "./style.css"
|
|
20
22
|
},
|
|
21
23
|
"keywords": [
|
|
22
24
|
"visualization",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console menu orchestrator.
|
|
3
|
+
* Creates the menu DOM, wires shared state to SVG highlighting and events.
|
|
4
|
+
*
|
|
5
|
+
* Layout (all collapsed by default):
|
|
6
|
+
* <details> Inspector Menu (wrapper)
|
|
7
|
+
* 1. Console (collapsed, at top, with clear button)
|
|
8
|
+
* 2. Rendered Elements (collapsed)
|
|
9
|
+
* - Nodes (grouped by dimension > division, then All Nodes)
|
|
10
|
+
* - Edges (grouped by nav rule, then All Edges)
|
|
11
|
+
* 3. Source Input (collapsed)
|
|
12
|
+
* - Data, Props, Dimensions, Divisions
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createMenuState } from './menu-state.js';
|
|
16
|
+
import { connectStateToSvg } from './svg-highlight.js';
|
|
17
|
+
import {
|
|
18
|
+
buildConsoleSection,
|
|
19
|
+
buildRenderedElementsSection,
|
|
20
|
+
buildSourceInputSection
|
|
21
|
+
} from './menu-sections.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create and mount the console menu.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} opts
|
|
27
|
+
* @param {Object} opts.structure - The data-navigator structure
|
|
28
|
+
* @param {SVGElement} opts.svgEl - The inspector SVG element
|
|
29
|
+
* @param {HTMLElement} opts.container - The root container (for event dispatching)
|
|
30
|
+
* @param {HTMLElement} opts.wrapperEl - The .dn-inspector-wrapper element
|
|
31
|
+
* @param {Object} opts.showConsoleMenu - The showConsoleMenu prop value
|
|
32
|
+
* @param {SVGCircleElement} opts.indicatorEl - The focus indicator circle
|
|
33
|
+
* @param {Object} opts.edgeSvgIdMap - edge key -> array of SVG element IDs
|
|
34
|
+
* @param {Function} opts.buildLabelFn - The buildLabel function for node labels
|
|
35
|
+
* @returns {{ state: Object, menuEl: HTMLElement, destroy: Function }}
|
|
36
|
+
*/
|
|
37
|
+
export function createConsoleMenu({
|
|
38
|
+
structure,
|
|
39
|
+
svgEl,
|
|
40
|
+
container,
|
|
41
|
+
wrapperEl,
|
|
42
|
+
showConsoleMenu,
|
|
43
|
+
indicatorEl,
|
|
44
|
+
edgeSvgIdMap,
|
|
45
|
+
buildLabelFn
|
|
46
|
+
}) {
|
|
47
|
+
const state = createMenuState();
|
|
48
|
+
|
|
49
|
+
// Outer wrapper: collapsible "Inspector Menu" section
|
|
50
|
+
const outerDetails = document.createElement('details');
|
|
51
|
+
outerDetails.className = 'dn-inspector-menu';
|
|
52
|
+
const outerSummary = document.createElement('summary');
|
|
53
|
+
outerSummary.className = 'dn-menu-summary dn-menu-summary-top';
|
|
54
|
+
outerSummary.textContent = 'Inspector Menu';
|
|
55
|
+
outerDetails.appendChild(outerSummary);
|
|
56
|
+
|
|
57
|
+
// 1. Console (collapsed, at top with clear button)
|
|
58
|
+
const { element: consoleEl, listEl: consoleListEl } = buildConsoleSection();
|
|
59
|
+
outerDetails.appendChild(consoleEl);
|
|
60
|
+
|
|
61
|
+
// 2. Rendered Elements (collapsed — Nodes grouped by dim/div, Edges grouped by nav rule)
|
|
62
|
+
const renderedEl = buildRenderedElementsSection(
|
|
63
|
+
structure, state, container, consoleListEl, buildLabelFn
|
|
64
|
+
);
|
|
65
|
+
outerDetails.appendChild(renderedEl);
|
|
66
|
+
|
|
67
|
+
// 3. Source Input (collapsed — Data, Props, Dimensions, Divisions)
|
|
68
|
+
const sourceInputEl = buildSourceInputSection(structure, showConsoleMenu);
|
|
69
|
+
outerDetails.appendChild(sourceInputEl);
|
|
70
|
+
|
|
71
|
+
// Mount
|
|
72
|
+
wrapperEl.appendChild(outerDetails);
|
|
73
|
+
|
|
74
|
+
// Wire state to SVG highlighting
|
|
75
|
+
const unsubSvg = connectStateToSvg(svgEl, state, edgeSvgIdMap, structure, indicatorEl);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
state,
|
|
79
|
+
menuEl: outerDetails,
|
|
80
|
+
destroy() {
|
|
81
|
+
unsubSvg();
|
|
82
|
+
wrapperEl.removeChild(outerDetails);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/force-graph.js
CHANGED
|
@@ -38,7 +38,8 @@ export function ForceGraph(
|
|
|
38
38
|
height = 400,
|
|
39
39
|
invalidation,
|
|
40
40
|
description,
|
|
41
|
-
hide
|
|
41
|
+
hide,
|
|
42
|
+
idPrefix = ''
|
|
42
43
|
} = {}
|
|
43
44
|
) {
|
|
44
45
|
// Compute values.
|
|
@@ -96,7 +97,8 @@ export function ForceGraph(
|
|
|
96
97
|
.attr('stroke-linecap', linkStrokeLinecap)
|
|
97
98
|
.selectAll('line')
|
|
98
99
|
.data(links)
|
|
99
|
-
.join('line')
|
|
100
|
+
.join('line')
|
|
101
|
+
.attr('id', (_, i) => idPrefix + 'svgedge' + LS[i] + '-' + LT[i]);
|
|
100
102
|
|
|
101
103
|
const node = svg
|
|
102
104
|
.append('g')
|
package/src/inspector.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ForceGraph } from './force-graph.js';
|
|
2
2
|
import { TreeGraph } from './tree-graph.js';
|
|
3
|
+
import { createConsoleMenu } from './console-menu.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Converts a structure's nodes or edges object into an array for D3.
|
|
@@ -86,8 +87,8 @@ const hideTooltip = (tooltipEl, indicatorEl) => {
|
|
|
86
87
|
/**
|
|
87
88
|
* Moves the SVG focus indicator circle to the target node.
|
|
88
89
|
*/
|
|
89
|
-
const highlightNode = (nodeId, svgEl, indicatorEl) => {
|
|
90
|
-
let target = svgEl.querySelector('#
|
|
90
|
+
const highlightNode = (nodeId, svgEl, indicatorEl, idPrefix) => {
|
|
91
|
+
let target = svgEl.querySelector('#' + idPrefix + nodeId);
|
|
91
92
|
if (!target || !indicatorEl) return;
|
|
92
93
|
indicatorEl.setAttribute('cx', target.getAttribute('cx'));
|
|
93
94
|
indicatorEl.setAttribute('cy', target.getAttribute('cy'));
|
|
@@ -125,8 +126,13 @@ const createFocusIndicator = (svgEl, id) => {
|
|
|
125
126
|
* @param {string[]} [options.edgeExclusions=[]] - Edge IDs to exclude from the graph
|
|
126
127
|
* @param {string[]} [options.nodeInclusions=[]] - Extra pseudo-node IDs to include
|
|
127
128
|
* @param {'force'|'tree'} [options.mode='force'] - Visualization mode: 'force' for force-directed, 'tree' for hierarchy layout
|
|
129
|
+
* @param {Object} [options.showConsoleMenu] - Optional console menu configuration
|
|
130
|
+
* @param {Array} options.showConsoleMenu.data - Required: the raw input dataset
|
|
131
|
+
* @param {Object} [options.showConsoleMenu.structure] - Optional: structure options for display
|
|
132
|
+
* @param {Object} [options.showConsoleMenu.input] - Optional: input options for display
|
|
133
|
+
* @param {Object} [options.showConsoleMenu.rendering] - Optional: rendering options for display
|
|
128
134
|
*
|
|
129
|
-
* @returns {{ svg: SVGElement, highlight: Function, clear: Function, destroy: Function }}
|
|
135
|
+
* @returns {{ svg: SVGElement, highlight: Function, clear: Function, destroy: Function, menuState?: Object }}
|
|
130
136
|
*/
|
|
131
137
|
export function Inspector({
|
|
132
138
|
structure,
|
|
@@ -136,7 +142,8 @@ export function Inspector({
|
|
|
136
142
|
nodeRadius = 5,
|
|
137
143
|
edgeExclusions = [],
|
|
138
144
|
nodeInclusions = [],
|
|
139
|
-
mode = 'force'
|
|
145
|
+
mode = 'force',
|
|
146
|
+
showConsoleMenu
|
|
140
147
|
}) {
|
|
141
148
|
const rootEl = typeof container === 'string' ? document.getElementById(container) : container;
|
|
142
149
|
const rootId = rootEl.id || 'dn-inspector-' + Math.random().toString(36).slice(2, 8);
|
|
@@ -147,13 +154,15 @@ export function Inspector({
|
|
|
147
154
|
const graphHeight = mode === 'tree' ? Math.round(size * 1.5) : size;
|
|
148
155
|
const nodeArray = convertToArray(structure.nodes, nodeInclusions);
|
|
149
156
|
const linkArray = convertToArray(structure.edges, [], edgeExclusions);
|
|
157
|
+
const idPrefix = rootId + '-';
|
|
150
158
|
const graphOptions = {
|
|
151
159
|
nodeId: d => d.id,
|
|
152
160
|
nodeGroup: d => (colorBy === 'dimensionLevel' ? d.dimensionLevel : d.data?.[colorBy]),
|
|
153
161
|
width: graphWidth,
|
|
154
162
|
height: graphHeight,
|
|
155
163
|
nodeRadius,
|
|
156
|
-
hide: true
|
|
164
|
+
hide: true,
|
|
165
|
+
idPrefix
|
|
157
166
|
};
|
|
158
167
|
|
|
159
168
|
const graph = mode === 'tree'
|
|
@@ -186,11 +195,12 @@ export function Inspector({
|
|
|
186
195
|
|
|
187
196
|
rootEl.appendChild(wrapperEl);
|
|
188
197
|
|
|
189
|
-
// Assign IDs to SVG circles for targeting
|
|
198
|
+
// Assign IDs to SVG circles for targeting (prefixed to avoid collisions)
|
|
190
199
|
const svgEl = graphContainer.querySelector('svg');
|
|
200
|
+
const nodeIdPrefix = idPrefix + 'svg';
|
|
191
201
|
graphContainer.querySelectorAll('circle').forEach(c => {
|
|
192
202
|
if (c.__data__?.id) {
|
|
193
|
-
c.id =
|
|
203
|
+
c.id = nodeIdPrefix + c.__data__.id;
|
|
194
204
|
}
|
|
195
205
|
c.addEventListener('mousemove', e => {
|
|
196
206
|
if (e.target?.__data__?.id) {
|
|
@@ -206,11 +216,46 @@ export function Inspector({
|
|
|
206
216
|
// Create focus indicator
|
|
207
217
|
const indicatorEl = createFocusIndicator(svgEl, rootId);
|
|
208
218
|
|
|
219
|
+
// Build console menu if requested
|
|
220
|
+
let menu = null;
|
|
221
|
+
if (showConsoleMenu) {
|
|
222
|
+
// Build edge-to-SVG-ID mapping by iterating all SVG edge elements
|
|
223
|
+
// and matching their IDs to structure edge keys (avoids fragile CSS selectors)
|
|
224
|
+
const edgeSvgIdMap = {};
|
|
225
|
+
const allSvgEdgeEls = svgEl.querySelectorAll('line[id], path[id]');
|
|
226
|
+
Object.keys(structure.edges).forEach(edgeKey => {
|
|
227
|
+
const edge = structure.edges[edgeKey];
|
|
228
|
+
const src = typeof edge.source === 'function' ? null : edge.source;
|
|
229
|
+
const tgt = typeof edge.target === 'function' ? null : edge.target;
|
|
230
|
+
if (src && tgt) {
|
|
231
|
+
const prefix = rootId + '-svgedge' + src + '-' + tgt;
|
|
232
|
+
const matched = [];
|
|
233
|
+
allSvgEdgeEls.forEach(el => {
|
|
234
|
+
if (el.id === prefix || el.id.startsWith(prefix + '-')) {
|
|
235
|
+
matched.push(el.id);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
edgeSvgIdMap[edgeKey] = matched;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
menu = createConsoleMenu({
|
|
243
|
+
structure,
|
|
244
|
+
svgEl,
|
|
245
|
+
container: rootEl,
|
|
246
|
+
wrapperEl,
|
|
247
|
+
showConsoleMenu,
|
|
248
|
+
indicatorEl,
|
|
249
|
+
edgeSvgIdMap,
|
|
250
|
+
buildLabelFn: (node) => buildLabel(node, colorBy)
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
209
254
|
// Public API
|
|
210
255
|
return {
|
|
211
256
|
svg: svgEl,
|
|
212
257
|
highlight(nodeId) {
|
|
213
|
-
highlightNode(nodeId, svgEl, indicatorEl);
|
|
258
|
+
highlightNode(nodeId, svgEl, indicatorEl, nodeIdPrefix);
|
|
214
259
|
if (structure.nodes[nodeId]) {
|
|
215
260
|
showTooltip(structure.nodes[nodeId], tooltipEl, graphWidth, graphHeight, colorBy);
|
|
216
261
|
}
|
|
@@ -219,7 +264,9 @@ export function Inspector({
|
|
|
219
264
|
hideTooltip(tooltipEl, indicatorEl);
|
|
220
265
|
},
|
|
221
266
|
destroy() {
|
|
267
|
+
if (menu) menu.destroy();
|
|
222
268
|
rootEl.removeChild(wrapperEl);
|
|
223
|
-
}
|
|
269
|
+
},
|
|
270
|
+
menuState: menu ? menu.state : undefined
|
|
224
271
|
};
|
|
225
272
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CustomEvent name constants and dispatch helper for the inspector console menu.
|
|
3
|
+
* All events are dispatched on the container element with `bubbles: true`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const EVENTS = {
|
|
7
|
+
ITEM_HOVER: 'dn-inspector:item-hover',
|
|
8
|
+
ITEM_UNHOVER: 'dn-inspector:item-unhover',
|
|
9
|
+
ITEM_CHECK: 'dn-inspector:item-check',
|
|
10
|
+
ITEM_UNCHECK: 'dn-inspector:item-uncheck',
|
|
11
|
+
ITEM_LOG: 'dn-inspector:item-log',
|
|
12
|
+
SELECTION_CHANGE: 'dn-inspector:selection-change'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dispatch a CustomEvent on the given container element.
|
|
17
|
+
* @param {HTMLElement} container
|
|
18
|
+
* @param {string} eventName - One of the EVENTS constants
|
|
19
|
+
* @param {Object} detail - Event detail payload
|
|
20
|
+
*/
|
|
21
|
+
export function dispatch(container, eventName, detail) {
|
|
22
|
+
container.dispatchEvent(new CustomEvent(eventName, {
|
|
23
|
+
bubbles: true,
|
|
24
|
+
detail
|
|
25
|
+
}));
|
|
26
|
+
}
|