@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-navigator/inspector",
3
- "version": "1.0.2",
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
+ }
@@ -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('#svg' + nodeId);
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 = 'svg' + c.__data__.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
+ }