@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@data-navigator/inspector",
3
- "version": "1.0.3",
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
+ }
@@ -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-inspector with modular D3 imports.
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('#svg' + nodeId);
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 = mode === 'tree'
160
- ? TreeGraph(
161
- { nodes: nodeArray, links: linkArray },
162
- { ...graphOptions, dimensions: structure.dimensions }
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 = 'svg' + c.__data__.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
+ }