@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.
Files changed (109) hide show
  1. package/README.md +45 -19
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49391 -39748
  4. package/dist/renderengine.umd.js +25366 -15650
  5. package/documentation/REFERENCE.md +38 -0
  6. package/package.json +6 -1
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +219 -0
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +3 -0
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +114 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/charts/AlluvialDiagram.js +311 -0
  34. package/src/charting/charts/AreaChart.js +52 -15
  35. package/src/charting/charts/BarChart.js +38 -3
  36. package/src/charting/charts/BarChart3D.js +5 -3
  37. package/src/charting/charts/BarChartBase.js +26 -6
  38. package/src/charting/charts/BeeswarmChart.js +164 -0
  39. package/src/charting/charts/BoxPlotChart.js +23 -1
  40. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  41. package/src/charting/charts/CandlestickChart.js +172 -4
  42. package/src/charting/charts/ChordDiagram.js +105 -2
  43. package/src/charting/charts/CirclePackChart.js +81 -0
  44. package/src/charting/charts/ComboChart.js +233 -0
  45. package/src/charting/charts/ContourChart.js +310 -0
  46. package/src/charting/charts/DonutChart3D.js +13 -0
  47. package/src/charting/charts/DumbbellChart.js +217 -0
  48. package/src/charting/charts/EcdfChart.js +150 -0
  49. package/src/charting/charts/FunnelChart.js +4 -1
  50. package/src/charting/charts/GeoMapChart.js +132 -11
  51. package/src/charting/charts/GroupedBarChart.js +44 -3
  52. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  53. package/src/charting/charts/HeatmapChart.js +2 -2
  54. package/src/charting/charts/HexbinChart.js +137 -0
  55. package/src/charting/charts/HierarchyChart.js +77 -3
  56. package/src/charting/charts/HorizonChart.js +105 -0
  57. package/src/charting/charts/KagiChart.js +167 -0
  58. package/src/charting/charts/LineChart.js +74 -18
  59. package/src/charting/charts/OhlcChart.js +79 -0
  60. package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
  61. package/src/charting/charts/PieChart.js +195 -17
  62. package/src/charting/charts/PieChart3D.js +137 -0
  63. package/src/charting/charts/PointAndFigureChart.js +198 -0
  64. package/src/charting/charts/PolarAreaChart.js +189 -0
  65. package/src/charting/charts/RadarChart.js +7 -1
  66. package/src/charting/charts/RadialBarChart.js +5 -2
  67. package/src/charting/charts/RenkoChart.js +178 -0
  68. package/src/charting/charts/RidgelineChart.js +113 -0
  69. package/src/charting/charts/SankeyDiagram.js +127 -4
  70. package/src/charting/charts/ScatterChart.js +13 -0
  71. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  72. package/src/charting/charts/SlopegraphChart.js +205 -0
  73. package/src/charting/charts/SparklineTableChart.js +166 -0
  74. package/src/charting/charts/StackedBarChart.js +48 -5
  75. package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
  76. package/src/charting/charts/StreamGraphChart.js +10 -3
  77. package/src/charting/charts/SunburstChart.js +27 -7
  78. package/src/charting/charts/SurfaceChart3D.js +157 -0
  79. package/src/charting/charts/TreemapChart.js +28 -8
  80. package/src/charting/charts/ViolinChart.js +18 -0
  81. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  82. package/src/charting/datamodel/DataLineage.js +150 -0
  83. package/src/charting/datamodel/DataModel.js +83 -1
  84. package/src/charting/datamodel/ModelMapping.js +87 -0
  85. package/src/charting/datamodel/SqlDdl.js +110 -85
  86. package/src/charting/datamodel/index.js +3 -0
  87. package/src/charting/decision/DecisionWorkbench.js +295 -0
  88. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  89. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  90. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  91. package/src/charting/diagrams/SketchView.js +121 -0
  92. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  93. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  94. package/src/charting/modelTree/NavigatorModel.js +410 -0
  95. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  96. package/src/charting/pivot/PivotChart.js +109 -4
  97. package/src/charting/pivot/PivotGridChart.js +321 -33
  98. package/src/charting/pivot/PivotView.js +82 -0
  99. package/src/charting/report/TemplatedReport.js +547 -0
  100. package/src/charting/templates/BoxTemplate.js +28 -27
  101. package/src/charting/templates/PackageTemplate.js +26 -25
  102. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  103. package/src/charting/templates/Template.js +219 -212
  104. package/src/cli.js +146 -0
  105. package/src/index.js +3370 -2995
  106. package/src/workspace/Workspace.js +126 -7
  107. package/src/workspace/archimateServices.js +4 -0
  108. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  109. package/src/workspace/styles.js +36 -1
@@ -0,0 +1,410 @@
1
+ import { treeLayerOf } from './ModelTreePanel.js';
2
+
3
+ /**
4
+ * NavigatorModel — pure, DOM-free graph traversal and navigation engine for an ArchiMate model.
5
+ *
6
+ * Allows exploring relationships step-by-step from any focused root element to arbitrary depth,
7
+ * with cycle detection, navigation history (back/forward), direction filters, path-finding
8
+ * between elements, and transitive impact analysis.
9
+ */
10
+ export class NavigatorModel {
11
+ /**
12
+ * @param {ArchimateModel} model
13
+ * @param {Object} [options]
14
+ * @param {string} [options.rootId] - Initial focused element id
15
+ * @param {'both'|'outgoing'|'incoming'} [options.direction='both']
16
+ * @param {Iterable<string>} [options.allowedKinds] - Optional relationship kinds to include
17
+ */
18
+ constructor(model, { rootId = null, direction = 'both', allowedKinds = null } = {}) {
19
+ this.model = model;
20
+ this.direction = direction;
21
+ this.allowedKinds = allowedKinds ? new Set(allowedKinds) : null;
22
+ this.pinned = false;
23
+
24
+ this.history = [];
25
+ this.historyIndex = -1;
26
+ this.currentId = null;
27
+
28
+ this.expandedPaths = new Set();
29
+ this._listeners = new Set();
30
+
31
+ if (rootId && model.getElement(rootId)) {
32
+ this.focus(rootId);
33
+ }
34
+ }
35
+
36
+ /** Register a listener called on state changes (focus, history, expansion, pin). */
37
+ onChange(listener) {
38
+ this._listeners.add(listener);
39
+ return () => this._listeners.delete(listener);
40
+ }
41
+
42
+ _notify() {
43
+ for (const listener of this._listeners) {
44
+ try {
45
+ listener(this);
46
+ } catch (err) {
47
+ console.warn('NavigatorModel listener error:', err);
48
+ }
49
+ }
50
+ }
51
+
52
+ // ── Navigation & History ──
53
+
54
+ /** Whether back navigation is available. */
55
+ get canBack() {
56
+ return this.historyIndex > 0;
57
+ }
58
+
59
+ /** Whether forward navigation is available. */
60
+ get canForward() {
61
+ return this.historyIndex < this.history.length - 1;
62
+ }
63
+
64
+ /**
65
+ * Focus on an element as the new root of the navigator.
66
+ * Pushes to history unless currently pinned or already focused.
67
+ * @param {string} elementId
68
+ * @param {Object} [options]
69
+ * @param {boolean} [options.force=false] - Ignore pinned state
70
+ * @returns {boolean} whether focus changed
71
+ */
72
+ focus(elementId, { force = false } = {}) {
73
+ if (!force && this.pinned && this.currentId) return false;
74
+ if (!elementId || elementId === this.currentId) return false;
75
+ const element = this.model.getElement(elementId);
76
+ if (!element) return false;
77
+
78
+ // Truncate forward history if we navigated from mid-stack
79
+ if (this.historyIndex < this.history.length - 1) {
80
+ this.history = this.history.slice(0, this.historyIndex + 1);
81
+ }
82
+
83
+ this.history.push(elementId);
84
+ this.historyIndex = this.history.length - 1;
85
+ this.currentId = elementId;
86
+ this.expandedPaths.clear();
87
+ this.expandedPaths.add(elementId); // root is expanded by default
88
+
89
+ this._notify();
90
+ return true;
91
+ }
92
+
93
+ /** Navigate back in history. */
94
+ back() {
95
+ if (!this.canBack) return false;
96
+ this.historyIndex--;
97
+ this.currentId = this.history[this.historyIndex];
98
+ this.expandedPaths.clear();
99
+ this.expandedPaths.add(this.currentId);
100
+ this._notify();
101
+ return true;
102
+ }
103
+
104
+ /** Navigate forward in history. */
105
+ forward() {
106
+ if (!this.canForward) return false;
107
+ this.historyIndex++;
108
+ this.currentId = this.history[this.historyIndex];
109
+ this.expandedPaths.clear();
110
+ this.expandedPaths.add(this.currentId);
111
+ this._notify();
112
+ return true;
113
+ }
114
+
115
+ /** Toggle pin state. When pinned, canvas selection changes won't re-root the navigator. */
116
+ togglePin() {
117
+ this.pinned = !this.pinned;
118
+ this._notify();
119
+ return this.pinned;
120
+ }
121
+
122
+ setDirection(direction) {
123
+ if (direction !== 'both' && direction !== 'outgoing' && direction !== 'incoming') return;
124
+ this.direction = direction;
125
+ this._notify();
126
+ }
127
+
128
+ setAllowedKinds(kinds) {
129
+ this.allowedKinds = kinds ? new Set(kinds) : null;
130
+ this._notify();
131
+ }
132
+
133
+ // ── Expansion State ──
134
+
135
+ isExpanded(pathKey) {
136
+ return this.expandedPaths.has(pathKey);
137
+ }
138
+
139
+ toggleExpand(pathKey) {
140
+ if (this.expandedPaths.has(pathKey)) {
141
+ this.expandedPaths.delete(pathKey);
142
+ } else {
143
+ this.expandedPaths.add(pathKey);
144
+ }
145
+ this._notify();
146
+ }
147
+
148
+ expand(pathKey) {
149
+ if (!this.expandedPaths.has(pathKey)) {
150
+ this.expandedPaths.add(pathKey);
151
+ this._notify();
152
+ }
153
+ }
154
+
155
+ collapse(pathKey) {
156
+ if (this.expandedPaths.has(pathKey)) {
157
+ this.expandedPaths.delete(pathKey);
158
+ this._notify();
159
+ }
160
+ }
161
+
162
+ collapseAll() {
163
+ this.expandedPaths.clear();
164
+ if (this.currentId) this.expandedPaths.add(this.currentId);
165
+ this._notify();
166
+ }
167
+
168
+ // ── Tree Construction ──
169
+
170
+ /**
171
+ * Build the multi-step navigation tree from the focused element,
172
+ * following expanded branches with cycle detection.
173
+ *
174
+ * @param {string} [rootId=this.currentId]
175
+ * @returns {Object|null}
176
+ */
177
+ buildTree(rootId = this.currentId) {
178
+ if (!rootId) return null;
179
+ const element = this.model.getElement(rootId);
180
+ if (!element) return null;
181
+
182
+ return this._buildNode(element, {
183
+ pathKey: rootId,
184
+ visitedInPath: new Set([rootId]),
185
+ });
186
+ }
187
+
188
+ /** @private */
189
+ _buildNode(element, { pathKey, visitedInPath }) {
190
+ const { incoming, outgoing } = this.neighbors(element.id);
191
+
192
+ const buildBranch = (rel, otherElement, dir) => {
193
+ const relKey = `${pathKey}:${dir}:${rel.id}:${otherElement.id}`;
194
+ const cycle = visitedInPath.has(otherElement.id);
195
+ const expanded = !cycle && this.isExpanded(relKey);
196
+
197
+ let children = null;
198
+ if (expanded) {
199
+ const nextVisited = new Set(visitedInPath);
200
+ nextVisited.add(otherElement.id);
201
+ children = this._buildNode(otherElement, {
202
+ pathKey: relKey,
203
+ visitedInPath: nextVisited,
204
+ });
205
+ }
206
+
207
+ return {
208
+ pathKey: relKey,
209
+ direction: dir,
210
+ relationship: rel,
211
+ element: otherElement,
212
+ layer: treeLayerOf(otherElement),
213
+ cycle,
214
+ expanded,
215
+ children,
216
+ };
217
+ };
218
+
219
+ const incomingBranches = (this.direction === 'both' || this.direction === 'incoming')
220
+ ? incoming.map(({ relationship, element: source }) => buildBranch(relationship, source, 'incoming'))
221
+ : [];
222
+
223
+ const outgoingBranches = (this.direction === 'both' || this.direction === 'outgoing')
224
+ ? outgoing.map(({ relationship, element: target }) => buildBranch(relationship, target, 'outgoing'))
225
+ : [];
226
+
227
+ return {
228
+ pathKey,
229
+ element,
230
+ layer: treeLayerOf(element),
231
+ views: this.model.viewsContaining(element.id),
232
+ incoming: incomingBranches,
233
+ outgoing: outgoingBranches,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Get immediate connected elements and relationships for an element.
239
+ * @param {string} elementId
240
+ * @returns {{ incoming: Array<{relationship: Object, element: Object}>, outgoing: Array<{relationship: Object, element: Object}> }}
241
+ */
242
+ neighbors(elementId) {
243
+ const { incoming, outgoing } = this.model.relationshipsFor(elementId);
244
+ const matchKind = (r) => !this.allowedKinds || this.allowedKinds.has(r.kind);
245
+
246
+ const inItems = incoming.filter(matchKind).map(rel => {
247
+ const source = this.model.getElement(rel.source);
248
+ return source ? { relationship: rel, element: source } : null;
249
+ }).filter(Boolean);
250
+
251
+ const outItems = outgoing.filter(matchKind).map(rel => {
252
+ const target = this.model.getElement(rel.target);
253
+ return target ? { relationship: rel, element: target } : null;
254
+ }).filter(Boolean);
255
+
256
+ return { incoming: inItems, outgoing: outItems };
257
+ }
258
+
259
+ // ── Graph Analysis: Path Finding & Impact Analysis ──
260
+
261
+ /**
262
+ * Find all simple paths between startId and endId up to maxDepth.
263
+ *
264
+ * @param {string} startId
265
+ * @param {string} endId
266
+ * @param {Object} [options]
267
+ * @param {number} [options.maxDepth=5]
268
+ * @param {'outgoing'|'both'|'incoming'} [options.direction='both']
269
+ * @param {Iterable<string>} [options.allowedKinds]
270
+ * @returns {Array<{ elements: Array<Object>, relationships: Array<Object>, length: number }>}
271
+ */
272
+ findPaths(startId, endId, { maxDepth = 5, direction = 'both', allowedKinds = null } = {}) {
273
+ const paths = [];
274
+ const start = this.model.getElement(startId);
275
+ const target = this.model.getElement(endId);
276
+ if (!start || !target || startId === endId) return paths;
277
+
278
+ const kinds = allowedKinds ? new Set(allowedKinds) : this.allowedKinds;
279
+ const matchKind = (r) => !kinds || kinds.has(r.kind);
280
+
281
+ const search = (currentId, currentElements, currentRels, visited) => {
282
+ if (currentElements.length - 1 > maxDepth) return;
283
+ if (currentId === endId) {
284
+ paths.push({
285
+ elements: [...currentElements],
286
+ relationships: [...currentRels],
287
+ length: currentRels.length,
288
+ });
289
+ return;
290
+ }
291
+
292
+ const { incoming, outgoing } = this.model.relationshipsFor(currentId);
293
+
294
+ if (direction === 'both' || direction === 'outgoing') {
295
+ for (const rel of outgoing) {
296
+ if (!matchKind(rel)) continue;
297
+ const nextId = rel.target;
298
+ if (!visited.has(nextId)) {
299
+ const nextElem = this.model.getElement(nextId);
300
+ if (!nextElem) continue;
301
+ visited.add(nextId);
302
+ currentElements.push(nextElem);
303
+ currentRels.push(rel);
304
+ search(nextId, currentElements, currentRels, visited);
305
+ currentRels.pop();
306
+ currentElements.pop();
307
+ visited.delete(nextId);
308
+ }
309
+ }
310
+ }
311
+
312
+ if (direction === 'both' || direction === 'incoming') {
313
+ for (const rel of incoming) {
314
+ if (!matchKind(rel)) continue;
315
+ const nextId = rel.source;
316
+ if (!visited.has(nextId)) {
317
+ const nextElem = this.model.getElement(nextId);
318
+ if (!nextElem) continue;
319
+ visited.add(nextId);
320
+ currentElements.push(nextElem);
321
+ currentRels.push(rel);
322
+ search(nextId, currentElements, currentRels, visited);
323
+ currentRels.pop();
324
+ currentElements.pop();
325
+ visited.delete(nextId);
326
+ }
327
+ }
328
+ }
329
+ };
330
+
331
+ const visited = new Set([startId]);
332
+ search(startId, [start], [], visited);
333
+
334
+ // Sort by path length ascending
335
+ return paths.sort((a, b) => a.length - b.length);
336
+ }
337
+
338
+ /**
339
+ * Compute transitive impact or dependencies from an element.
340
+ *
341
+ * @param {string} elementId
342
+ * @param {Object} [options]
343
+ * @param {'outgoing'|'incoming'|'both'} [options.direction='outgoing']
344
+ * @param {number} [options.maxDepth=10]
345
+ * @param {Iterable<string>} [options.allowedKinds]
346
+ * @returns {{ root: Object, elements: Array<Object>, relationships: Array<Object>, depths: Map<string, number>, layers: Set<string> }}
347
+ */
348
+ traceImpact(elementId, { direction = 'outgoing', maxDepth = 10, allowedKinds = null } = {}) {
349
+ const root = this.model.getElement(elementId);
350
+ if (!root) return { root: null, elements: [], relationships: [], depths: new Map(), layers: new Set() };
351
+
352
+ const kinds = allowedKinds ? new Set(allowedKinds) : this.allowedKinds;
353
+ const matchKind = (r) => !kinds || kinds.has(r.kind);
354
+
355
+ const depths = new Map([[elementId, 0]]);
356
+ const elementsMap = new Map();
357
+ const relsMap = new Map();
358
+ const layers = new Set([treeLayerOf(root)]);
359
+
360
+ const queue = [{ id: elementId, depth: 0 }];
361
+
362
+ while (queue.length) {
363
+ const { id: currId, depth } = queue.shift();
364
+ if (depth >= maxDepth) continue;
365
+
366
+ const { incoming, outgoing } = this.model.relationshipsFor(currId);
367
+ const nextDepth = depth + 1;
368
+
369
+ if (direction === 'both' || direction === 'outgoing') {
370
+ for (const rel of outgoing) {
371
+ if (!matchKind(rel)) continue;
372
+ const target = this.model.getElement(rel.target);
373
+ if (!target) continue;
374
+
375
+ relsMap.set(rel.id, rel);
376
+ if (!depths.has(target.id)) {
377
+ depths.set(target.id, nextDepth);
378
+ elementsMap.set(target.id, target);
379
+ layers.add(treeLayerOf(target));
380
+ queue.push({ id: target.id, depth: nextDepth });
381
+ }
382
+ }
383
+ }
384
+
385
+ if (direction === 'both' || direction === 'incoming') {
386
+ for (const rel of incoming) {
387
+ if (!matchKind(rel)) continue;
388
+ const source = this.model.getElement(rel.source);
389
+ if (!source) continue;
390
+
391
+ relsMap.set(rel.id, rel);
392
+ if (!depths.has(source.id)) {
393
+ depths.set(source.id, nextDepth);
394
+ elementsMap.set(source.id, source);
395
+ layers.add(treeLayerOf(source));
396
+ queue.push({ id: source.id, depth: nextDepth });
397
+ }
398
+ }
399
+ }
400
+ }
401
+
402
+ return {
403
+ root,
404
+ elements: [...elementsMap.values()],
405
+ relationships: [...relsMap.values()],
406
+ depths,
407
+ layers,
408
+ };
409
+ }
410
+ }