@peter.prib/renderengine 0.5.0 → 0.8.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 (74) hide show
  1. package/README.md +4 -0
  2. package/REFERENCE.md +297 -17
  3. package/dist/renderengine-workspace.umd.js +5369 -2952
  4. package/dist/renderengine.umd.js +4466 -2320
  5. package/examples/archimate.html +8 -28
  6. package/examples/data-model.html +45 -556
  7. package/package.json +70 -70
  8. package/src/archimateArchiFormat.js +11 -3
  9. package/src/archimateExchangeFormat.js +17 -9
  10. package/src/chartCatalog.js +10 -0
  11. package/src/charting/ArchimateDiagramEditor.js +1 -1
  12. package/src/charting/Axis.js +20 -0
  13. package/src/charting/ChartChooser.js +229 -0
  14. package/src/charting/DiagramEditor.js +35 -3
  15. package/src/charting/Dialog.js +195 -0
  16. package/src/charting/DuplicatesDialog.js +108 -0
  17. package/src/charting/FormDialog.js +28 -97
  18. package/src/charting/InteractionManager.js +3 -0
  19. package/src/charting/Legend.js +45 -2
  20. package/src/charting/MergeCompareDialog.js +81 -0
  21. package/src/charting/PopupMenu.js +12 -3
  22. package/src/charting/SettingsDialog.js +6 -2
  23. package/src/charting/SettingsManager.js +14 -1
  24. package/src/charting/archimate/ArchimateCanvasEditor.js +190 -15
  25. package/src/charting/archimate/ArchimatePalette.js +1 -1
  26. package/src/charting/charts/AreaChart.js +5 -2
  27. package/src/charting/charts/BarChart.js +5 -3
  28. package/src/charting/charts/BarChart3DOrbit.js +5 -7
  29. package/src/charting/charts/BubbleChart.js +5 -7
  30. package/src/charting/charts/BubbleChart3D.js +5 -7
  31. package/src/charting/charts/CandlestickChart.js +3 -1
  32. package/src/charting/charts/DensityChart.js +4 -6
  33. package/src/charting/charts/GroupedBarChart.js +2 -2
  34. package/src/charting/charts/LineChart.js +5 -2
  35. package/src/charting/charts/ParallelCoordinatesChart.js +2 -2
  36. package/src/charting/charts/PipeChart.js +4 -6
  37. package/src/charting/charts/RadarChart.js +5 -6
  38. package/src/charting/charts/ScatterChart.js +5 -7
  39. package/src/charting/charts/StackedBarChart.js +4 -2
  40. package/src/charting/charts/StackedBarChart3DOrbit.js +5 -7
  41. package/src/charting/charts/StreamGraphChart.js +5 -2
  42. package/src/charting/charts/TableChart.js +98 -0
  43. package/src/charting/charts/WaterfallChart.js +2 -1
  44. package/src/charting/content/ContentSource.js +129 -0
  45. package/src/charting/content/ContentSources.js +77 -0
  46. package/src/charting/datamodel/DataModel.js +116 -0
  47. package/src/charting/diagrams/ArchimateModel.js +38 -3
  48. package/src/charting/diagrams/ArchimateModelChecker.js +13 -4
  49. package/src/charting/diagrams/MergeElementsCommand.js +124 -101
  50. package/src/charting/diagrams/SvgZoom.js +20 -1
  51. package/src/charting/diagrams/duplicates.js +95 -13
  52. package/src/charting/duplicateSearchForm.js +58 -0
  53. package/src/charting/modelTree/ModelTree.js +239 -202
  54. package/src/charting/modelTree/ModelTreePanel.js +955 -919
  55. package/src/charting/modelTree/TreeBuilder.js +93 -45
  56. package/src/charting/modelTree/TreeViewMode.js +9 -6
  57. package/src/charting/modelTree/viewFolders.js +15 -6
  58. package/src/icons/IconSet.js +86 -0
  59. package/src/icons/index.js +59 -0
  60. package/src/index.js +86 -10
  61. package/src/workspace/dom.js +56 -42
  62. package/src/workspace/index.js +1 -1
  63. package/src/workspace/nav/NavEntry.js +42 -0
  64. package/src/workspace/nav/NavGroup.js +43 -0
  65. package/src/workspace/nav/NavItem.js +53 -0
  66. package/src/workspace/nav/NavMenu.js +143 -0
  67. package/src/workspace/nav/NavSection.js +17 -0
  68. package/src/workspace/nav/index.js +5 -0
  69. package/src/workspace/parts/ArchimateCanvasPart.js +8 -3
  70. package/src/workspace/parts/DataModelPart.js +91 -0
  71. package/src/workspace/parts/EditorPart.js +8 -0
  72. package/src/workspace/parts/ModelHosted.js +1 -0
  73. package/src/workspace/parts/ModelsPart.js +765 -622
  74. package/src/workspace/styles.js +28 -1
@@ -1,3 +1,20 @@
1
+ /** A row of the tree, branch or leaf: one line, compact; a label too long ends in an ellipsis. */
2
+ const ROW_STYLE = `
3
+ cursor: pointer;
4
+ padding: 1px 4px;
5
+ margin: 0;
6
+ border-radius: 3px;
7
+ user-select: none;
8
+ border-left: 3px solid transparent;
9
+ transition: background-color 0.1s;
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 4px;
13
+ line-height: 1.35;
14
+ white-space: nowrap;
15
+ overflow: hidden;
16
+ `;
17
+
1
18
  /**
2
19
  * Generic tree component builder for hierarchical node rendering.
3
20
  * Handles expand/collapse, selection, context menus, and recursive rendering.
@@ -19,8 +36,14 @@ export class TreeBuilder {
19
36
  * @param {Map<string, boolean>} [options.openState] - Which folders are open, by node id: kept
20
37
  * by the caller so a rebuilt tree opens them as they were. A folder not in it is open unless
21
38
  * its node says `collapsed: true`; opening or closing one records it here.
39
+ * @param {(node) => boolean} [options.filter] - Which leaves are shown: a folder is shown when a
40
+ * leaf under it is, and open while filtering. None: every node.
41
+ * @param {number} [options.leafLimit] - The leaves a folder shows; the rest wait behind a
42
+ * "▸ N more" row, which shows them. The folders inside it always show.
43
+ * @param {Set<string>} [options.shownAll] - The folders (by node id) showing every leaf, kept by
44
+ * the caller as openState is; asking for "more" records the folder here.
22
45
  */
23
- constructor(doc, { container, rootNodes = [], onNodeClick = null, onNodeContext = null, getNodeChildren = null, openState = new Map(), iconFor = null } = {}) {
46
+ constructor(doc, { container, rootNodes = [], onNodeClick = null, onNodeContext = null, getNodeChildren = null, openState = new Map(), iconFor = null, filter = null, leafLimit = Infinity, shownAll = new Set() } = {}) {
24
47
  this.doc = doc;
25
48
  this.container = container;
26
49
  this.rootNodes = rootNodes;
@@ -29,7 +52,12 @@ export class TreeBuilder {
29
52
  this.getNodeChildren = getNodeChildren;
30
53
  this.openState = openState;
31
54
  this.iconFor = iconFor;
55
+ this.filter = filter;
56
+ this.leafLimit = leafLimit;
57
+ this.shownAll = shownAll;
32
58
  this.selectedNode = null;
59
+ /** Each node's shown children, asked for once per render (getNodeChildren makes new nodes). */
60
+ this._children = new WeakMap();
33
61
  }
34
62
 
35
63
  /**
@@ -40,10 +68,25 @@ export class TreeBuilder {
40
68
  const container = this.container;
41
69
  if (!container) return;
42
70
 
43
- container.innerHTML = '';
44
- for (const node of nodes) {
45
- container.appendChild(this._renderNode(node));
71
+ this._children = new WeakMap();
72
+ container.replaceChildren(...nodes.filter(node => this._shown(node)).map(node => this._renderNode(node)));
73
+ }
74
+
75
+ /** A node's children that are shown: with a filter, the leaves it keeps and the folders holding one. @private */
76
+ _childrenOf(node) {
77
+ if (!this._children.has(node)) {
78
+ const all = this.getNodeChildren?.(node) || [];
79
+ this._children.set(node, this.filter ? all.filter(child => this._shown(child)) : all);
46
80
  }
81
+ return this._children.get(node);
82
+ }
83
+
84
+ /** Whether a node is shown: always without a filter; else a leaf the filter keeps, or a folder with a shown child. @private */
85
+ _shown(node) {
86
+ if (!this.filter) return true;
87
+ if (this._childrenOf(node).length) return true;
88
+ // A folder whose children are all filtered out is not a leaf to test.
89
+ return !(this.getNodeChildren?.(node) || []).length && !!this.filter(node);
47
90
  }
48
91
 
49
92
  /**
@@ -51,14 +94,8 @@ export class TreeBuilder {
51
94
  * @private
52
95
  */
53
96
  _renderNode(node) {
54
- const children = this.getNodeChildren?.(node) || [];
55
- const hasChildren = children.length > 0;
56
-
57
- if (hasChildren) {
58
- return this._renderExpandable(node, children);
59
- } else {
60
- return this._renderLeaf(node);
61
- }
97
+ const children = this._childrenOf(node);
98
+ return children.length ? this._renderExpandable(node, children) : this._renderLeaf(node);
62
99
  }
63
100
 
64
101
  /**
@@ -67,23 +104,13 @@ export class TreeBuilder {
67
104
  */
68
105
  _renderExpandable(node, children) {
69
106
  const details = this.doc.createElement('details');
70
- details.open = this.openState.get(node.id) ?? !node.collapsed;
107
+ // Open while filtering, to show what was found (not recorded); else as it was.
108
+ details.open = this.filter ? true : this.openState.get(node.id) ?? !node.collapsed;
71
109
  details.setAttribute('data-node-id', node.id);
72
- details.addEventListener('toggle', () => this.openState.set(node.id, details.open));
110
+ details.addEventListener('toggle', () => { if (!this.filter) this.openState.set(node.id, details.open); });
73
111
 
74
112
  const summary = this.doc.createElement('summary');
75
- summary.style.cssText = `
76
- cursor: pointer;
77
- padding: 4px 6px;
78
- margin: 2px 0;
79
- border-radius: 3px;
80
- user-select: none;
81
- border-left: 3px solid transparent;
82
- transition: background-color 0.1s;
83
- display: flex;
84
- align-items: center;
85
- gap: 6px;
86
- `;
113
+ summary.style.cssText = ROW_STYLE;
87
114
  summary.setAttribute('data-node-id', node.id);
88
115
 
89
116
  this._applyNodeStyling(summary, node);
@@ -95,15 +122,45 @@ export class TreeBuilder {
95
122
  details.appendChild(summary);
96
123
 
97
124
  const childContainer = this.doc.createElement('div');
98
- childContainer.style.cssText = 'padding-left: 16px;';
99
- for (const child of children) {
100
- childContainer.appendChild(this._renderNode(child));
101
- }
125
+ childContainer.style.cssText = 'padding-left: 12px;';
126
+ this._fillChildren(childContainer, node, children);
102
127
  details.appendChild(childContainer);
103
128
 
104
129
  return details;
105
130
  }
106
131
 
132
+ /**
133
+ * A folder's children: its folders, and its leaves up to leafLimit, then a
134
+ * "▸ N more" row that shows the rest (unless the folder shows them all already).
135
+ * @private
136
+ */
137
+ _fillChildren(container, node, children) {
138
+ const limit = this.shownAll.has(node.id) ? Infinity : this.leafLimit;
139
+ let leaves = 0;
140
+ let waiting = 0;
141
+ const rows = [];
142
+ for (const child of children) {
143
+ if (!this._childrenOf(child).length && leaves++ >= limit) { waiting++; continue; }
144
+ rows.push(this._renderNode(child));
145
+ }
146
+ if (waiting) rows.push(this._moreRow(waiting, () => {
147
+ this.shownAll.add(node.id);
148
+ this._fillChildren(container, node, children);
149
+ }));
150
+ container.replaceChildren(...rows);
151
+ }
152
+
153
+ /** The row standing for `count` leaves not shown; choosing it calls `showAll`. @private */
154
+ _moreRow(count, showAll) {
155
+ const row = this.doc.createElement('div');
156
+ row.setAttribute('data-tree-more', String(count));
157
+ row.style.cssText = `${ROW_STYLE} color: #666; font-style: italic;`;
158
+ row.textContent = `▸ ${count} more`;
159
+ row.title = 'Show them all';
160
+ row.addEventListener('click', (e) => { e.stopPropagation(); showAll(); });
161
+ return row;
162
+ }
163
+
107
164
  /**
108
165
  * Render a leaf node without children.
109
166
  * @private
@@ -111,23 +168,12 @@ export class TreeBuilder {
111
168
  _renderLeaf(node) {
112
169
  const leaf = this.doc.createElement('div');
113
170
  leaf.setAttribute('data-node-id', node.id);
114
- // Element leaves are filter targets and drag sources; mark them here so a rebuilt tree keeps that.
171
+ // Element leaves are drag sources; mark them here so a rebuilt tree keeps that.
115
172
  if (node.type === 'element' && node._element) {
116
173
  leaf.setAttribute('data-element-id', node._element.id);
117
174
  leaf.draggable = true;
118
175
  }
119
- leaf.style.cssText = `
120
- padding: 4px 6px;
121
- margin: 2px 0;
122
- border-radius: 3px;
123
- cursor: pointer;
124
- user-select: none;
125
- border-left: 3px solid transparent;
126
- transition: background-color 0.1s;
127
- display: flex;
128
- align-items: center;
129
- gap: 6px;
130
- `;
176
+ leaf.style.cssText = ROW_STYLE;
131
177
 
132
178
  this._applyNodeStyling(leaf, node);
133
179
  leaf.addEventListener('click', (e) => this._handleNodeClick(e, leaf, node));
@@ -153,13 +199,15 @@ export class TreeBuilder {
153
199
  } else if (node.icon) {
154
200
  const icon = this.doc.createElement('span');
155
201
  icon.textContent = node.icon;
156
- icon.style.cssText = 'flex-shrink: 0; font-size: 14px;';
202
+ icon.style.cssText = 'flex-shrink: 0; font-size: 11px;';
157
203
  element.appendChild(icon);
158
204
  }
159
205
 
160
- // Label
206
+ // Label: one line, cut with an ellipsis; the whole of it on hover.
161
207
  const label = this.doc.createElement('span');
162
208
  label.textContent = node.label || 'Untitled';
209
+ label.style.cssText = 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;';
210
+ element.title = label.textContent;
163
211
  element.appendChild(label);
164
212
  }
165
213
 
@@ -7,6 +7,13 @@ export class TreeViewMode {
7
7
  static key = null; // registry key; subclasses override
8
8
  static label = ''; // human-readable label; subclasses override
9
9
 
10
+ /** Whether `text` is in any of `fields` (case ignored); no text matches everything. */
11
+ static textMatches(text, fields) {
12
+ if (!text) return true;
13
+ const lower = text.toLowerCase();
14
+ return fields.some(f => String(f ?? '').toLowerCase().includes(lower));
15
+ }
16
+
10
17
  constructor() {
11
18
  if (new.target === TreeViewMode) {
12
19
  throw new TypeError('TreeViewMode is abstract — subclass it');
@@ -59,12 +66,8 @@ export class TreeViewMode {
59
66
  */
60
67
  _matchesFilter(item, { text = '', typeIds = null, specializationIds = null, propertyIds = null, inDescriptions = false } = {}) {
61
68
  // Text filter (label or type; the description too when asked)
62
- if (text) {
63
- const textLower = text.toLowerCase();
64
- const fields = [item.label, item.type, ...(inDescriptions ? [item.description] : [])];
65
- if (!fields.some(f => String(f ?? '').toLowerCase().includes(textLower))) {
66
- return false;
67
- }
69
+ if (!TreeViewMode.textMatches(text, [item.label, item.type, ...(inDescriptions ? [item.description] : [])])) {
70
+ return false;
68
71
  }
69
72
 
70
73
  // Type filter (elements only; 'type:business-actor' format)
@@ -6,23 +6,32 @@ const VIEW_ICONS = Object.freeze({ canvas: '🎨' });
6
6
  * keeps them: each view remembers its sub-folder path (`archiFolder`, folder
7
7
  * steps {id, name}). A folder node per sub-folder at this depth (holding the
8
8
  * views under it, opened a level deeper), then the views that sit here, each
9
- * in the model's order.
9
+ * in the model's order. A sub-folder that holds no view is there too, from
10
+ * its path (the model's `emptyViewFolders`).
10
11
  * @param {Array<Object>} views
11
12
  * @param {number} [depth]
13
+ * @param {Array<Array<{id, name}>>} [emptyFolders] - The paths of the folders with no view in them
12
14
  * @returns {Array<Object>} TreeBuilder nodes: {id, label, icon, type: 'view-folder'|'view', ...}
13
15
  */
14
- export function viewFolderNodes(views, depth = 0) {
16
+ export function viewFolderNodes(views, depth = 0, emptyFolders = []) {
15
17
  const folders = new Map();
18
+ const folderOf = (step) => {
19
+ if (!folders.has(step.id)) folders.set(step.id, { step, views: [], empty: [] });
20
+ return folders.get(step.id);
21
+ };
16
22
  const here = [];
17
23
  for (const view of views) {
18
24
  const step = view.archiFolder?.[depth];
19
25
  if (!step) { here.push(view); continue; }
20
- if (!folders.has(step.id)) folders.set(step.id, { step, views: [] });
21
- folders.get(step.id).views.push(view);
26
+ folderOf(step).views.push(view);
27
+ }
28
+ for (const path of emptyFolders) {
29
+ if (path.length > depth) folderOf(path[depth]).empty.push(path);
22
30
  }
23
31
  return [
24
- ...[...folders.values()].map(({ step, views: inside }) => ({
25
- id: `view-folder-${step.id}`, label: step.name || 'Untitled Folder', icon: '📁', type: 'view-folder', _views: inside, _depth: depth + 1,
32
+ ...[...folders.values()].map(({ step, views: inside, empty }) => ({
33
+ id: `view-folder-${step.id}`, label: step.name || 'Untitled Folder', icon: '📁', type: 'view-folder',
34
+ _views: inside, _emptyFolders: empty, _depth: depth + 1,
26
35
  })),
27
36
  ...here.map(view => ({
28
37
  id: view.id, label: view.name || view.title || 'Untitled View', icon: VIEW_ICONS[view.constructor.kind] ?? '📄', type: 'view', _view: view,
@@ -0,0 +1,86 @@
1
+ /**
2
+ * IconSet — named icons as data, built as `<svg>` specs for el().
3
+ *
4
+ * An icon is JSON: a viewBox, how it is painted, and its shapes as
5
+ * `[tag, attrs]` pairs:
6
+ *
7
+ * { viewBox: '0 0 24 24', paint: 'stroke', shapes: [['circle', { cx: 12, cy: 12, r: 10 }]] }
8
+ *
9
+ * `paint` is 'stroke' (outlined in currentColor — the default, so an icon
10
+ * takes its parent's colour), 'fill' (filled in currentColor) or 'none' (the
11
+ * shapes carry their own colours: a brand mark). No markup is parsed: el()
12
+ * builds the spec in the SVG namespace.
13
+ */
14
+ const PAINT = {
15
+ stroke: { fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' },
16
+ fill: { fill: 'currentColor' },
17
+ none: {},
18
+ };
19
+
20
+ export class IconSet {
21
+ /** @param {Object<string, Object>} [defs] - Name → icon */
22
+ constructor(defs = {}) {
23
+ this.defs = new Map();
24
+ this.add(defs);
25
+ }
26
+
27
+ /** Add (or replace) icons, name → icon. */
28
+ add(defs) {
29
+ for (const [name, def] of Object.entries(defs)) this.define(name, def);
30
+ return this;
31
+ }
32
+
33
+ /** Add (or replace) one icon; throws when it is not one. */
34
+ define(name, def) {
35
+ if (typeof def?.viewBox !== 'string' || !Array.isArray(def.shapes) || !def.shapes.length) {
36
+ throw new Error(`IconSet: icon "${name}" needs a viewBox and shapes`);
37
+ }
38
+ if (def.paint != null && !(def.paint in PAINT)) {
39
+ throw new Error(`IconSet: icon "${name}" paint is ${Object.keys(PAINT).join(', ')}, not "${def.paint}"`);
40
+ }
41
+ for (const shape of def.shapes) {
42
+ if (!Array.isArray(shape) || typeof shape[0] !== 'string') throw new Error(`IconSet: icon "${name}" shapes are [tag, attrs] pairs`);
43
+ }
44
+ this.defs.set(name, def);
45
+ return this;
46
+ }
47
+
48
+ has(name) { return this.defs.has(name); }
49
+
50
+ get(name) { return this.defs.get(name) ?? null; }
51
+
52
+ /** Every icon's name, in the order added. */
53
+ names() { return [...this.defs.keys()]; }
54
+
55
+ /** A copy of this set with `defs` added: a host's own icons over these. */
56
+ with(defs) {
57
+ return new IconSet(Object.fromEntries(this.defs)).add(defs);
58
+ }
59
+
60
+ /**
61
+ * The icon as an el() spec.
62
+ * @param {string} name
63
+ * @param {Object} [options]
64
+ * @param {number|string} [options.size] - Width and height (default 24; CSS can override)
65
+ * @param {string} [options.title] - Its accessible name; without one it is hidden from screen readers
66
+ * @param {string|string[]} [options.cls] - Extra class names
67
+ */
68
+ spec(name, { size = 24, title = null, cls = null } = {}) {
69
+ const def = this.defs.get(name);
70
+ if (!def) throw new Error(`IconSet: no icon "${name}"`);
71
+ return {
72
+ tag: 'svg',
73
+ cls: ['re-icon', ...(Array.isArray(cls) ? cls : [cls])],
74
+ attrs: {
75
+ viewBox: def.viewBox, width: size, height: size, focusable: 'false',
76
+ role: title ? 'img' : null, 'aria-hidden': title ? null : 'true',
77
+ ...PAINT[def.paint ?? 'stroke'],
78
+ },
79
+ dataset: { icon: name },
80
+ children: [
81
+ ...(title ? [{ tag: 'title', text: title }] : []),
82
+ ...def.shapes.map(([tag, attrs]) => ({ tag, attrs })),
83
+ ],
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The built-in icons — a shared pool any host draws from (`ICONS.spec('cpu')`,
3
+ * or `icon('cpu')`), extended with its own (`ICONS.with({ ... })`). Outlined
4
+ * icons on a 24-unit grid, stroked in currentColor; the brand marks keep their
5
+ * colours. Names say what the picture is, not where it is used.
6
+ */
7
+ import { IconSet } from './IconSet.js';
8
+
9
+ const box = (x, y, width, height, rx = 1) => ['rect', { x, y, width, height, rx }];
10
+ const path = (d) => ['path', { d }];
11
+
12
+ export const ICON_DEFS = {
13
+ dashboard: { viewBox: '0 0 24 24', shapes: [box(3, 3, 7, 9), box(14, 3, 7, 5), box(14, 12, 7, 9), box(3, 16, 7, 5)] },
14
+ zap: { viewBox: '0 0 24 24', shapes: [path('m13 2-2 10h8L11 22l2-10H5L13 2z')] },
15
+ clock: { viewBox: '0 0 24 24', shapes: [['circle', { cx: 12, cy: 12, r: 10 }], ['polyline', { points: '12 6 12 12 16 14' }]] },
16
+ 'clipboard-list': { viewBox: '0 0 24 24', shapes: [
17
+ box(8, 2, 8, 4),
18
+ path('M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'),
19
+ path('M12 11h4'), path('M12 16h4'), path('M8 11h.01'), path('M8 16h.01'),
20
+ ] },
21
+ book: { viewBox: '0 0 24 24', shapes: [
22
+ path('M4 19.5A2.5 2.5 0 0 1 6.5 17H20'),
23
+ path('M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z'),
24
+ ] },
25
+ cpu: { viewBox: '0 0 24 24', shapes: [
26
+ box(4, 4, 16, 16, 2), box(9, 9, 6, 6),
27
+ ...['M15 2v2', 'M15 20v2', 'M2 15h2', 'M2 9h2', 'M20 15h2', 'M20 9h2', 'M9 2v2', 'M9 20v2'].map(path),
28
+ ] },
29
+ memory: { viewBox: '0 0 24 24', shapes: [
30
+ path('M22 12s-3 1-5 1-3-1-5-1-3 1-5 1-5-1-5-1'), path('M7 21h10'), path('M9 21V10'), path('M15 21V10'),
31
+ path('M8 3h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z'),
32
+ ] },
33
+ activity: { viewBox: '0 0 24 24', shapes: [['polyline', { points: '22 12 18 12 15 21 9 3 6 12 2 12' }]] },
34
+ database: { viewBox: '0 0 24 24', shapes: [
35
+ ['ellipse', { cx: 12, cy: 5, rx: 9, ry: 3 }],
36
+ path('M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5'),
37
+ path('M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3'),
38
+ ] },
39
+ google: { viewBox: '0 0 24 24', paint: 'none', shapes: [
40
+ ['path', { fill: '#4285F4', d: 'M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z' }],
41
+ ['path', { fill: '#34A853', d: 'M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z' }],
42
+ ['path', { fill: '#FBBC05', d: 'M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z' }],
43
+ ['path', { fill: '#EA4335', d: 'M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z' }],
44
+ ] },
45
+ microsoft: { viewBox: '0 0 21 21', paint: 'none', shapes: [
46
+ ['rect', { x: 1, y: 1, width: 9, height: 9, fill: '#F25022' }],
47
+ ['rect', { x: 11, y: 1, width: 9, height: 9, fill: '#7FBA00' }],
48
+ ['rect', { x: 1, y: 11, width: 9, height: 9, fill: '#00A4EF' }],
49
+ ['rect', { x: 11, y: 11, width: 9, height: 9, fill: '#FFB900' }],
50
+ ] },
51
+ };
52
+
53
+ /** The built-in set. */
54
+ export const ICONS = new IconSet(ICON_DEFS);
55
+
56
+ /** A built-in icon as an el() spec — `ICONS.spec(name, options)`. */
57
+ export const icon = (name, options) => ICONS.spec(name, options);
58
+
59
+ export { IconSet };
package/src/index.js CHANGED
@@ -33,6 +33,8 @@ import { LagPlotChart } from './charting/charts/LagPlotChart.js';
33
33
  import { TreemapChart } from './charting/charts/TreemapChart.js';
34
34
  import { SunburstChart } from './charting/charts/SunburstChart.js';
35
35
  import { OutlineChart } from './charting/charts/OutlineChart.js';
36
+ import { TableChart } from './charting/charts/TableChart.js';
37
+ import { ChartChooser } from './charting/ChartChooser.js';
36
38
  import { RadarChart } from './charting/charts/RadarChart.js';
37
39
  import { FunnelChart } from './charting/charts/FunnelChart.js';
38
40
  import { WaterfallChart } from './charting/charts/WaterfallChart.js';
@@ -96,7 +98,7 @@ import { ArchimateSpecializationRegistry } from './archimateSpecializations.js';
96
98
  import { ArchimateSpecializationSelector } from './charting/ArchimateSpecializationSelector.js';
97
99
  import { ImageSelector } from './charting/ImageSelector.js';
98
100
  import { CheckboxTreeSelector } from './charting/CheckboxTreeSelector.js';
99
- import { SettingsManager, settingsManager } from './charting/SettingsManager.js';
101
+ import { SettingsManager, settingsManager, TREE_SETTINGS_PROPERTIES } from './charting/SettingsManager.js';
100
102
  import { showGlobalSettingsDialog } from './charting/SettingsDialog.js';
101
103
  import { ArchimateModel, ArchimateView } from './charting/diagrams/ArchimateModel.js';
102
104
  import { CanvasView } from './charting/diagrams/CanvasView.js';
@@ -144,6 +146,7 @@ import { PopupMenu } from './charting/PopupMenu.js';
144
146
  import { ViewTabStrip } from './charting/ViewTabStrip.js';
145
147
  import { EditorShell } from './workspace/EditorShell.js';
146
148
  import { FormDialog } from './charting/FormDialog.js';
149
+ import { Dialog } from './charting/Dialog.js';
147
150
  import { ColorField } from './charting/ColorField.js';
148
151
  import { TextStyle } from './charting/TextStyle.js';
149
152
  import { ArchimateModelChecker } from './charting/diagrams/ArchimateModelChecker.js';
@@ -158,6 +161,10 @@ import { PropertiesPanel } from './charting/PropertiesPanel.js';
158
161
  import { MenuBar } from './charting/MenuBar.js';
159
162
  import { ArchimateDiagramEditor } from './charting/ArchimateDiagramEditor.js';
160
163
  import { ModelTree } from './charting/modelTree/ModelTree.js';
164
+ import { TreeBuilder } from './charting/modelTree/TreeBuilder.js';
165
+ import { ContentSource } from './charting/content/ContentSource.js';
166
+ import { ContentSources } from './charting/content/ContentSources.js';
167
+ import { DUPLICATE_OPTIONS, NOISE_WORDS, FILLER_WORDS, duplicateGroups, duplicateKey, wordKey, nameKey } from './charting/diagrams/duplicates.js';
161
168
  import { TreeModeRegistry } from './charting/modelTree/TreeModeRegistry.js';
162
169
  import { TREE_MODE_CATALOG, treeModeRegistry, getTreeModeCatalogEntry } from './treeModeCatalog.js';
163
170
  import { buildModelTreePanel } from './charting/modelTree/ModelTreePanel.js';
@@ -210,20 +217,21 @@ import {
210
217
  // separate browser-only entry: import 'renderengine/src/workspace.js' directly.
211
218
 
212
219
  export {
213
- Mat4, OrbitCamera, CHART_CATALOG, getChartCatalogEntry, chartRegistry,
220
+ Mat4, OrbitCamera, CHART_CATALOG, getChartCatalogEntry, chartRegistry, ChartChooser, TableChart,
214
221
  DIAGRAM_CATALOG, getDiagramCatalogEntry, diagramRegistry,
215
222
  TREE_MODE_CATALOG, treeModeRegistry, getTreeModeCatalogEntry,
216
223
  AXIS_KIND, ENCODING_CHANNEL, COORD_SYSTEM, COLOR_MODE, CAMERA_MODE,
217
224
  Command, CompositeCommand, CommandHistory,
218
- DiagramEditor, DiagramEditorState, Template, PolygonTemplate, RegularPolygonTemplate, RectangleTemplate, RoundedRectangleTemplate, StadiumTemplate, PredefinedProcessTemplate, ClassTemplate, ComponentTemplate, SwimlaneTemplate, CircleTemplate, FinalStateTemplate, EllipseTemplate, DiamondTemplate, TriangleTemplate, PentagonTemplate, HexagonTemplate, OctagonTemplate, StarTemplate, ParallelogramTemplate, TrapezoidTemplate, ManualInputTemplate, CylinderTemplate, DocumentTemplate, NoteTemplate, CloudTemplate, ActorTemplate, PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate, TemplateCatalog, Palette, PropertiesPanel, MenuBar, MoveElementCommand, BringToFrontCommand, SendToBackCommand, RaiseCommand, LowerCommand, CreateTemplateCommand, EditPropertyCommand, ResizeCommand, RotateCommand, SetPropertiesCommand, ReorderCommand, DIAGRAM_TYPES, getTemplatesForDiagramType, getAllDiagramTypes, getDiagramTypesByGroup, EdgeMarker, ArrowMarker, DiamondMarker, CircleMarker, CrowsFootMarker, HollowDiamondMarker, HollowTriangleMarker, OpenArrowMarker, HalfArrowMarker, EDGE_MARKERS, getEdgeMarker, Edge, AddEdgeCommand, DeleteEdgeCommand, ReconnectEdgeCommand, EDGE_STYLE_PROPERTIES, LINE_ONLY_PROPERTIES, LINE_STYLE_DASH, EdgeStyleCascade, SetStyleCommand, EdgeStyleDialog, DiagramModel, DiagramView, ViewSetModel, ViewMenu, ViewPropertiesDialog, SettingsViewPropertiesDialog, DiagramViewPropertiesDialog, inheritableFields, readInheritableFields, LocalJsonStore, PopupMenu, ViewTabStrip, EditorShell, FormDialog, ToolbarItem, ToolbarButton, ToolbarMenu, ToolbarSelect, ToolbarCheckbox, ToolbarText, ToolbarSeparator, pickFile, reorderedZ, stackingOrder, MoveWaypointCommand, AddWaypointCommand, DeleteWaypointCommand,
225
+ DiagramEditor, DiagramEditorState, Template, PolygonTemplate, RegularPolygonTemplate, RectangleTemplate, RoundedRectangleTemplate, StadiumTemplate, PredefinedProcessTemplate, ClassTemplate, ComponentTemplate, SwimlaneTemplate, CircleTemplate, FinalStateTemplate, EllipseTemplate, DiamondTemplate, TriangleTemplate, PentagonTemplate, HexagonTemplate, OctagonTemplate, StarTemplate, ParallelogramTemplate, TrapezoidTemplate, ManualInputTemplate, CylinderTemplate, DocumentTemplate, NoteTemplate, CloudTemplate, ActorTemplate, PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate, TemplateCatalog, Palette, PropertiesPanel, MenuBar, MoveElementCommand, BringToFrontCommand, SendToBackCommand, RaiseCommand, LowerCommand, CreateTemplateCommand, EditPropertyCommand, ResizeCommand, RotateCommand, SetPropertiesCommand, ReorderCommand, DIAGRAM_TYPES, getTemplatesForDiagramType, getAllDiagramTypes, getDiagramTypesByGroup, EdgeMarker, ArrowMarker, DiamondMarker, CircleMarker, CrowsFootMarker, HollowDiamondMarker, HollowTriangleMarker, OpenArrowMarker, HalfArrowMarker, EDGE_MARKERS, getEdgeMarker, Edge, AddEdgeCommand, DeleteEdgeCommand, ReconnectEdgeCommand, EDGE_STYLE_PROPERTIES, LINE_ONLY_PROPERTIES, LINE_STYLE_DASH, EdgeStyleCascade, SetStyleCommand, EdgeStyleDialog, DiagramModel, DiagramView, ViewSetModel, ViewMenu, ViewPropertiesDialog, SettingsViewPropertiesDialog, DiagramViewPropertiesDialog, inheritableFields, readInheritableFields, LocalJsonStore, PopupMenu, ViewTabStrip, EditorShell, Dialog, FormDialog, ToolbarItem, ToolbarButton, ToolbarMenu, ToolbarSelect, ToolbarCheckbox, ToolbarText, ToolbarSeparator, pickFile, reorderedZ, stackingOrder, MoveWaypointCommand, AddWaypointCommand, DeleteWaypointCommand,
219
226
  ebnfAstToDiagram, parseEbnf,
220
227
  createDataWindow, GraphDataWindow,
221
228
  ArchimateNotation, ArchimateLayer, ArchimateElementType, ArchimateRelationshipType,
222
229
  Glyph, GLYPHS, glyphFor, drawGlyph, glyphElement, glyphToSvg, GlyphTemplate, ArchimateElementTemplate, ArchimateEdge, ArchimateCanvasEditor, ArchimatePalette, KIND_PREFERENCE, ARCHIMATE_TYPE_MIME,
223
230
  ArchimateModel, ArchimateView, CanvasView, PlaceItemsCommand, RemoveLineCommand, CanvasBoxKind, CANVAS_BOX_KINDS, canvasBoxKind, canvasBoxMenuItems, ArchiHint, HintPanel, EditHintCommand, hintMenuItems, CanvasPalette, CANVAS_BOX_MIME, ViewLineEdge, Arrangement, ItemGroup, BackgroundGroup, GroupCommand, UngroupCommand, LockCommand, BackgroundCommand, arrangeMenuItems, TemplateLibrary, CanvasTemplateLibrary, CanvasTemplateMenu, DEFAULT_CANVAS_TEMPLATES, canvasTemplate, EditViewItemCommand, viewItemChanges, ColorField, TextStyle, ArchimateModelChecker, ViewSetChecker, ViewFit, archiElementType, NEAREST_ARCHI_TYPE, ElementInstance, ViewObjectInstance, RelationshipInstance, ArchimateDiagramEditor,
224
231
  ITIconNotation, ITElementType,
225
- ArchimateValidator, ArchimateSpecializationSelector, ImageSelector, CheckboxTreeSelector, SettingsManager, settingsManager,
226
- ModelTree, TreeModeRegistry,
232
+ ArchimateValidator, ArchimateSpecializationSelector, ImageSelector, CheckboxTreeSelector, SettingsManager, settingsManager, TREE_SETTINGS_PROPERTIES,
233
+ ModelTree, TreeBuilder, TreeModeRegistry, ContentSource, ContentSources,
234
+ DUPLICATE_OPTIONS, NOISE_WORDS, FILLER_WORDS, duplicateGroups, duplicateKey, wordKey, nameKey,
227
235
  RadialLayout, DerivedRelationships, EdgeGeometry,
228
236
  buildModelTreePanel, buildAnalysisPanel, buildAnalysisData, buildTypeCategoryTree, buildPropertyTree, DiagramTabs,
229
237
  DataModelNotation, DataModelLevel, AttributedLevel, ConceptualLevel, LogicalLevel, PhysicalLevel, NotationStyle, RelationshipKind, AssociationKind, GeneralizationKind, EntityRole, FactRole, DimensionRole,
@@ -251,6 +259,11 @@ export {
251
259
  * @class RenderEngine
252
260
  * This class decides which renderer to use and provides a unified drawing API.
253
261
  */
262
+ // Navigation menus (a sidebar's): DOM built from JSON, safe to import under node.
263
+ export { NavEntry, NavItem, NavGroup, NavSection, NavMenu } from './workspace/nav/index.js';
264
+ // Named icons as data (an <svg> spec each); the built-in pool and its class.
265
+ export { IconSet, ICONS, ICON_DEFS, icon } from './icons/index.js';
266
+
254
267
  export class RenderEngine {
255
268
  /** Layout constants for graphDiagram's `shape: 'table'` node type. */
256
269
  static TABLE_NODE = NodeGeometry.TABLE_NODE;
@@ -439,8 +452,12 @@ export class RenderEngine {
439
452
  * @param {HTMLElement} container The container element for the chart.
440
453
  * @param {object} [options]
441
454
  * @param {'canvas' | 'svg'} [options.rendererType='canvas']
455
+ * @param {false|{wheelModifier?: 'ctrl'|'alt'|'shift'}} [options.zoom] - SVG wheel zoom: off, or only
456
+ * with a modifier held (so a plain wheel scrolls the page); on, with a plain wheel, by default
442
457
  */
443
458
  constructor(container, options = {}) {
459
+ // Kept: the engines refreshChart() makes for other containers are made with the same options.
460
+ this.options = options;
444
461
  if (container) {
445
462
  const { rendererType = 'canvas' } = options;
446
463
  const width = container.clientWidth;
@@ -448,11 +465,9 @@ export class RenderEngine {
448
465
 
449
466
  this.container = container; // Store container for event listeners
450
467
  this.renderer = RendererFactory.create(rendererType, container, width, height);
451
- } else {
452
- // Allow initialization without a container for testing
453
- this.options = options;
454
- console.log('RenderEngine initialized in test mode.');
455
468
  }
469
+ // No container: a test, or an engine keeping a page of registered
470
+ // charts, each drawn in its own element (refreshChart).
456
471
 
457
472
  if (this.container) {
458
473
  this._interactions = new InteractionManager(this);
@@ -1404,6 +1419,24 @@ export class RenderEngine {
1404
1419
  this._renderChart(new FunnelChart(this.renderer, data, options));
1405
1420
  }
1406
1421
 
1422
+ /**
1423
+ * Renders rows as a table — a header of column names, a row per record, numeric columns
1424
+ * right-aligned, values shown exactly. Drawn with the renderer's primitives, so it works under
1425
+ * SVG and canvas. ChartChooser offers it for any result, last.
1426
+ * @param {Array<object>} data one object per row
1427
+ * @param {object} [options]
1428
+ * @param {Array<string|{key:string, label?:string, align?:'start'|'end'}>} [options.columns] every key of the first row when absent
1429
+ * @param {number} [options.maxRows] rows beyond it are counted, not drawn
1430
+ * @param {string} [options.nullText] what a null or missing value shows ('—')
1431
+ * @param {string} [options.title]
1432
+ */
1433
+ tableChart(data, options = {}) {
1434
+ if (!this.renderer) return;
1435
+ this._chart3D = false;
1436
+ this._chartState = { type: 'table', data, options };
1437
+ this._renderChart(new TableChart(this.renderer, data, options));
1438
+ }
1439
+
1407
1440
  /**
1408
1441
  * Renders a waterfall chart — running-total bars, each floating from the
1409
1442
  * previous cumulative value to the new one (a "delta" bar), except
@@ -2588,7 +2621,10 @@ export class RenderEngine {
2588
2621
 
2589
2622
  /**
2590
2623
  * Refreshes a registered chart, re-resolving its data and re-invoking
2591
- * its render method.
2624
+ * its render method, in the element with that id: this engine's own
2625
+ * container, or — so one engine can keep a page of charts — an engine of
2626
+ * its own for that element, made with this engine's options and kept
2627
+ * (so a zoom survives the next refresh) while the element is the same.
2592
2628
  * @param {string} id Chart container ID
2593
2629
  */
2594
2630
  refreshChart(id) {
@@ -2596,7 +2632,46 @@ export class RenderEngine {
2596
2632
  const config = this.registry[id];
2597
2633
  const container = document.getElementById(id);
2598
2634
  if (!container) return;
2635
+ const target = this._engineFor(id, container);
2636
+ if (target !== this) target.fitToContainer();
2637
+ target._drawRegistered(config);
2638
+ }
2639
+
2640
+ /** Every registered chart, refreshed. */
2641
+ refreshAll() {
2642
+ for (const id of Object.keys(this.registry ?? {})) this.refreshChart(id);
2643
+ }
2644
+
2645
+ /**
2646
+ * Set the data registered charts read by `dataPath`, and refresh them all —
2647
+ * a live page's poll: `engine.setData(await fetchMetrics())`.
2648
+ * @param {object} data
2649
+ */
2650
+ setData(data) {
2651
+ this.setGlobalData(data);
2652
+ this.refreshAll();
2653
+ }
2654
+
2655
+ /** The engine that draws into `container` for registered chart `id`. @private */
2656
+ _engineFor(id, container) {
2657
+ if (container === this.container) return this;
2658
+ this._childEngines ??= new Map();
2659
+ let engine = this._childEngines.get(id);
2660
+ if (engine?.container !== container) {
2661
+ engine = this._newEngine(container);
2662
+ this._childEngines.set(id, engine);
2663
+ }
2664
+ engine.globalData = this.globalData;
2665
+ return engine;
2666
+ }
2667
+
2668
+ /** An engine for another container, with this one's options. @private */
2669
+ _newEngine(container) {
2670
+ return new RenderEngine(container, this.options);
2671
+ }
2599
2672
 
2673
+ /** Draw a registration (`{type, options}`) with this engine. @private */
2674
+ _drawRegistered(config) {
2600
2675
  // Catalog keys ('bar', 'heatmap', 'graph', 'archimate', ...) resolve
2601
2676
  // through the registries to their real method name; railroadDiagram
2602
2677
  // isn't a registry entry, so `type` already IS the method name there.
@@ -2613,6 +2688,7 @@ export class RenderEngine {
2613
2688
  this[methodName](config.options.nodes, config.options.edges || [], config.options);
2614
2689
  } else {
2615
2690
  const data = config.options.data || this.resolvePath(config.options.dataPath, config.options.dataMapper);
2691
+ if (data === undefined) return; // registered before its data has arrived
2616
2692
  this[methodName](data, config.options);
2617
2693
  }
2618
2694
  }