@peter.prib/renderengine 0.14.0 → 0.15.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/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49391 -39748
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +38 -0
- package/package.json +6 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- 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
|
}
|