@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/package.json +4 -2
- package/src/console-menu.js +79 -0
- package/src/force-graph.js +5 -3
- package/src/inspector.js +63 -18
- package/src/menu-events.js +28 -0
- package/src/menu-sections.js +938 -0
- package/src/menu-state.js +89 -0
- package/src/svg-highlight.js +479 -0
- package/src/tree-graph.js +60 -35
- package/style.css +295 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@data-navigator/inspector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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,79 @@
|
|
|
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 { buildConsoleSection, buildRenderedElementsSection, buildSourceInputSection } from './menu-sections.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create and mount the console menu.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} opts
|
|
23
|
+
* @param {Object} opts.structure - The data-navigator structure
|
|
24
|
+
* @param {SVGElement} opts.svgEl - The inspector SVG element
|
|
25
|
+
* @param {HTMLElement} opts.container - The root container (for event dispatching)
|
|
26
|
+
* @param {HTMLElement} opts.wrapperEl - The .dn-inspector-wrapper element
|
|
27
|
+
* @param {Object} opts.showConsoleMenu - The showConsoleMenu prop value
|
|
28
|
+
* @param {SVGCircleElement} opts.indicatorEl - The focus indicator circle
|
|
29
|
+
* @param {Object} opts.edgeSvgIdMap - edge key -> array of SVG element IDs
|
|
30
|
+
* @param {Function} opts.buildLabelFn - The buildLabel function for node labels
|
|
31
|
+
* @returns {{ state: Object, menuEl: HTMLElement, destroy: Function }}
|
|
32
|
+
*/
|
|
33
|
+
export function createConsoleMenu({
|
|
34
|
+
structure,
|
|
35
|
+
svgEl,
|
|
36
|
+
container,
|
|
37
|
+
wrapperEl,
|
|
38
|
+
showConsoleMenu,
|
|
39
|
+
indicatorEl,
|
|
40
|
+
edgeSvgIdMap,
|
|
41
|
+
buildLabelFn
|
|
42
|
+
}) {
|
|
43
|
+
const state = createMenuState();
|
|
44
|
+
|
|
45
|
+
// Outer wrapper: collapsible "Inspector Menu" section
|
|
46
|
+
const outerDetails = document.createElement('details');
|
|
47
|
+
outerDetails.className = 'dn-inspector-menu';
|
|
48
|
+
const outerSummary = document.createElement('summary');
|
|
49
|
+
outerSummary.className = 'dn-menu-summary dn-menu-summary-top';
|
|
50
|
+
outerSummary.textContent = 'Inspector Menu';
|
|
51
|
+
outerDetails.appendChild(outerSummary);
|
|
52
|
+
|
|
53
|
+
// 1. Console (collapsed, at top with clear button)
|
|
54
|
+
const { element: consoleEl, listEl: consoleListEl } = buildConsoleSection();
|
|
55
|
+
outerDetails.appendChild(consoleEl);
|
|
56
|
+
|
|
57
|
+
// 2. Rendered Elements (collapsed — Nodes grouped by dim/div, Edges grouped by nav rule)
|
|
58
|
+
const renderedEl = buildRenderedElementsSection(structure, state, container, consoleListEl, buildLabelFn);
|
|
59
|
+
outerDetails.appendChild(renderedEl);
|
|
60
|
+
|
|
61
|
+
// 3. Source Input (collapsed — Data, Props, Dimensions, Divisions)
|
|
62
|
+
const sourceInputEl = buildSourceInputSection(structure, showConsoleMenu);
|
|
63
|
+
outerDetails.appendChild(sourceInputEl);
|
|
64
|
+
|
|
65
|
+
// Mount
|
|
66
|
+
wrapperEl.appendChild(outerDetails);
|
|
67
|
+
|
|
68
|
+
// Wire state to SVG highlighting
|
|
69
|
+
const unsubSvg = connectStateToSvg(svgEl, state, edgeSvgIdMap, structure, indicatorEl);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
state,
|
|
73
|
+
menuEl: outerDetails,
|
|
74
|
+
destroy() {
|
|
75
|
+
unsubSvg();
|
|
76
|
+
wrapperEl.removeChild(outerDetails);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
package/src/force-graph.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright 2021-2024 Observable, Inc.
|
|
2
2
|
// Released under the ISC license.
|
|
3
3
|
// https://observablehq.com/@d3/force-directed-graph
|
|
4
|
-
// Adapted for data-navigator
|
|
4
|
+
// Adapted for @data-navigator/inspector with modular D3 imports.
|
|
5
5
|
|
|
6
6
|
import { map, sort } from 'd3-array';
|
|
7
7
|
import { drag } from 'd3-drag';
|
|
@@ -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.
|
|
@@ -70,6 +71,9 @@ const showTooltip = (node, tooltipEl, graphWidth, graphHeight, colorBy) => {
|
|
|
70
71
|
const bbox = tooltipEl.getBoundingClientRect();
|
|
71
72
|
const yOffset = bbox.height / 2;
|
|
72
73
|
tooltipEl.style.textAlign = 'left';
|
|
74
|
+
tooltipEl.style.position = 'absolute';
|
|
75
|
+
tooltipEl.style.left = 0;
|
|
76
|
+
tooltipEl.style.top = 0;
|
|
73
77
|
tooltipEl.style.transform = `translate(${graphWidth}px,${graphHeight / 2 - yOffset}px)`;
|
|
74
78
|
};
|
|
75
79
|
|
|
@@ -86,8 +90,8 @@ const hideTooltip = (tooltipEl, indicatorEl) => {
|
|
|
86
90
|
/**
|
|
87
91
|
* Moves the SVG focus indicator circle to the target node.
|
|
88
92
|
*/
|
|
89
|
-
const highlightNode = (nodeId, svgEl, indicatorEl) => {
|
|
90
|
-
let target = svgEl.querySelector('#
|
|
93
|
+
const highlightNode = (nodeId, svgEl, indicatorEl, idPrefix) => {
|
|
94
|
+
let target = svgEl.querySelector('#' + idPrefix + nodeId);
|
|
91
95
|
if (!target || !indicatorEl) return;
|
|
92
96
|
indicatorEl.setAttribute('cx', target.getAttribute('cx'));
|
|
93
97
|
indicatorEl.setAttribute('cy', target.getAttribute('cy'));
|
|
@@ -125,8 +129,13 @@ const createFocusIndicator = (svgEl, id) => {
|
|
|
125
129
|
* @param {string[]} [options.edgeExclusions=[]] - Edge IDs to exclude from the graph
|
|
126
130
|
* @param {string[]} [options.nodeInclusions=[]] - Extra pseudo-node IDs to include
|
|
127
131
|
* @param {'force'|'tree'} [options.mode='force'] - Visualization mode: 'force' for force-directed, 'tree' for hierarchy layout
|
|
132
|
+
* @param {Object} [options.showConsoleMenu] - Optional console menu configuration
|
|
133
|
+
* @param {Array} options.showConsoleMenu.data - Required: the raw input dataset
|
|
134
|
+
* @param {Object} [options.showConsoleMenu.structure] - Optional: structure options for display
|
|
135
|
+
* @param {Object} [options.showConsoleMenu.input] - Optional: input options for display
|
|
136
|
+
* @param {Object} [options.showConsoleMenu.rendering] - Optional: rendering options for display
|
|
128
137
|
*
|
|
129
|
-
* @returns {{ svg: SVGElement, highlight: Function, clear: Function, destroy: Function }}
|
|
138
|
+
* @returns {{ svg: SVGElement, highlight: Function, clear: Function, destroy: Function, menuState?: Object }}
|
|
130
139
|
*/
|
|
131
140
|
export function Inspector({
|
|
132
141
|
structure,
|
|
@@ -136,7 +145,8 @@ export function Inspector({
|
|
|
136
145
|
nodeRadius = 5,
|
|
137
146
|
edgeExclusions = [],
|
|
138
147
|
nodeInclusions = [],
|
|
139
|
-
mode = 'force'
|
|
148
|
+
mode = 'force',
|
|
149
|
+
showConsoleMenu
|
|
140
150
|
}) {
|
|
141
151
|
const rootEl = typeof container === 'string' ? document.getElementById(container) : container;
|
|
142
152
|
const rootId = rootEl.id || 'dn-inspector-' + Math.random().toString(36).slice(2, 8);
|
|
@@ -147,24 +157,21 @@ export function Inspector({
|
|
|
147
157
|
const graphHeight = mode === 'tree' ? Math.round(size * 1.5) : size;
|
|
148
158
|
const nodeArray = convertToArray(structure.nodes, nodeInclusions);
|
|
149
159
|
const linkArray = convertToArray(structure.edges, [], edgeExclusions);
|
|
160
|
+
const idPrefix = rootId + '-';
|
|
150
161
|
const graphOptions = {
|
|
151
162
|
nodeId: d => d.id,
|
|
152
163
|
nodeGroup: d => (colorBy === 'dimensionLevel' ? d.dimensionLevel : d.data?.[colorBy]),
|
|
153
164
|
width: graphWidth,
|
|
154
165
|
height: graphHeight,
|
|
155
166
|
nodeRadius,
|
|
156
|
-
hide: true
|
|
167
|
+
hide: true,
|
|
168
|
+
idPrefix
|
|
157
169
|
};
|
|
158
170
|
|
|
159
|
-
const graph =
|
|
160
|
-
|
|
161
|
-
{ nodes: nodeArray, links: linkArray },
|
|
162
|
-
{
|
|
163
|
-
)
|
|
164
|
-
: ForceGraph(
|
|
165
|
-
{ nodes: nodeArray, links: linkArray },
|
|
166
|
-
graphOptions
|
|
167
|
-
);
|
|
171
|
+
const graph =
|
|
172
|
+
mode === 'tree'
|
|
173
|
+
? TreeGraph({ nodes: nodeArray, links: linkArray }, { ...graphOptions, dimensions: structure.dimensions })
|
|
174
|
+
: ForceGraph({ nodes: nodeArray, links: linkArray }, graphOptions);
|
|
168
175
|
|
|
169
176
|
// Create wrapper structure
|
|
170
177
|
const wrapperEl = document.createElement('div');
|
|
@@ -186,11 +193,12 @@ export function Inspector({
|
|
|
186
193
|
|
|
187
194
|
rootEl.appendChild(wrapperEl);
|
|
188
195
|
|
|
189
|
-
// Assign IDs to SVG circles for targeting
|
|
196
|
+
// Assign IDs to SVG circles for targeting (prefixed to avoid collisions)
|
|
190
197
|
const svgEl = graphContainer.querySelector('svg');
|
|
198
|
+
const nodeIdPrefix = idPrefix + 'svg';
|
|
191
199
|
graphContainer.querySelectorAll('circle').forEach(c => {
|
|
192
200
|
if (c.__data__?.id) {
|
|
193
|
-
c.id =
|
|
201
|
+
c.id = nodeIdPrefix + c.__data__.id;
|
|
194
202
|
}
|
|
195
203
|
c.addEventListener('mousemove', e => {
|
|
196
204
|
if (e.target?.__data__?.id) {
|
|
@@ -206,11 +214,46 @@ export function Inspector({
|
|
|
206
214
|
// Create focus indicator
|
|
207
215
|
const indicatorEl = createFocusIndicator(svgEl, rootId);
|
|
208
216
|
|
|
217
|
+
// Build console menu if requested
|
|
218
|
+
let menu = null;
|
|
219
|
+
if (showConsoleMenu) {
|
|
220
|
+
// Build edge-to-SVG-ID mapping by iterating all SVG edge elements
|
|
221
|
+
// and matching their IDs to structure edge keys (avoids fragile CSS selectors)
|
|
222
|
+
const edgeSvgIdMap = {};
|
|
223
|
+
const allSvgEdgeEls = svgEl.querySelectorAll('line[id], path[id]');
|
|
224
|
+
Object.keys(structure.edges).forEach(edgeKey => {
|
|
225
|
+
const edge = structure.edges[edgeKey];
|
|
226
|
+
const src = typeof edge.source === 'function' ? null : edge.source;
|
|
227
|
+
const tgt = typeof edge.target === 'function' ? null : edge.target;
|
|
228
|
+
if (src && tgt) {
|
|
229
|
+
const prefix = rootId + '-svgedge' + src + '-' + tgt;
|
|
230
|
+
const matched = [];
|
|
231
|
+
allSvgEdgeEls.forEach(el => {
|
|
232
|
+
if (el.id === prefix || el.id.startsWith(prefix + '-')) {
|
|
233
|
+
matched.push(el.id);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
edgeSvgIdMap[edgeKey] = matched;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
menu = createConsoleMenu({
|
|
241
|
+
structure,
|
|
242
|
+
svgEl,
|
|
243
|
+
container: rootEl,
|
|
244
|
+
wrapperEl,
|
|
245
|
+
showConsoleMenu,
|
|
246
|
+
indicatorEl,
|
|
247
|
+
edgeSvgIdMap,
|
|
248
|
+
buildLabelFn: node => buildLabel(node, colorBy)
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
209
252
|
// Public API
|
|
210
253
|
return {
|
|
211
254
|
svg: svgEl,
|
|
212
255
|
highlight(nodeId) {
|
|
213
|
-
highlightNode(nodeId, svgEl, indicatorEl);
|
|
256
|
+
highlightNode(nodeId, svgEl, indicatorEl, nodeIdPrefix);
|
|
214
257
|
if (structure.nodes[nodeId]) {
|
|
215
258
|
showTooltip(structure.nodes[nodeId], tooltipEl, graphWidth, graphHeight, colorBy);
|
|
216
259
|
}
|
|
@@ -219,7 +262,9 @@ export function Inspector({
|
|
|
219
262
|
hideTooltip(tooltipEl, indicatorEl);
|
|
220
263
|
},
|
|
221
264
|
destroy() {
|
|
265
|
+
if (menu) menu.destroy();
|
|
222
266
|
rootEl.removeChild(wrapperEl);
|
|
223
|
-
}
|
|
267
|
+
},
|
|
268
|
+
menuState: menu ? menu.state : undefined
|
|
224
269
|
};
|
|
225
270
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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(
|
|
23
|
+
new CustomEvent(eventName, {
|
|
24
|
+
bubbles: true,
|
|
25
|
+
detail
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
}
|