@peter.prib/renderengine 0.14.0 → 0.15.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.
Files changed (113) hide show
  1. package/README.md +45 -19
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49641 -39747
  4. package/dist/renderengine.umd.js +25366 -15650
  5. package/documentation/REFERENCE.md +206 -0
  6. package/examples/decision-workbench.html +26 -0
  7. package/package.json +7 -1
  8. package/src/CanvasRenderer.js +225 -200
  9. package/src/SVGRenderer.js +476 -454
  10. package/src/archimate.js +22 -2
  11. package/src/archimateArchiFormat.js +1735 -1701
  12. package/src/archimateArchive.js +36 -7
  13. package/src/archimateCSV.js +93 -37
  14. package/src/archimateDrawio.js +2 -0
  15. package/src/archimateExchangeFormat.js +3 -1
  16. package/src/chartCatalog.js +219 -0
  17. package/src/charting/Annotations.js +169 -0
  18. package/src/charting/ArchimateDiagramEditor.js +54 -10
  19. package/src/charting/Axis.js +217 -5
  20. package/src/charting/Chart.js +99 -9
  21. package/src/charting/Chart3D.js +3 -0
  22. package/src/charting/ChartBox.js +8 -1
  23. package/src/charting/ColorPalette.js +42 -4
  24. package/src/charting/DiagramEditor.js +174 -39
  25. package/src/charting/DiagramEditorCommands.js +28 -0
  26. package/src/charting/Edge.js +7 -0
  27. package/src/charting/Hierarchy.js +114 -0
  28. package/src/charting/InteractionManager.js +4 -0
  29. package/src/charting/ModelFileFormat.js +5 -1
  30. package/src/charting/SettingsManager.js +35 -0
  31. package/src/charting/ShapePaths.js +82 -10
  32. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  33. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  34. package/src/charting/charts/AlluvialDiagram.js +311 -0
  35. package/src/charting/charts/AreaChart.js +52 -15
  36. package/src/charting/charts/BarChart.js +38 -3
  37. package/src/charting/charts/BarChart3D.js +5 -3
  38. package/src/charting/charts/BarChartBase.js +26 -6
  39. package/src/charting/charts/BeeswarmChart.js +164 -0
  40. package/src/charting/charts/BoxPlotChart.js +23 -1
  41. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  42. package/src/charting/charts/CandlestickChart.js +172 -4
  43. package/src/charting/charts/ChordDiagram.js +105 -2
  44. package/src/charting/charts/CirclePackChart.js +81 -0
  45. package/src/charting/charts/ComboChart.js +233 -0
  46. package/src/charting/charts/ContourChart.js +310 -0
  47. package/src/charting/charts/DonutChart3D.js +13 -0
  48. package/src/charting/charts/DumbbellChart.js +217 -0
  49. package/src/charting/charts/EcdfChart.js +150 -0
  50. package/src/charting/charts/FunnelChart.js +4 -1
  51. package/src/charting/charts/GeoMapChart.js +132 -11
  52. package/src/charting/charts/GroupedBarChart.js +44 -3
  53. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  54. package/src/charting/charts/HeatmapChart.js +2 -2
  55. package/src/charting/charts/HexbinChart.js +137 -0
  56. package/src/charting/charts/HierarchyChart.js +77 -3
  57. package/src/charting/charts/HorizonChart.js +105 -0
  58. package/src/charting/charts/KagiChart.js +167 -0
  59. package/src/charting/charts/LineChart.js +74 -18
  60. package/src/charting/charts/OhlcChart.js +79 -0
  61. package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
  62. package/src/charting/charts/PieChart.js +195 -17
  63. package/src/charting/charts/PieChart3D.js +137 -0
  64. package/src/charting/charts/PointAndFigureChart.js +198 -0
  65. package/src/charting/charts/PolarAreaChart.js +189 -0
  66. package/src/charting/charts/RadarChart.js +7 -1
  67. package/src/charting/charts/RadialBarChart.js +5 -2
  68. package/src/charting/charts/RenkoChart.js +178 -0
  69. package/src/charting/charts/RidgelineChart.js +113 -0
  70. package/src/charting/charts/SankeyDiagram.js +127 -4
  71. package/src/charting/charts/ScatterChart.js +13 -0
  72. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  73. package/src/charting/charts/SlopegraphChart.js +205 -0
  74. package/src/charting/charts/SparklineTableChart.js +166 -0
  75. package/src/charting/charts/StackedBarChart.js +48 -5
  76. package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
  77. package/src/charting/charts/StreamGraphChart.js +10 -3
  78. package/src/charting/charts/SunburstChart.js +27 -7
  79. package/src/charting/charts/SurfaceChart3D.js +157 -0
  80. package/src/charting/charts/TreemapChart.js +28 -8
  81. package/src/charting/charts/ViolinChart.js +18 -0
  82. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  83. package/src/charting/datamodel/DataLineage.js +150 -0
  84. package/src/charting/datamodel/DataModel.js +83 -1
  85. package/src/charting/datamodel/ModelMapping.js +87 -0
  86. package/src/charting/datamodel/SqlDdl.js +110 -85
  87. package/src/charting/datamodel/index.js +3 -0
  88. package/src/charting/decision/DecisionWorkbench.js +295 -0
  89. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  90. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  91. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  92. package/src/charting/diagrams/SketchView.js +121 -0
  93. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  94. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  95. package/src/charting/modelTree/NavigatorModel.js +410 -0
  96. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  97. package/src/charting/pivot/PivotChart.js +109 -4
  98. package/src/charting/pivot/PivotGridChart.js +321 -33
  99. package/src/charting/pivot/PivotView.js +82 -0
  100. package/src/charting/report/TemplatedReport.js +547 -0
  101. package/src/charting/templates/BoxTemplate.js +28 -27
  102. package/src/charting/templates/PackageTemplate.js +26 -25
  103. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  104. package/src/charting/templates/Template.js +219 -212
  105. package/src/cli.js +146 -0
  106. package/src/index.js +3370 -2995
  107. package/src/workspace/Workspace.js +126 -7
  108. package/src/workspace/archimateServices.js +4 -0
  109. package/src/workspace/index.js +3 -0
  110. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  111. package/src/workspace/parts/DecisionWorkbenchPart.js +58 -0
  112. package/src/workspace/parts/ViewsPart.js +196 -0
  113. package/src/workspace/styles.js +36 -1
@@ -0,0 +1,453 @@
1
+ import { NavigatorModel } from './NavigatorModel.js';
2
+ import { typeTitle } from '../archimate/archimateNames.js';
3
+ import { treeLayerOf } from './ModelTreePanel.js';
4
+ import { ArchimateNotation } from '../../archimate.js';
5
+
6
+ /**
7
+ * NavigatorPanel — interactive, multi-step relationship visual explorer panel.
8
+ *
9
+ * Provides step-by-step traversal along incoming and outgoing relationships,
10
+ * breadcrumbs / history navigation (back/forward), pinning, direction filters,
11
+ * and view navigation.
12
+ */
13
+ export class NavigatorPanel {
14
+ /**
15
+ * @param {Document} doc
16
+ * @param {Object} options
17
+ * @param {ArchimateModel} options.model
18
+ * @param {NavigatorModel} [options.navigatorModel]
19
+ * @param {string} [options.elementId]
20
+ * @param {(item: { kind: string, id: string }) => void} [options.onSelectItem]
21
+ * @param {(viewId: string) => void} [options.onSelectView]
22
+ */
23
+ constructor(doc, { model, navigatorModel = null, elementId = null, onSelectItem = () => {}, onSelectView = () => {} } = {}) {
24
+ this.doc = doc;
25
+ this.model = model;
26
+ this.nav = navigatorModel || new NavigatorModel(model, { rootId: elementId });
27
+ if (elementId && !this.nav.currentId) {
28
+ this.nav.focus(elementId);
29
+ }
30
+ this.onSelectItem = onSelectItem;
31
+ this.onSelectView = onSelectView;
32
+
33
+ this.element = this._createDOM();
34
+ this._unsubscribe = this.nav.onChange(() => this.render());
35
+ }
36
+
37
+ destroy() {
38
+ this._unsubscribe?.();
39
+ }
40
+
41
+ /** Set or update the focused element (respects navigator pinning). */
42
+ setElementId(elementId) {
43
+ if (!elementId) return;
44
+ this.nav.focus(elementId);
45
+ }
46
+
47
+ /** The container DOM element. */
48
+ get dom() {
49
+ return this.element;
50
+ }
51
+
52
+ _createDOM() {
53
+ const root = this.doc.createElement('div');
54
+ root.className = 're-navigator-panel';
55
+ root.style.cssText = `
56
+ display: flex;
57
+ flex-direction: column;
58
+ height: 100%;
59
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
60
+ font-size: 12px;
61
+ color: #333;
62
+ background: #fff;
63
+ overflow: hidden;
64
+ user-select: none;
65
+ `;
66
+
67
+ this.toolbar = this._buildToolbar();
68
+ root.appendChild(this.toolbar);
69
+
70
+ this.content = this.doc.createElement('div');
71
+ this.content.style.cssText = `
72
+ flex: 1;
73
+ overflow-y: auto;
74
+ min-height: 0;
75
+ padding: 8px;
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 8px;
79
+ `;
80
+ root.appendChild(this.content);
81
+
82
+ this.render();
83
+ return root;
84
+ }
85
+
86
+ _buildToolbar() {
87
+ const bar = this.doc.createElement('div');
88
+ bar.style.cssText = `
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 4px;
92
+ padding: 6px 8px;
93
+ background: #f7f7f8;
94
+ border-bottom: 1px solid #e1e4e8;
95
+ flex-shrink: 0;
96
+ `;
97
+
98
+ const btn = (icon, title, onClick) => {
99
+ const b = this.doc.createElement('button');
100
+ b.type = 'button';
101
+ b.title = title;
102
+ b.textContent = icon;
103
+ b.style.cssText = `
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ width: 24px;
108
+ height: 24px;
109
+ padding: 0;
110
+ border: 1px solid #d1d5da;
111
+ border-radius: 3px;
112
+ background: #fff;
113
+ cursor: pointer;
114
+ font-size: 12px;
115
+ color: #444;
116
+ `;
117
+ b.addEventListener('click', onClick);
118
+ b.addEventListener('mouseenter', () => { if (!b.disabled) b.style.background = '#eef1f5'; });
119
+ b.addEventListener('mouseleave', () => { if (!b.disabled) b.style.background = b.dataset.active ? '#e1e4e8' : '#fff'; });
120
+ return b;
121
+ };
122
+
123
+ this.btnBack = btn('◀', 'Navigate back', () => this.nav.back());
124
+ this.btnForward = btn('▶', 'Navigate forward', () => this.nav.forward());
125
+ this.btnPin = btn('📌', 'Pin element (prevents auto-changing root)', () => {
126
+ this.nav.togglePin();
127
+ this._updateToolbarState();
128
+ });
129
+
130
+ // Direction selector
131
+ const dirSelect = this.doc.createElement('select');
132
+ dirSelect.style.cssText = `
133
+ font-size: 11px;
134
+ padding: 2px 4px;
135
+ border: 1px solid #d1d5da;
136
+ border-radius: 3px;
137
+ background: #fff;
138
+ color: #333;
139
+ margin-left: auto;
140
+ `;
141
+ const dirs = [
142
+ ['both', '⇄ Both'],
143
+ ['outgoing', '→ Outgoing'],
144
+ ['incoming', '← Incoming'],
145
+ ];
146
+ for (const [val, label] of dirs) {
147
+ const opt = this.doc.createElement('option');
148
+ opt.value = val;
149
+ opt.textContent = label;
150
+ dirSelect.appendChild(opt);
151
+ }
152
+ dirSelect.value = this.nav.direction;
153
+ dirSelect.addEventListener('change', () => this.nav.setDirection(dirSelect.value));
154
+ this.dirSelect = dirSelect;
155
+
156
+ bar.appendChild(this.btnBack);
157
+ bar.appendChild(this.btnForward);
158
+ bar.appendChild(this.btnPin);
159
+ bar.appendChild(this.dirSelect);
160
+
161
+ this._updateToolbarState();
162
+ return bar;
163
+ }
164
+
165
+ _updateToolbarState() {
166
+ if (!this.btnBack) return;
167
+ this.btnBack.disabled = !this.nav.canBack;
168
+ this.btnBack.style.opacity = this.nav.canBack ? '1' : '0.4';
169
+ this.btnBack.style.cursor = this.nav.canBack ? 'pointer' : 'default';
170
+
171
+ this.btnForward.disabled = !this.nav.canForward;
172
+ this.btnForward.style.opacity = this.nav.canForward ? '1' : '0.4';
173
+ this.btnForward.style.cursor = this.nav.canForward ? 'pointer' : 'default';
174
+
175
+ this.btnPin.dataset.active = this.nav.pinned ? 'true' : '';
176
+ this.btnPin.style.background = this.nav.pinned ? '#e1e4e8' : '#fff';
177
+ this.btnPin.style.borderColor = this.nav.pinned ? '#888' : '#d1d5da';
178
+ }
179
+
180
+ render() {
181
+ this._updateToolbarState();
182
+ if (!this.content) return;
183
+ this.content.replaceChildren();
184
+
185
+ const tree = this.nav.buildTree();
186
+ if (!tree) {
187
+ const empty = this.doc.createElement('div');
188
+ empty.style.cssText = 'color: #888; font-style: italic; padding: 12px; text-align: center;';
189
+ empty.textContent = '(Select an element to explore connections)';
190
+ this.content.appendChild(empty);
191
+ return;
192
+ }
193
+
194
+ // Focused element card
195
+ this.content.appendChild(this._renderRootCard(tree));
196
+
197
+ // Navigation tree container
198
+ const treeSection = this.doc.createElement('div');
199
+ treeSection.style.cssText = 'display: flex; flex-direction: column; gap: 4px;';
200
+
201
+ const hasConnections = tree.outgoing.length > 0 || tree.incoming.length > 0;
202
+ if (!hasConnections) {
203
+ const noRel = this.doc.createElement('div');
204
+ noRel.style.cssText = 'color: #888; font-size: 11px; padding: 4px 6px;';
205
+ noRel.textContent = '(No relationships found in selected direction)';
206
+ treeSection.appendChild(noRel);
207
+ } else {
208
+ if (tree.outgoing.length > 0) {
209
+ treeSection.appendChild(this._renderSectionHeader(`Outgoing (${tree.outgoing.length})`, '→'));
210
+ for (const branch of tree.outgoing) {
211
+ treeSection.appendChild(this._renderBranch(branch));
212
+ }
213
+ }
214
+
215
+ if (tree.incoming.length > 0) {
216
+ treeSection.appendChild(this._renderSectionHeader(`Incoming (${tree.incoming.length})`, '←'));
217
+ for (const branch of tree.incoming) {
218
+ treeSection.appendChild(this._renderBranch(branch));
219
+ }
220
+ }
221
+ }
222
+
223
+ this.content.appendChild(treeSection);
224
+ }
225
+
226
+ _renderSectionHeader(title, arrow) {
227
+ const h = this.doc.createElement('div');
228
+ h.style.cssText = `
229
+ font-weight: 600;
230
+ font-size: 11px;
231
+ color: #586069;
232
+ text-transform: uppercase;
233
+ letter-spacing: 0.5px;
234
+ padding: 4px 0 2px 2px;
235
+ margin-top: 6px;
236
+ `;
237
+ h.textContent = `${arrow} ${title}`;
238
+ return h;
239
+ }
240
+
241
+ _renderRootCard(tree) {
242
+ const card = this.doc.createElement('div');
243
+ card.style.cssText = `
244
+ background: #f6f8fa;
245
+ border: 1px solid #e1e4e8;
246
+ border-radius: 4px;
247
+ padding: 8px;
248
+ display: flex;
249
+ flex-direction: column;
250
+ gap: 4px;
251
+ `;
252
+
253
+ const titleRow = this.doc.createElement('div');
254
+ titleRow.style.cssText = 'display: flex; align-items: center; justify-content: space-between;';
255
+
256
+ const nameSpan = this.doc.createElement('span');
257
+ nameSpan.style.cssText = 'font-weight: 600; font-size: 13px; color: #24292e;';
258
+ nameSpan.textContent = tree.element.name || '(unnamed)';
259
+ titleRow.appendChild(nameSpan);
260
+
261
+ card.appendChild(titleRow);
262
+
263
+ const metaRow = this.doc.createElement('div');
264
+ metaRow.style.cssText = 'font-size: 11px; color: #586069; display: flex; gap: 8px;';
265
+ const typeSpan = this.doc.createElement('span');
266
+ typeSpan.textContent = typeTitle(tree.element.type);
267
+ const layerSpan = this.doc.createElement('span');
268
+ layerSpan.textContent = `• ${typeTitle(tree.layer)}`;
269
+ metaRow.appendChild(typeSpan);
270
+ metaRow.appendChild(layerSpan);
271
+ card.appendChild(metaRow);
272
+
273
+ // Views list
274
+ if (tree.views && tree.views.length > 0) {
275
+ const viewsDiv = this.doc.createElement('div');
276
+ viewsDiv.style.cssText = 'margin-top: 4px; padding-top: 4px; border-top: 1px dashed #e1e4e8; font-size: 11px;';
277
+ const vHeader = this.doc.createElement('div');
278
+ vHeader.style.cssText = 'color: #586069; margin-bottom: 2px;';
279
+ vHeader.textContent = `In views (${tree.views.length}):`;
280
+ viewsDiv.appendChild(vHeader);
281
+
282
+ for (const view of tree.views) {
283
+ const vLink = this.doc.createElement('span');
284
+ vLink.style.cssText = `
285
+ display: inline-block;
286
+ margin: 2px 4px 2px 0;
287
+ padding: 1px 6px;
288
+ background: #eef1f5;
289
+ border-radius: 3px;
290
+ color: #0366d6;
291
+ cursor: pointer;
292
+ `;
293
+ vLink.textContent = view.name || 'View';
294
+ vLink.addEventListener('click', () => {
295
+ this.onSelectView(view.id);
296
+ this.onSelectItem({ kind: 'element', id: tree.element.id });
297
+ });
298
+ viewsDiv.appendChild(vLink);
299
+ }
300
+ card.appendChild(viewsDiv);
301
+ }
302
+
303
+ return card;
304
+ }
305
+
306
+ _renderBranch(branch, level = 0) {
307
+ const wrap = this.doc.createElement('div');
308
+ wrap.style.cssText = `
309
+ display: flex;
310
+ flex-direction: column;
311
+ margin-left: ${level * 14}px;
312
+ border-left: ${level > 0 ? '1px dashed #d1d5da' : 'none'};
313
+ padding-left: ${level > 0 ? '6px' : '0'};
314
+ margin-bottom: 2px;
315
+ `;
316
+
317
+ const row = this.doc.createElement('div');
318
+ row.style.cssText = `
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 4px;
322
+ padding: 3px 6px;
323
+ border-radius: 3px;
324
+ cursor: pointer;
325
+ transition: background 0.1s;
326
+ `;
327
+
328
+ // Expand / collapse chevron (if branch has potential further connections and not cycle)
329
+ const toggle = this.doc.createElement('span');
330
+ toggle.style.cssText = `
331
+ width: 14px;
332
+ height: 14px;
333
+ display: inline-flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ font-size: 9px;
337
+ color: #6a737d;
338
+ cursor: pointer;
339
+ flex-shrink: 0;
340
+ `;
341
+
342
+ if (branch.cycle) {
343
+ toggle.textContent = '↻';
344
+ toggle.title = 'Cycle detected (element already in path)';
345
+ toggle.style.color = '#e36209';
346
+ } else {
347
+ toggle.textContent = branch.expanded ? '▼' : '▶';
348
+ toggle.addEventListener('click', (e) => {
349
+ e.stopPropagation();
350
+ this.nav.toggleExpand(branch.pathKey);
351
+ });
352
+ }
353
+ row.appendChild(toggle);
354
+
355
+ // Relationship badge
356
+ const relBadge = this.doc.createElement('span');
357
+ relBadge.style.cssText = `
358
+ padding: 1px 4px;
359
+ background: #f1f3f5;
360
+ border: 1px solid #e1e4e8;
361
+ border-radius: 3px;
362
+ font-size: 10px;
363
+ color: #495057;
364
+ flex-shrink: 0;
365
+ `;
366
+ const dirSymbol = branch.direction === 'outgoing' ? '→' : '←';
367
+ relBadge.textContent = `${dirSymbol} ${typeTitle(branch.relationship.kind)}`;
368
+ relBadge.addEventListener('click', (e) => {
369
+ e.stopPropagation();
370
+ this.onSelectItem({ kind: 'relationship', id: branch.relationship.id });
371
+ });
372
+ row.appendChild(relBadge);
373
+
374
+ // Element name and layer
375
+ const elSpan = this.doc.createElement('span');
376
+ elSpan.style.cssText = `
377
+ font-size: 11px;
378
+ color: #24292e;
379
+ white-space: nowrap;
380
+ overflow: hidden;
381
+ text-overflow: ellipsis;
382
+ flex: 1;
383
+ `;
384
+ elSpan.textContent = branch.element.name || '(unnamed)';
385
+ elSpan.title = `${branch.element.name} (${typeTitle(branch.element.type)} - ${typeTitle(branch.layer)})`;
386
+ row.appendChild(elSpan);
387
+
388
+ // Focus jump button (⊙)
389
+ const focusBtn = this.doc.createElement('span');
390
+ focusBtn.textContent = '⊙';
391
+ focusBtn.title = 'Re-center navigator on this element';
392
+ focusBtn.style.cssText = `
393
+ color: #0366d6;
394
+ font-size: 12px;
395
+ opacity: 0.6;
396
+ padding: 0 4px;
397
+ cursor: pointer;
398
+ flex-shrink: 0;
399
+ `;
400
+ focusBtn.addEventListener('click', (e) => {
401
+ e.stopPropagation();
402
+ this.nav.focus(branch.element.id, { force: true });
403
+ });
404
+ row.appendChild(focusBtn);
405
+
406
+ row.addEventListener('click', () => {
407
+ this.onSelectItem({ kind: 'element', id: branch.element.id });
408
+ });
409
+
410
+ row.addEventListener('mouseenter', () => {
411
+ row.style.background = '#f1f3f5';
412
+ focusBtn.style.opacity = '1';
413
+ });
414
+ row.addEventListener('mouseleave', () => {
415
+ row.style.background = '';
416
+ focusBtn.style.opacity = '0.6';
417
+ });
418
+
419
+ wrap.appendChild(row);
420
+
421
+ // Render recursive children if expanded
422
+ if (branch.expanded && branch.children) {
423
+ const childWrap = this.doc.createElement('div');
424
+ childWrap.style.cssText = 'display: flex; flex-direction: column; gap: 2px;';
425
+
426
+ const sub = branch.children;
427
+ const hasKids = sub.outgoing.length > 0 || sub.incoming.length > 0;
428
+ if (!hasKids) {
429
+ const noMore = this.doc.createElement('div');
430
+ noMore.style.cssText = `margin-left: ${(level + 1) * 14}px; font-size: 10px; color: #888; font-style: italic;`;
431
+ noMore.textContent = '(end of path)';
432
+ childWrap.appendChild(noMore);
433
+ } else {
434
+ for (const outK of sub.outgoing) {
435
+ childWrap.appendChild(this._renderBranch(outK, level + 1));
436
+ }
437
+ for (const inK of sub.incoming) {
438
+ childWrap.appendChild(this._renderBranch(inK, level + 1));
439
+ }
440
+ }
441
+ wrap.appendChild(childWrap);
442
+ }
443
+
444
+ return wrap;
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Functional helper to build a NavigatorPanel DOM element.
450
+ */
451
+ export function buildNavigatorPanel(doc, options) {
452
+ return new NavigatorPanel(doc, options).dom;
453
+ }
@@ -15,9 +15,10 @@ export class PivotChart {
15
15
  * @param {string} [options.type] a catalog key to prefer while it fits (else ChartChooser's best)
16
16
  * @param {ChartChooser} [options.chooser]
17
17
  */
18
- constructor(view, { type = null, chooser = null } = {}) {
18
+ constructor(view, { type = null, chooser = null, facetBy = null } = {}) {
19
19
  this.view = view;
20
20
  this.type = type;
21
+ this.facetBy = facetBy;
21
22
  this.chooser = chooser ?? new ChartChooser();
22
23
  }
23
24
 
@@ -115,6 +116,12 @@ export class PivotChart {
115
116
  out.push(add('populationPyramid', `${numbers[0].name} vs ${numbers[1].name} for each ${category.name}`,
116
117
  rows.map(r => ({ label: String(r[category.name]), left: r[numbers[0].name], right: r[numbers[1].name] })),
117
118
  { leftLabel: numbers[0].name, rightLabel: numbers[1].name, showValues: true }));
119
+ out.push(add('dumbbell', `${numbers[0].name} to ${numbers[1].name} gap for each ${category.name}`,
120
+ rows.map(r => ({ label: String(r[category.name]), start: r[numbers[0].name], end: r[numbers[1].name] })),
121
+ { startLabel: numbers[0].name, endLabel: numbers[1].name, showDelta: true }));
122
+ out.push(add('slopegraph', `${names} trajectory across ${category.name}`,
123
+ rows.map(r => ({ label: String(r[category.name]), values: numbers.map(n => r[n.name]) })),
124
+ { stageLabels: numbers.map(n => n.name) }));
118
125
  const flowLinks = rows.flatMap(r => numbers
119
126
  .filter(n => r[n.name] > 0)
120
127
  .map(n => ({ source: String(r[category.name]), target: n.name, value: r[n.name] })));
@@ -128,6 +135,8 @@ export class PivotChart {
128
135
  out.push(add('boxPlot', `distribution of ${category.name} values for each of ${names}`, distData));
129
136
  out.push(add('density', `density curve of ${category.name} values for each of ${names}`, distData));
130
137
  out.push(add('violin', `violin distribution of ${category.name} values for each of ${names}`, distData));
138
+ out.push(add('ridgeline', `ridgeline joy plot of ${category.name} values for each of ${names}`, distData));
139
+ out.push(add('beeswarm', `beeswarm raincloud plot of ${category.name} values for each of ${names}`, distData));
131
140
  }
132
141
  out.push(add('scatter', `${numbers[0].name} against ${numbers[1].name} for each ${category.name}`,
133
142
  rows.map(r => ({ x: r[numbers[0].name], y: r[numbers[1].name], label: String(r[category.name]) }))));
@@ -144,6 +153,10 @@ export class PivotChart {
144
153
  if (allNonNeg) {
145
154
  out.push(add('bar3DOrbit', `${names} for each ${category.name}, in 3D`,
146
155
  rows.flatMap(r => numbers.map(n => ({ x: String(r[category.name]), z: n.name, value: r[n.name] })))));
156
+ if (rows.length >= 2 && numbers.length >= 2) {
157
+ out.push(add('surface3D', `3D surface mesh of ${names} across ${category.name}`,
158
+ rows.flatMap(r => numbers.map(n => ({ x: String(r[category.name]), z: n.name, value: r[n.name] })))));
159
+ }
147
160
  }
148
161
  if (additiveNonNeg) {
149
162
  out.push(add('stackedBar3DOrbit', `${names} stacked for each ${category.name}, in 3D`,
@@ -161,13 +174,19 @@ export class PivotChart {
161
174
  if (includeBase) {
162
175
  out.push(add('bar', `total ${number.name} for each ${category.name}`, points));
163
176
  if (nonNeg && complete && number.measure === 'additive' && points.length <= 8) {
164
- out.push(add('pie', `share of total ${number.name} across ${category.name}`, points));
165
- out.push(add('donut', `share of total ${number.name} across ${category.name}, as a ring`, points));
177
+ out.push(add('pie', `share of total ${number.name} across ${category.name}`, points, { exploded: 'max', hoverExplode: true }));
178
+ out.push(add('donut', `share of total ${number.name} across ${category.name}, as a ring`, points, { exploded: 'max', hoverExplode: true, showTotal: true }));
179
+ out.push(add('pie3D', `share of total ${number.name} across ${category.name}, in 3D`, points, { exploded: 'max', hoverExplode: true }));
180
+ out.push(add('donut3D', `share of total ${number.name} across ${category.name}, as a 3D ring`, points, { exploded: 'max', hoverExplode: true }));
166
181
  }
182
+ } else if (nonNeg && complete && number.measure === 'additive' && points.length <= 8) {
183
+ out.push(add('pie3D', `share of total ${number.name} across ${category.name}, in 3D`, points, { exploded: 'max', hoverExplode: true }));
184
+ out.push(add('donut3D', `share of total ${number.name} across ${category.name}, as a 3D ring`, points, { exploded: 'max', hoverExplode: true }));
167
185
  }
168
186
  if (hierarchy) {
169
187
  out.push(add('treemap', `${number.name} across the ${category.name} hierarchy`, hierarchy.tree));
170
188
  out.push(add('sunburst', `${number.name} across the ${category.name} hierarchy, in rings`, hierarchy.tree));
189
+ out.push(add('circlePack', `${number.name} across the ${category.name} hierarchy, as packed circles`, hierarchy.tree));
171
190
  out.push(add('icicle', `${number.name} across the ${category.name} hierarchy, as an icicle partition`, hierarchy.tree));
172
191
  out.push(add('outline', `${number.name} across the ${category.name} hierarchy, as an outline`, hierarchy.tree));
173
192
  }
@@ -187,6 +206,7 @@ export class PivotChart {
187
206
  if (nonNeg && points.length <= 12) {
188
207
  out.push(add('funnel', `${number.name} across ${category.name} stages`, points));
189
208
  out.push(add('radialBar', `${number.name} for each ${category.name}, around a circle`, points));
209
+ out.push(add('polarArea', `${number.name} for each ${category.name}, as a 3D Nightingale Rose`, points, { extrude3D: true, exploded: 'max', hoverExplode: true }));
190
210
  if (!includeBase && points.length >= 3) {
191
211
  out.push(add('radar', `${number.name} across ${category.name} axes`, points.map(d => ({ axis: d.label, value: d.value }))));
192
212
  }
@@ -214,6 +234,77 @@ export class PivotChart {
214
234
  return this.chosen();
215
235
  }
216
236
 
237
+ /** Sets or clears the facet dimension for small-multiples grids. */
238
+ setFacetBy(dimension) {
239
+ this.facetBy = dimension;
240
+ this.redraw?.();
241
+ return this.facetBy;
242
+ }
243
+
244
+ /** Dimensions available to facet small-multiples charts by. */
245
+ facetChoices() {
246
+ const pool = [...(this.view.pageAxis?.map(p => p.dimension) ?? []), ...this.view.rowAxis, ...this.view.columnAxis];
247
+ return Array.from(new Set(pool));
248
+ }
249
+
250
+ /**
251
+ * Splits chart data into small-multiple facets by `facetDimension`, calculating unified global scale bounds.
252
+ * @param {string} [facetDimension]
253
+ * @returns {{ facetDimension: string, facets: Array<object>, globalMax: number, globalMin: number }|null}
254
+ */
255
+ facetData(facetDimension = this.facetBy) {
256
+ if (!facetDimension) return null;
257
+ const chartData = this.view.chartData();
258
+ const { rows, columns, complete } = chartData;
259
+ if (!rows || rows.length === 0) return null;
260
+
261
+ // Group rows by facetDimension
262
+ const groups = new Map();
263
+ rows.forEach(r => {
264
+ const key = r[facetDimension] ?? 'All';
265
+ if (!groups.has(key)) groups.set(key, []);
266
+ groups.get(key).push(r);
267
+ });
268
+
269
+ if (groups.size <= 1) return null;
270
+
271
+ const numCols = columns.filter(c => c.kind === 'numeric').map(c => c.name);
272
+ let globalMax = -Infinity;
273
+ let globalMin = Infinity;
274
+ rows.forEach(r => {
275
+ numCols.forEach(col => {
276
+ const val = r[col];
277
+ if (typeof val === 'number' && Number.isFinite(val)) {
278
+ if (val > globalMax) globalMax = val;
279
+ if (val < globalMin) globalMin = val;
280
+ }
281
+ });
282
+ });
283
+
284
+ const chosenChart = this.chosen();
285
+ const baseKey = chosenChart?.key ?? 'bar';
286
+
287
+ const facets = [];
288
+ groups.forEach((groupRows, facetValue) => {
289
+ const subChart = this.chooser.choose(groupRows, columns, { complete }).find(c => c.key === baseKey)
290
+ ?? this.chooser.choose(groupRows, columns, { complete })[0];
291
+ facets.push({
292
+ key: facetValue,
293
+ label: String(facetValue),
294
+ chartKey: subChart?.key ?? baseKey,
295
+ data: subChart?.data ?? groupRows,
296
+ options: { ...(subChart?.options || {}), maxValue: globalMax, minValue: globalMin },
297
+ });
298
+ });
299
+
300
+ return {
301
+ facetDimension,
302
+ facets,
303
+ globalMax: Number.isFinite(globalMax) ? globalMax : 1,
304
+ globalMin: Number.isFinite(globalMin) ? globalMin : 0,
305
+ };
306
+ }
307
+
217
308
  /** The chart's menu: "Chart as" each chart that fits (plus quick top-level items), the one showing ticked. */
218
309
  menu(redraw = () => this.redraw?.()) {
219
310
  const current = this.chosen()?.key;
@@ -225,14 +316,28 @@ export class PivotChart {
225
316
  const quickKeys = new Set([
226
317
  'groupedBar', 'stackedBar', 'bar', 'line', 'area', 'stackedArea',
227
318
  'pareto', 'waterfall', 'marimekko', 'lollipop', 'heatmap', 'radar',
228
- 'treemap', 'sunburst', 'icicle', 'sankey', 'pie', 'donut',
319
+ 'treemap', 'sunburst', 'icicle', 'sankey', 'pie', 'donut', 'pie3D', 'donut3D', 'polarArea', 'surface3D',
229
320
  ]);
230
321
  const quickItems = all.filter(c => quickKeys.has(c.key)).map(toItem);
322
+
323
+ const facetDims = this.facetChoices();
324
+ const facetItems = [
325
+ { label: `(None)${!this.facetBy ? ' ✓' : ''}`, action: () => { this.setFacetBy(null); redraw(); } },
326
+ ...facetDims.map(d => ({
327
+ label: `${d}${this.facetBy === d ? ' ✓' : ''}`,
328
+ action: () => { this.setFacetBy(d); redraw(); },
329
+ })),
330
+ ];
331
+
231
332
  return [
232
333
  {
233
334
  label: 'Chart as',
234
335
  items: all.map(toItem),
235
336
  },
337
+ ...(facetDims.length > 0 ? [{
338
+ label: 'Facet by (Small Multiples)',
339
+ items: facetItems,
340
+ }] : []),
236
341
  ...(quickItems.length > 0 ? [{ separator: true }, ...quickItems] : []),
237
342
  ];
238
343
  }