@peter.prib/renderengine 0.2.2 → 0.2.4

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/REFERENCE.md CHANGED
@@ -1,2205 +1,2227 @@
1
- # RenderEngine — Reference
2
-
3
- One document for "what can I call and what does it take" — every chart/diagram entry point,
4
- every shared option, every utility, and (the thing none of the older docs cover) the pattern for
5
- feeding a chart a live, growing-then-capped data source.
6
-
7
- This supersedes `README_COMPLETE.md`, `FEATURES_ADDED.md`, `FRAMEWORK_ALIGNMENT.md`,
8
- `COMPATIBILITY_MATRIX.md`, and `DELIVERY_SUMMARY.md` as the day-to-day API reference — those five
9
- predate most of the current chart-type set (some still say "8 chart types") and are kept only as
10
- historical record; see `DOCUMENTATION_INDEX.md`. For *why* things are built the way they are —
11
- the chart-type catalog pattern, the 3D camera split, per-chart design rationale — see
12
- `ARCHITECTURE.md` and `CHART_TYPES_ROADMAP.md`. This doc is the *what/how*, not the *why*.
13
-
14
- ## Setup
15
-
16
- ```javascript
17
- import { RenderEngine } from 'renderengine'; // or dist/renderengine.umd.js in a browser
18
-
19
- const engine = new RenderEngine(document.getElementById('chart'), { rendererType: 'canvas' }); // or 'svg'
20
- const headless = new RenderEngine(null); // test mode — every chart call no-ops safely
21
- ```
22
-
23
- - **Canvas** — higher raw point-count ceiling, no native DOM hit-testing (`hitTest()` does the
24
- work manually).
25
- - **SVG** — native tooltips/hit-testing, crisp at any zoom, automatic wheel-zoom and
26
- drag-empty-space-to-pan on every SVG-rendered chart (`resetView()` clears it back to identity;
27
- a 3D-orbit chart's own drag-to-rotate/wheel-to-zoom takes over instead — see below).
28
- - Both renderers sit behind one primitive API (`rect`/`circle`/`path`/`text`/`line`/…) — every
29
- `Chart` subclass is renderer-agnostic, so any `xChart()` call below works unchanged under either.
30
- - **Size.** The renderer takes the container's size once, when the engine is made. Call
31
- `engine.fitToContainer()` before drawing in a container that may since have been resized or
32
- was hidden then (it measured 0×0): it re-measures and resizes the renderer, returning whether
33
- the size changed, and does nothing while the container is still 0×0. An SVG diagram that set
34
- its own viewBox keeps it and scales into the new box; a canvas is cleared, so draw after.
35
- Without it, a drawing made in a narrower or hidden pane stayed that size — clipped, or blank
36
- until the page was reloaded.
37
-
38
- ## The rendering model (read this before the streaming section)
39
-
40
- Every chart call — `barChart`, `lineChart`, all of them — starts with `this.renderer.clear()`
41
- (via the `Chart.render()` base method) and redraws the entire chart from the `data` array you pass
42
- in. There is no incremental/diff update, no "append one point" method, no built-in windowing.
43
- Calling the same method again on the same container **replaces** what was there. This is deliberate
44
- (it's what keeps every one of the 28 chart types simple and renderer-agnostic) but it means
45
- "streaming"/"bounded window" behavior is entirely a caller-side pattern: keep your own array as the
46
- source of truth, mutate *that*, then re-call the chart method. See "Dynamically adding data points
47
- in a bounded window" below for the concrete pattern, straight from a real working implementation
48
- in this repo.
49
-
50
- ## Chart types
51
-
52
- `src/chartCatalog.js` exports `CHART_CATALOG` (also reachable as `RenderEngine.CHART_CATALOG`) —
53
- the single source of truth for this table; build a type-picker from it rather than hand-copying
54
- this list (see `ARCHITECTURE.md`). `engine.renderChartType(key, data, options)` dispatches by
55
- catalog `key` for you.
56
-
57
- | Key | Method | Data shape | Notable options |
58
- |---|---|---|---|
59
- | `bar` | `barChart(data, opts)` | `Array<{label, value}>` | `colors`, `showLegend`, `barColor` — one of the types with numeric y-axis ticks/gridlines, see note below |
60
- | `groupedBar` | `groupedBarChart(data, opts)` | multi-series `Array<{name, values:Array<{label,value}>}>` | `colors`, `legend`, `groupPadding`, `barPadding` |
61
- | `line` | `lineChart(data, opts)` | `Array<{label, value, error?, errorPlus?, errorMinus?}>` or multi-series `Array<{name, values:[{label,value,error?,...}]}>` | `colors`, `legend`, `zAxis` (isometric depth per series); per-point `error` / `errorPlus` / `errorMinus` draw vertical error bars |
62
- | `pie` | `pieChart(data, opts)` | `Array<{label, value}>` | `innerRadiusRatio` (>0 = doughnut), `colors`, `legend` |
63
- | `donut` | `donutChart(data, opts)` | `Array<{label, value}>` | same as `pie` with `innerRadiusRatio` defaulting to 0.5 |
64
- | `area` | `areaChart(data, opts)` | same as `line` | `stacked`, `colors`, `legend` |
65
- | `stackedArea` | `stackedAreaChart(data, opts)` | multi-series `Array<{name, values:Array<{label,value}>}>` | same as `area` with `stacked:true` forced |
66
- | `scatter` | `scatterChart(data, opts)` | `Array<{label, x, y, errorX?, errorY?, errorXMinus?, errorXPlus?, errorYMinus?, errorYPlus?}>` | `colorCol` (color-group by field, or a compiled formula — see below), `referenceLine`; symmetric or asymmetric x/y error bars |
67
- | `bubble` | `bubbleChart(data, opts)` | `Array<{label, x, y, z}>` | `z` drives size; `colorCol`; `zAxis` (isometric depth) |
68
- | `heatmap` | `heatmap(data, opts)` | `Array<{x, y, value}>` | `colorScale` (low→high stops), `xLabels`/`yLabels` |
69
- | `calendarHeatmap` | `calendarHeatmap(data, opts)` | `Array<{date: string\|Date, value: number}>` | `colorScale`, `emptyColor`; one row per calendar day |
70
- | `gantt` | `ganttChart(data, opts)` | `Array<{label, start, end, progress?, category?, color?}>` | `start`/`end` accept Date/ISO/number; `progress` 0–100 |
71
- | `streamGraph` | `streamGraph(data, opts)` | multi-series `Array<{name, values:Array<{label,value}>}>` | silhouette baseline; `colors`, `legend` |
72
- | `radialBar` | `radialBarChart(data, opts)` | `Array<{label, value}>` | angular categories with radial bar length; `colors`, `legend` |
73
- | `populationPyramid` | `populationPyramid(data, opts)` | `Array<{label, left, right}>` | back-to-back horizontal bars; `leftColor`, `rightColor` |
74
- | `sankey` | `sankeyDiagram(data, opts)` | `{nodes?: Array<{id,label?,color?}>, links: Array<{source,target,value}>}` | proportional-width flow links; `nodeWidth`, `linkOpacity` |
75
- | `chord` | `chordDiagram(data, opts)` | same as `sankey` | nodes on a circle with curved chords of proportional thickness |
76
- | `candlestick` | `candlestick(data, opts)` | `Array<{label, open, high, low, close}>` | `upColor`, `downColor`, `markers` (tooltip annotations) |
77
- | `stackedBar` | `stackedBarChart(data, opts)` | `Array<{label, values:number[]}>` | `colors` (no built-in legend — a known gap) |
78
- | `pipe` | `pipeChart(data, opts)` | `Array<{label, value, metric?}>` or multi-series | `metric` tapers line width; `legend` |
79
- | `radar` | `radarChart(data, opts)` | `Array<{axis, value}>` or multi-series | `colors`, `legend` |
80
- | `funnel` | `funnelChart(data, opts)` | `Array<{label, value}>` (largest first) | `colors` |
81
- | `waterfall` | `waterfallChart(data, opts)` | `Array<{label, value, type?:'delta'|'total'}>` | `upColor`, `downColor`, `totalColor` |
82
- | `histogram` | `histogram(values, opts)` | `number[]` (raw sample, binned internally) | bin count derived, not caller-set |
83
- | `boxPlot` | `boxPlot(data, opts)` | `Array<{label, values:number[]}>` (raw per-group sample) | quartiles/whiskers computed internally |
84
- | `density` | `densityChart(data, opts)` | same as `boxPlot` | Gaussian KDE overlay per group; `legend` |
85
- | `violin` | `violinChart(data, opts)` | same as `boxPlot` | mirrored KDE silhouette per group |
86
- | `qq` | `qqPlot(values, opts)` | `number[]` | delegates to `scatterChart`; fitted reference line |
87
- | `lag` | `lagPlot(values, opts)` | `number[]` | `lag` (default 1); delegates to `scatterChart` |
88
- | `treemap` | `treemapChart(data, opts)` | `Array<{label, value?, children?}>` (hierarchical) | squarified layout, palette by top-level branch |
89
- | `sunburst` | `sunburstChart(data, opts)` | same hierarchical shape as `treemap` | nested rings by depth |
90
- | `outline` | `outlineChart(data, opts)` | same hierarchical shape as `treemap`/`sunburst` | `onNodeClick`; collapsible indented list |
91
- | `gauge-dial` | `dialGauge(value, opts)` | `number`, `{value, target?}`, or `[{value, label?, color?}]` for a needle each (0–100 unless `min`/`max`) | the gauge options below; `cx`, `cy`, `r`, `thickness`, `startAngle`, `sweep`, `needleColors`; several needles get a legend, the first fills the arc |
92
- | `gauge-bar` | `barGauge(value, opts)` | `number` or `{value, target?}` (0–100 unless `min`/`max`) | the gauge options below; `x`, `y`, `width`, `height`, `orientation: 'vertical'` |
93
- | `gauge-arc` | `arcGauge(value, opts)` | `number` or `{value, target?}` | the gauge options below; `cx`, `cy`, `r`, `thickness`, `startAngle`, `sweep` (a half circle by default), `cap` |
94
- | `gauge-thermometer` | `thermometerGauge(value, opts)` | `number` or `{value, target?}` | the gauge options below; `x`, `y`, `width`, `height` (the tube), `bulb` (its radius) |
95
- | `gauge-ring` | `ringGauge(value, opts)` | `number` or `{value, target?}` | the gauge options below; a whole circle from the top, the value in the middle |
96
- | `gauge-rings` | `multiRingGauge(values, opts)` | numbers or `{value, label?, color?, min?, max?}`, outer first | one ring per value on its own scale; `gap`, `colors`, `legend` |
97
- | `kpi-tile` | `kpiTile(data, opts)` | `number` or `{value?, previous?, history?:number[]}` | the figure (grouped), its change (`change`: `'percent'` or `'value'`; `upIsGood`), a sparkline of the history; `x`, `y`, `width`, `height`, `bands`, `sparkColor`, `background` |
98
- | `bullet` | `bulletGauge(data, opts)` | `number` or `{value, target?, ranges?:Array<{max,label?,color?}>}` | the gauge options below; `ranges` (its name for `bands`), `barColor`, `rangeColors`, `showLabels` |
99
- | `bar3D` | `barChart3D(data, opts)` | `Array<{label, value, depth?}>` | `depthScale`; fixed-angle isometric, not a real camera |
100
- | `bubble3D` | `bubbleChart3D(data, opts)` | `Array<{label, x, y, z, size?, category?}>` | real orbit camera — drag to rotate, wheel to zoom |
101
- | `bar3DOrbit` | `barChart3DOrbit(data, opts)` | `Array<{x, z, value}>` (grid) | real orbit camera |
102
- | `stackedBar3DOrbit` | `stackedBarChart3DOrbit(data, opts)` | `Array<{x, z, values:number[]}>` (grid) | real orbit camera |
103
- | `wordCloud` | `wordCloud(data, opts)` | `Array<{text: string, value: number}>` | spiral placement + bounding-box collision; `minFontSize`, `maxFontSize`, `padding` |
104
- | `parallelCoordinates` | `parallelCoordinates(data, opts)` | `Array<{label?, category?, [dim: number]}>` | one vertical axis per numeric dimension; category-driven colors; `dimensions` |
105
- | `geoMap` | `geoMap(data, opts)` | `{features: Array<{id, name, value?, color?, geometry}>, bubbles?: Array<{lat, lon, value}>}` | equirectangular projection; `center`, `scale`, `colorScale`; see note below |
106
-
107
- **Gauges.** Every gauge is a `GaugeChart` (`src/charting/charts/GaugeChart.js`) and takes the same options:
108
-
109
- | Option | Meaning |
110
- |---|---|
111
- | `min`, `max` | The scale (0 and 100). A 0–100 scale reads as a percentage ("75.0%"), any other as its number |
112
- | `format`, `units` | How numbers read: `(v) => string`, or `{decimals, prefix, suffix, grouping, compact}` ("$1,234.5", "12.3K"); `units` is a suffix ("120.0 ms") |
113
- | `bands` | `[{to, color?, label?}]` in order, each from where the one before ended; drawn lighter behind the value, and the value takes its band's colour. `bandColors` colours the bands without their own |
114
- | `color` | The value's colour (wins over its band's): one, a gradient array of colours or `{stop, color}`, or `(percent) => colour` |
115
- | `target`, `targetColor` | A marker across the track at a value (or the data's `target`) |
116
- | `ticks` | That many equal steps, marked outside the track and labelled with only the decimals a step needs |
117
- | `segments`, `segmentGap` | The track as that many blocks, `segmentGap` pixels apart (2), lit up to the value (LED style); with bands each lit block takes its own band's colour, a level meter |
118
- | `animate` | `true` (400 ms) or milliseconds: drawing the same type of gauge again on the same engine eases from the value it shows to the new one (the value, target and each of several values; ease-out). A newer value stops an older animation and starts from where it got to |
119
- | `title` | |
120
-
121
- ```js
122
- engine.dialGauge(72, { min: 0, max: 120, units: 'km/h', ticks: 6, target: 100,
123
- bands: [{ to: 80, color: '#6bcb77' }, { to: 100, color: '#ffd93d' }, { to: 120, color: '#ff6b6b' }] });
124
- engine.barGauge({ value: 40, target: 55 }, { orientation: 'vertical', ticks: 4 });
125
- engine.ringGauge({ value: 380 }, { max: 500, format: { decimals: 0, suffix: ' / 500' } });
126
- engine.multiRingGauge([{ label: 'Move', value: 420, max: 600 }, { label: 'Stand', value: 9, max: 12 }]);
127
- engine.kpiTile({ history: [182000, 191200, 214300] }, { title: 'Revenue', format: { prefix: '$', grouping: true } });
128
- engine.dialGauge([{ label: 'Actual', value: 68 }, { label: 'Budget', value: 80 }], { animate: true });
129
- engine.barGauge(84, { segments: 20, bands: [{ to: 70, color: '#2ed573' }, { to: 100, color: '#ff4757' }] });
130
- ```
131
-
132
- The pieces are in `src/charting/gauges/`: `GaugeScale` (range, fractions, ticks, formatting), `GaugeBands`
133
- (the bands and which one a value is in), `GaugeValue` (value, target, previous, history, several values) and the
134
- tracks a value moves along, `HorizontalTrack`, `VerticalTrack` (both `LinearTrack`s) and `ArcTrack` (any start
135
- angle and sweep, a full ring included), `Sparkline` (a series fitted to a box) and `GaugeAnimation` (frames from `GaugeValue.between(from, to, t)` on an
136
- injectable clock). A new gauge picks a track and
137
- draws with `drawBackground`, `drawValue`, `drawTarget` and `drawTicks`; its `static LAYOUT` gives its default place
138
- and size (`cx`, `cy`, `r`, ...), which the options of the same names override. `ArcGauge` → `RingGauge` is the
139
- pattern: the ring only changes the layout and where the labels go.
140
-
141
- **`geoMap` note:** the `test/charts.html` demo uses deliberately rough, hand-drawn continental
142
- polygons so the page needs no external map file. Real use should pass proper GeoJSON-like
143
- `Polygon`/`MultiPolygon` features; the engine itself has no bundled world geometry and makes no
144
- external network requests.
145
-
146
- **Not in `CHART_CATALOG`** (diagram renderers invoked directly, not "chart a tabular result"
147
- types — see `ARCHITECTURE.md`):
148
-
149
- - `graphDiagram(nodes, edges, opts)` — force-directed node/edge diagram. `nodes:[{id,label,group?,shape?,size?}]`,
150
- `edges:[{source,target,label?,weight?}]`. Key options: `onNodeClick`, `onEdgeClick`, `editable` (drag-to-connect,
151
- right-click rename/delete/add), `contextMenuItems`/`edgeContextMenuItems`/`canvasContextMenuItems`
152
- (replace the built-in right-click menus; support `{label, items:[...]}` submenus), `colors`, `nodeRadius`.
153
- Dewey-style hierarchical `id`s (`"1.2.3"`) auto-infer parent/child edges in place of `edges`.
154
- **Sizing:** a node's `size` (or, with `sizeBy: 'degree'`, its connection count — no `size` field needed)
155
- scales its radius between `minRadius`/`maxRadius` (`sizeScale: 'linear'|'sqrt'`); an edge's `weight`
156
- scales its stroke width between `minEdgeWidth`/`maxEdgeWidth` (`edgeWidthScale`) the same way. Either
157
- is off by default — nothing changes size until some node/edge actually carries a value (or `sizeBy` is set).
158
- **Edge routing:** `edgeRouting: 'straight'|'curved'|'orthogonal'|'polyline'` sets the diagram-wide default
159
- (`'straight'` unless set); any edge can override it with its own `routing`. `'curved'` is a bezier that
160
- leaves/arrives perpendicular to each node; `'orthogonal'` is a right-angle Manhattan connector;
161
- `'polyline'` is a straight line unless another node sits in the way, in which case it steps around it with
162
- one waypoint. **Ports:** an edge's `sourceSide`/`targetSide` (`'top'|'right'|'bottom'|'left'`, default
163
- `'auto'`) pin where on the node it attaches — that point stays fixed to the node (moves with it) instead of
164
- always sliding around to face whatever it's connected to, matching how `fromCol`/`toCol` table links already
165
- anchor to a fixed side. Table-to-table FK edges (`fromCol`/`toCol` set, both ends `shape:'table'`) keep
166
- their own fixed-column bezier regardless of `routing`/side options. The force-directed *node* layout
167
- (springs pulling connected nodes together, mutual repulsion apart) is what `graphDiagram` already runs by
168
- default — `edgeRouting` only changes how a connector is *drawn* between wherever the two nodes end up.
169
- **Edge selection tolerance:** `edgeHitTolerance` (default 8 SVG units) widens the hit area around edge
170
- lines for `hitTest()`, so thin or angled connections are easier to select, hover, or double-click for
171
- `onEdgeDoubleClick`. The renderer's own pixel-exact hit is tried first; if it misses, the closest edge
172
- within `edgeHitTolerance` is returned.
173
- **Manual routing:** with `editable: true`, right-click an edge and choose "Add Waypoint Here" to drop a
174
- bend point where you clicked (`_insertEdgeWaypoint`/its pure half `_edgeWaypointsWithInsertion`), drag a
175
- bend point to move it, double-click a bend point to remove it — this builds `edge.waypoints` (`[{x,y}, ...]`),
176
- which beats every `routing` style outright and threads the line straight through each point in order. Set
177
- `edge.waypoints` yourself to hand in a route programmatically, or read it back via
178
- `onEdgeWaypointsChange(edge, waypoints)`. (Adding a waypoint used to be double-click-the-line itself — moved
179
- to the context menu once double-click was repurposed for `onEdgeDoubleClick`, see below.) The same edge
180
- context menu also offers **Invert Direction**, which swaps `source`/`target` in place and fires
181
- `onEdgeInvert(edge, oldSource, oldTarget)`.
182
- **Undo/redo:** with `editable: true`, every model edit (move, resize, rename, add/delete node,
183
- add/delete edge, invert edge, waypoint edits) is recorded in a `CommandHistory` bound to the diagram.
184
- Press `Ctrl+Z` / `Ctrl+Y` (or `Ctrl+Shift+Z` for redo) to step through the stack. `historySize` sets
185
- the maximum number of retained edits (default `200`), and `onHistoryChange(canUndo, canRedo)` fires
186
- whenever the stack changes so a UI can enable/disable its own undo/redo buttons.
187
- A host that re-renders the diagram passes its own `history` (a `CommandHistory`): edits are recorded
188
- there, so undo outlives each render, and the host binds the undo keys itself (the diagram's own
189
- `Ctrl+Z` handler stands down; `historySize`/`onHistoryChange` then belong to the host's history).
190
- Recorded commands act on whichever diagram is on screen when they are undone, finding nodes and
191
- edges by `id`. Every apply, undo and redo included, reports the result, so a host's copy stays in
192
- step:
193
-
194
- | Edit | Hook |
195
- |---|---|
196
- | move a node | `onNodeMove(nodeId, {x, y})` |
197
- | resize a node | `onNodeResize(node)` (its `width`/`height`, or `radius` for a round shape) |
198
- | add, move or delete a bend point | `onEdgeWaypointsChange(edge, waypoints)` |
199
- | drag an end to pin its side | `onEdgeSideChange(edge, 'source' \| 'target', side)` (`undefined` = automatic again) |
200
- | draw an edge | `onEdgeAdd(edge)` on add and redo, `onEdgeRemove(edge)` on undo |
201
-
202
- An `onEdgeAdd` handler may give the new edge an `id`; the command finds it by that id after a
203
- re-render.
204
-
205
- ```javascript
206
- const history = new CommandHistory();
207
- const layout = { a: { x: 80, y: 80 }, b: { x: 300, y: 200 } };
208
- const draw = () => engine.graphDiagram(
209
- Object.entries(layout).map(([id, p]) => ({ id, name: id.toUpperCase(), ...p })),
210
- [{ id: 'ab', source: 'a', target: 'b' }],
211
- { editable: true, history, onNodeMove: (id, p) => { layout[id] = p; } },
212
- );
213
- draw();
214
- // ...the user drags A; the page redraws...
215
- draw();
216
- history.undo(); // A goes back on screen, and onNodeMove puts layout.a back too
217
- ```
218
- **Grid:** `gridSize` (number, default `0`) snaps dragged and newly-added nodes to the nearest multiple;
219
- `showGrid` (boolean, default `false`) draws a light grid behind the diagram. Both are plain `graphDiagram`
220
- options, so they work for any editable diagram including ArchiMate. `GraphDiagram.snapToGrid(value, gridSize)`
221
- is the pure helper if you need the same rounding rule elsewhere.
222
- **Double-click:** dispatches to whatever's under the cursor — `onNodeDoubleClick(node)`/
223
- `onEdgeDoubleClick(edge)` fire for a node/edge's line (nothing built-in happens without one; a typical wiring
224
- opens the same dialog the right-click menu's "Show/Edit Properties…" does), `onCanvasDoubleClick(point)`
225
- fires for empty canvas (SVG/world coordinates — no single object there to edit, so this is the natural place
226
- for a diagram/view-level settings dialog instead). Double-clicking an existing waypoint handle still removes
227
- it regardless of these — a distinct, precise gesture on a small routing control point, not "the edge" in
228
- general. Every context-menu action (built-in or caller-supplied, via `contextMenuItems`/
229
- `edgeContextMenuItems`/`canvasContextMenuItems`) now also receives the right-click's SVG/world point as a
230
- third argument, `action(subject, engine, point)`.
231
- **Edge tooltips:** hovering an edge shows a tooltip with its non-structural ("extra") data, the same
232
- "only what isn't already visible on the diagram itself" convention a node's own hover tooltip uses
233
- (`_edgeTooltipText`) — a bare `{source,target}` edge with nothing else set shows no tooltip at all. An
234
- ArchiMate relationship's `kind` (extra data from `_archimateBuildEdge`'s point of view) always shows this
235
- way.
236
- **Manual resize:** `resizable: true` (or a node's own `node.resizable`, which overrides the diagram
237
- default either way) shows a small handle on hover; dragging it sets `node.radius` — a hard override that
238
- beats `sizeBy`/domain sizing, the same way explicit `node.x`/`node.y` already beat computed layout — or,
239
- for a `shape:'table'` node, `node.width` (height still comes from column count). The handle's corner
240
- defaults to `'top-right'`; `resizeCorner` changes it diagram-wide, `resizeCornerByShape: {shape: corner}`
241
- per shape, and a node's own `node.resizeCorner` per node — each level overriding the one before it. See
242
- `onNodeResize(node)`.
243
- **Icons:** a node's `icon` (a short glyph/emoji string) or `iconUrl` (an image/SVG file or `data:` URI)
244
- replaces the plain initials text centred on its shape (`iconPosition: 'center'`, the default) — `iconUrl`
245
- wins if both are set; scales with the node's own radius, so it grows/shrinks along with manual resize.
246
- `iconPosition: 'corner'` (diagram-wide, or a node's own `node.iconPosition`) instead draws it as a small
247
- badge at a corner — same node > `iconCornerByShape` > diagram-wide `iconCorner` precedence as the resize
248
- handle's corner, independently configurable (`archimateDiagram` uses this for its type-icon badge). Neither
249
- mode applies to `shape:'person'` (already draws its own glyph) or `shape:'table'`.
250
- **Box sizing/label position:** a `shape:'rect'` node's own `width`/`height` (either or both) override the
251
- usual square-of-radius box, same idea as a table node's `node.width`. `labelPosition: 'inside'` (diagram-wide
252
- default `'below'`, or a node's own `node.labelPosition`) left-aligns the label inside the box instead of
253
- captioning it underneath, word-wrapped to as many lines as fit the box rather than one overflowing line
254
- (`_wrapLabelLines`, measured with the renderer's `measureText(text, font)` — a 2D canvas context in the SVG and
255
- Canvas renderers, an average-character estimate where there is none), keeping clear of a corner icon in a top
256
- corner — meant for exactly this wider-than-tall case (`archimateDiagram` sets both). A node's own text style:
257
- `textColor`, `fontFamily`, `fontSize` (`"12px"` or `"9pt"`), `fontWeight`, `fontStyle`; `strokeDasharray` dashes
258
- its outline (an ArchiMate Grouping is drawn dashed). `options.edgesOnTop` draws lines over boxes instead of under
259
- them, as ArchiMate tools do (`archimateDiagram` sets it), so a line into an element drawn inside another stays
260
- visible.
261
- **Label alignment:** `textAlign` (`'left'`|`'center'`|`'right'`) and `verticalAlign` (`'top'`|`'middle'`|`'bottom'`)
262
- control the label's placement. Per-node values override diagram-wide `options.textAlign`/`options.verticalAlign`.
263
- Defaults preserve existing behaviour: `labelPosition:'inside'` defaults to top-left; `'below'` defaults to
264
- horizontal centre. `verticalAlign` only affects inside labels.
265
- **Notes / annotations:** a node with `shape:'note'` renders as a yellow sticky note with a folded corner,
266
- suppresses the centred icon/initials, and wraps its label inside. `archimateDiagram` accepts an `annotations`
267
- option (array of `{id?, label/name, x, y, width?, height?}`) — each annotation becomes a note node on the
268
- diagram but is not validated as an ArchiMate element, so notes are saved with the view without affecting the
269
- model. Any plain `graphDiagram` can also use `shape:'note'` directly.
270
- **Importable images:** a node with `shape:'image'` renders its `iconUrl` as an SVG `<image>` filling the node's
271
- `width`/`height` (`preserveAspectRatio` defaults to slice; set `imagePreserveAspectRatio` on the node or
272
- diagram options to change, e.g. `'xMidYMid meet'`). Image nodes suppress the usual centred icon/initials and
273
- caption label. An optional `href` property turns the image into a clickable hyperlink (opened on click).
274
- `test/charts.html` lets users add images via a file picker, drag-and-drop, or paste, and the node Properties
275
- dialog exposes the link field. `RenderEngine.importArchiXML()` maps Archi `DiagramModelImage` elements to
276
- `shape:'image'` and preserves their `iconUrl`/`href`.
277
- **Label resolution** (`_resolveNodeLabel`, used everywhere a label renders — the box/caption, tooltip, table
278
- header, keyword search): `label` may be a plain string (used as-is), a function `(node) => string` (called
279
- at render time, the same "may be a function" convention `contextMenuItems`' own `label` already uses), or a
280
- string with `{field}` placeholders (each replaced with that field's own value off the node — plain
281
- substitution, not a parser, e.g. `'{name} ({type})'`). An empty/unset `label` falls back to `node.name`.
282
- **More arrowheads/line styles:** beyond `'arrow'|'circle'|'tee'|'none'`, `arrowhead` (and a second,
283
- independent `arrowheadStart` for the source end — e.g. a diamond at the "whole" end while the target end
284
- carries none) also accepts `'diamond'|'diamond-hollow'|'triangle-hollow'|'open-arrow'`. `lineStyle:
285
- 'solid'|'dashed'|'dotted'` generalizes the older boolean `dashed` (still honored when `lineStyle` isn't set).
286
- **Node/edge properties:** `showPropertiesDialog(subject, engine, kind, {onSave})` and
287
- `showDiagramPropertiesDialog(engine, {onSave})` are **shipped** (`src/propertiesDialog.js`, exported from the
288
- package root) — wire them to `onNodeDoubleClick`/`onEdgeDoubleClick`/`onCanvasDoubleClick` or a context-menu
289
- item and a diagram becomes editable. They used to exist only as ~400 lines inside `test/charts.html`, which
290
- meant a consumer had to copy a demo to get double-click editing, so nobody did. `onSave(subject, changedKeys)`
291
- fires after the subject is updated and the diagram re-rendered — that's where a consumer persists. The
292
- decisions are pure and exported separately for reuse or testing: `propertyRowsFor` (which fields, which
293
- section, which control), `coerceFieldValue` (read a string back as the type the field held), `derivedRefreshPlan`
294
- (may a derived colour/icon be refreshed silently), `LAYER_CHOICES`, `NODE_SECTIONS`.
295
- **Typed user-defined properties:** nodes and edges may carry `properties: [{name, type, value}]` where `type`
296
- is `'string'`|`'integer'`|`'boolean'`|`'date'`. The Properties dialog shows these in their own section, lets
297
- the user add/remove/edit them, and coerces values by type. `RenderEngine.NODE_STRUCTURAL_KEYS` and
298
- `EDGE_STRUCTURAL_KEYS` include `'properties'` so they don't appear as untyped "extra data" tooltip lines.
299
- A host that adds its own bookkeeping fields to the nodes and edges it hands in lists them in the
300
- `internalKeys` option (an array); they are then left out of tooltips, the keyword filter and the
301
- Properties dialog's "Other" section like the built-in fields. `graphDiagram.structuralKeys('node' | 'table' |
302
- 'edge')` returns the combined set. ArchiMate passes `ArchimateView.PROJECTION_KEYS` (`elementId`,
303
- `relationshipId`, `diagramInstanceId`, `visible`, `zIndex`, `viewObject`).
304
- Archi native `.archimate` import/export round-trips properties: import converts flat `<property key="..." value="..."/>`
305
- elements to typed `'string'` entries; export stringifies typed values back to flat key/value pairs.
306
-
307
- There is still no built-in "Show/Edit Properties" **menu item** — `contextMenuItems`/
308
- `edgeContextMenuItems` are plain hooks a caller wires up themselves; `RenderEngine.NODE_STRUCTURAL_KEYS`/
309
- `EDGE_STRUCTURAL_KEYS` (used internally to keep built-in fields out of the auto tooltip's "extra data"
310
- lines) are public statics for exactly this — see the `showPropertiesDialog` example and its use in the
311
- Editable + Value Sizing demo in `test/charts.html` for a small dialog that lists every field on a clicked
312
- node/edge (built-in and caller-added alike) as an editable input, and `showDiagramPropertiesDialog` (wired
313
- to `onCanvasDoubleClick`/a canvas context-menu item in the same demos) for a diagram/view-level settings
314
- dialog — `edgeRouting`, plus (for an ArchiMate diagram) the live `iconChoiceIndex` setting via
315
- `_archimateSetIconChoiceIndex`. In that Properties dialog's own layer/type cascade (`test/charts.html`),
316
- changing an ArchiMate element's `layer` clears and refocuses its `type` field — every official type belongs
317
- to exactly one layer, so the old type essentially never still applies once the category changes.
318
- - `graphDiagram3D(nodes, edges, opts)` — same data shape and sizing options, real orbit camera instead of
319
- 2D force layout (node radius/edge width are additionally scaled by the camera's own perspective factor).
320
- - `archimateDiagram(elements, relationships, opts)` — ArchiMate-notation diagram (Archi/archimatetool.com-style):
321
- a thin transform over `graphDiagram` that builds rect nodes/edges with per-layer pastel fill, the label
322
- inside the box, a small type-icon corner badge, and the 11 official relationship line/arrowhead notations,
323
- then calls `graphDiagram` with them — every `graphDiagram` capability (drag, resize, routing, right-click
324
- menus, editable add/rename/delete, ...) still applies.
325
-
326
- **Archi native `.archimate` import/export:** `RenderEngine.importArchiXML(xml, viewId?)` parses an Archi
327
- native XML file and returns `{ elements, relationships, options }` ready for `archimateDiagram()`. It maps
328
- ArchiMate elements/relationships, view `DiagramObject` children (bounds, font, line thickness), and the
329
- visual objects `DiagramModelNote`, `DiagramModelGroup`, `DiagramModelImage`, `DiagramModelReference`,
330
- plus `SketchModelSticky`/`Actor` and `CanvasModelBlock`/`Sticky`/`Image`. Archi stores a nested visual's bounds
331
- relative to its parent (a group, a group in a group, or an element drawn inside another element); import
332
- turns them into positions on the view, so an element at `x="10"` inside a group at `x="100"` lands at 110.
333
- A group inside a group keeps its `parentId`. On export, a view writes the groups drawn on it (those
334
- with bounds in its `folderLayout`; `view.placedFolders`) nested as they are, each element inside the
335
- group its folder is drawn as (`view.placedFolderOf(elementId)`, the nearest drawn ancestor), bounds
336
- relative to the group. A folder that only exists in the model tree is not written as a group.
337
- `model.allFolders()` lists every folder after its parent, read from `parentId`.
338
- `RenderEngine.exportArchiXML(model, views)`
339
- serializes back to Archi native XML, including element and relationship names, documentation and
340
- properties. `views` are `{id, name, children}`; `fromArchimateView(view)` builds `children` from an
341
- `ArchimateView`: one visual per instance, referencing the model element and relationship ids, with
342
- bounds, bend points, and view-only notes and images.
343
- The XML is what Archi 5.8 itself reads and writes (checked by opening our files in Archi and reading
344
- back what it saved): all nine top-level folders in Archi's order (Archi fails to open a model with a
345
- view but without them), elements as ArchiMate 3.2 classes (Archi knows no other: an ArchiMate 4
346
- `process` in the application layer is written as `ApplicationProcess`, see `archimate32Type`),
347
- relationships as `<Kind>Relationship` (a directed association is `AssociationRelationship
348
- directed="true"`), groups and notes as `archimate:Group`/`archimate:Note` (a note's text in a
349
- `<content>` element), each line naming its relationship as `archimateRelationship`, and each box
350
- listing its incoming lines in `targetConnections` (without them Archi leaves some lines undrawn).
351
- Import reads Archi's names and the older ones earlier versions of this module wrote.
352
-
353
- **What an import keeps.** Checked against a real Archi model (canvas, sketch and ArchiMate views, nested
354
- folders, styles, a legend, hints, report properties): Archi renders our export of it pixel-for-pixel as it
355
- renders the original, and re-saving our export in Archi gives back the original model.
356
- - **Positions.** The renderer places a node by its centre; Archi keeps a box's top-left corner. Import
357
- centres each box (`x + width/2`), export takes the corner back (whole pixels, so a centre comes back
358
- within half a pixel).
359
- - **One instance per box.** Each box Archi draws is an instance with its own size and style, so the same
360
- element twice on a view is two instances; each line joins the two instances it joins. A view shows what
361
- Archi drew on it and nothing else (no "all elements", no lines Archi did not draw).
362
- - **All views.** Views in sub-folders of Views, and canvas (`canvas:CanvasModel`) and sketch views: canvas
363
- blocks and stickies and sketch stickies are view-only boxes with their text, colours and picture; a canvas
364
- box without a border colour has no border; a sticky's text is centred vertically, as Archi draws it.
365
- - **Style.** Archi's attributes `fillColor`, `lineColor`/`borderColor`, `fontColor`, `font` (SWT FontData:
366
- name, size in points, bold/italic), `lineWidth`, `textAlignment` (centred when absent) and `textPosition`
367
- become the instance's `appearance`.
368
- - **Plain lines.** A line with no relationship (a note to an element, a canvas connection) is a
369
- `ViewConnectionInstance` on the view, its look from Archi's `type` bits (solid/dashed/dotted; filled, hollow
370
- or line arrows at either end). The validator leaves it alone.
371
- - **Everything else, written back.** What the model does not use is kept on the item (`instance.archi`,
372
- `view.archi`, `model.archi`, a concept's `archi`) and written back unchanged: other attributes (`hintTitle`,
373
- `imagePosition`, `accessType`, `viewpoint`, a line's `locked`, ...), other child elements (`hintContent`, `notes`,
374
- features such as a legend's), the original style (each attribute written as it was until its meaning is
375
- changed), Archi's ids for boxes and lines, the stacking (document) order, a box's place inside another box
376
- (canvas stickies in their blocks), the model-tree sub-folders (`archiFolder`), picture paths in the
377
- archive, the model's id, `documentation` (Archi's purpose) and properties, and view properties (keys may
378
- repeat). A copied view's boxes get new ids. Text keeps its spaces, and a CR is written as `&#xD;`.
379
- - **Locks and groups.** A box's lock is the view's arrangement: Archi's `locked="true"` on import, and on
380
- export where Archi can lock (canvas boxes); elsewhere (ArchiMate elements, notes, groups, images) Archi has
381
- no lock, so it is the feature `renderengine.locked`, which Archi keeps. A logical group is the feature
382
- `renderengine.group` (the group's id) on each member.
383
- - **Canvases and templates.** A canvas (`canvas:CanvasModel`) is a `CanvasView`, its own kind of
384
- `ArchimateView` (`view.constructor.kind === 'canvas'`; `ArchimateModel.registerViewType(archiViewType, Class)`
385
- maps an Archi view type to a view class). A canvas kept to copy from is a template (`view.template`);
386
- Archi has no such flag, so it is the view feature `renderengine.template` (`ARCHI_TEMPLATE_FEATURE`),
387
- which Archi keeps. A copied view keeps its boxes inside their blocks.
388
- - **Types Archi lacks.** Archi has a class for each ArchiMate 3.2 element type only, and will not open a
389
- file naming another. An ArchiMate 4 type is written as its 3.2 type; any other (an IT icon type such as
390
- `load-balancer`, or a custom type) as the nearest 3.2 type by layer and aspect (`archiElementType`,
391
- `NEAREST_ARCHI_TYPE`: a technology device is a Node, a data store an Artifact, and so on), with its own type
392
- kept as the feature `renderengine.type` and an Archi specialization named after it ("Load Balancer"), so
393
- Archi users see it too. Import gives the type back and drops that specialization. An element Archi had
394
- already given a specialization keeps its own.
395
- - **No colour.** A box with no fill (`fillColor: 'none'`) is Archi's `alpha="0"`, and one with no outline
396
- (`strokeColor: 'none'`) is Archi's `lineAlpha` feature at 0, both ways.
397
- Bend points are written as Archi writes them, offsets from the centres of the two connected boxes
398
- (`<bendpoint startX startY endX endY/>`, centres taken in whole pixels as Archi does). On import, bend
399
- point i of n sits at `(1 - w)(sourceCentre + start) + w(targetCentre + end)` with `w = (i + 1)/(n + 1)`;
400
- an `x`/`y` bend point from an earlier export is still read as a point on the view. An element saved
401
- at Archi's default size (`width="-1"`) is 120 x 55 (a group 400 x 140, a note 185 x 80).
402
-
403
- ```js
404
- // Archi wrote <bendpoint startX="40" startY="13" endX="-160" endY="-87"/> on a line from a box at
405
- // (0,0,120,55) to one at (200,100,120,55):
406
- toRenderEngineModel(parseArchiModel(xml)).relationships[0].waypoints; // → [{ x: 100, y: 40 }]
407
- ```
408
- `toArchimateModel(parseArchiModel(xml))` reads all of that back, keeping notes and images on their
409
- view (not as model elements) and the model's `name`.
410
-
411
- **Archi files with pictures** are zip archives: the XML as `model.xml` and each picture under
412
- `images/`, which `imagePath` names. `parseArchiFile(input)` takes a file as text, bytes or an
413
- `ArrayBuffer`, plain or archived, and returns what `parseArchiModel` does with each archived picture
414
- turned into a `data:` URL. `toArchiFile(model, views)` takes what `toArchiXML` does and returns
415
- `{ data, archive, mimeType }`: plain XML when no visual has a `data:` picture, otherwise an archive
416
- (bytes) with the pictures under `images/`, once each. `src/archimateArchive.js` owns the bridge;
417
- `ZipArchive` (`src/zip/`, with a raw DEFLATE decoder and CRC-32, no dependencies) reads stored and
418
- deflated entries, which is what Archi writes, and writes stored ones.
419
-
420
- ```js
421
- const file = toArchiFile(model, views); // { data, archive: true, mimeType: 'application/zip' }
422
- const back = toArchimateModel(parseArchiFile(file.data));
423
- ```
424
-
425
- **Open Group Exchange Format** (ArchiMate Model Exchange File Format 3.1), the standard route to and
426
- from other ArchiMate tools (Archi, Sparx EA, BiZZdesign, ...). `parseExchangeModel(xml)` gives what
427
- `parseArchiModel` gives, and `toExchangeXML(model, views)` takes what `toArchiXML` takes, so both share
428
- the Archi mapping; `toArchiExport(model)` gives an `ArchimateModel` as `[modelData, views]` for any of the
429
- writers. `src/archimateExchangeFormat.js` owns what differs:
430
- - **Positions.** A node's x/y and a bendpoint are on the view; Archi's are relative to the parent box and
431
- offsets from the line's ends. The format allows no negative position, so a view reaching left of or
432
- above 0 is moved right and down as a whole (as Archi exports).
433
- - **Concepts.** Properties name a property definition (one per key). An Or junction is Archi's junction of
434
- type "or"; an access relationship's `accessType` (Read, Access, ReadWrite; Write when absent) and an
435
- influence's `modifier` are Archi's `accessType` and `strength`; `isDirected` is a directed association.
436
- A type Archi lacks is written as its nearest 3.2 type, with its own kept in the property
437
- `renderengine.type` (`EXCHANGE_TYPE_PROPERTY`), which import reads back.
438
- - **Folders** are organizations: an item per Archi top-level folder, holding the sub-folders. On import an
439
- outer item named as one of Archi's folders is that folder; any other outer item is a sub-folder.
440
- - **Views.** Only ArchiMate diagrams: the format has no canvases or sketches, which are not written. A
441
- group is a Container, a note (or picture, or view reference) a Label. A viewpoint is the format's name
442
- ("Business Process Cooperation", `EXCHANGE_VIEWPOINTS`) and Archi's id (`business_process_cooperation`).
443
- - **Style.** Colours are r/g/b with a 0-100 alpha (0: no fill or no line), the font a name, size and
444
- `style` ("bold italic"). Only what a box or line sets itself is written; the reading tool draws the
445
- rest with its own defaults.
446
- - **Identifiers** must be xs:IDs: an id that is one is kept, any other (`1`, `a b`) is made one
447
- (`id-1`, `id-a_b`) and every reference follows it.
448
-
449
- Checked with Archi 5.8: Archi validates the files against the schema and opens them; a file Archi
450
- exported comes back through `parseExchangeModel`, `toArchimateModel`, `toArchiExport` and
451
- `toExchangeXML` identical to Archi's own import of it. Archi's importer ignores `lineWidth`.
452
-
453
- ```js
454
- const model = toArchimateModel(parseExchangeModel(xml)); // a file from Archi, Sparx EA, BiZZdesign, ...
455
- const out = toExchangeXML(...toArchiExport(model)); // and back
456
- ```
457
-
458
- `src/archimateArchiFormat.js` owns the pure
459
- mapping and the tiny dependency-free XML parser (uses `DOMParser` in browsers; under Node, a fallback
460
- that decodes entities and character references, takes either quote, and skips comments and CDATA).
461
- **CSV import/export:** `RenderEngine.exportModelCSV({elements, relationships})` returns `{ elementsCSV,
462
- relationshipsCSV }` for spreadsheet editing; `RenderEngine.importModelCSV(elementsCSV, relationshipsCSV)`
463
- parses them back into element/relationship objects. Columns are `id,name,type,layer,documentation,properties`
464
- for elements and `id,source,target,kind,label,documentation,properties` for relationships. The `properties`
465
- column holds the typed property array as JSON. `src/archimateCSV.js` owns the pure CSV helpers and is also
466
- exported directly (`exportElementsCSV`, `importRelationshipsCSV`, etc.).
467
-
468
- **`src/archimate.js`** owns the actual notation data — `ArchimateLayer`/`ArchimateElementType`/
469
- `ArchimateRelationshipType` instances (all also re-exported from the package root, and as
470
- `RenderEngine.ArchimateNotation`/`.ArchimateLayer`/etc. statics for convenience), queried through the
471
- `ArchimateNotation` registry rather than archimateDiagram's own flat string-keyed maps — deliberately OO
472
- because a type's layer is then a real object reference (`type.layer`), not a name in one lookup table and
473
- the same name in a second, separate one that could silently drift apart. `ArchimateNotation.typesInLayer
474
- (layerName)` is the query that relationship supports directly — the dependency a "type" control that must
475
- narrow to match a chosen "layer" needs (see the cascading `layer`/`type` fields in the Properties dialog in
476
- `test/charts.html`, which read exactly this both ways: picking a layer narrows type's own suggestions to
477
- that layer's types; picking/typing a recognised type snaps layer to match it).
478
-
479
- Every element type and relationship type also carries a short `description` for authoring UIs:
480
- `ArchimateNotation.describe(typeName, version)` returns an element's description, and
481
- `ArchimateNotation.describeRelationship(kind)` returns a relationship's description (both undefined for
482
- unrecognised names). The Properties dialog in `test/charts.html` shows these in a "?" tooltip next to the
483
- `type` and `kind` fields. `LLM_ARCHIMATE_GUIDE.md` packages the same catalog plus prompting guidance for
484
- LLMs that generate diagram data.
485
-
486
- **IT / infrastructure extension types.** `archimateDiagram` also recognises a set of practical IT concepts
487
- that ArchiMate does not name directly: `firewall`, `laptop`, `server`, `database`, `table`, `file`, `folder`,
488
- `container`, `pod`, `load-balancer`, `cloud-region`, and more (see `src/itIcons.js`/`RenderEngine.ITIconNotation`).
489
- These resolve to the `technology` layer with the correct `aspect` (`active-structure` for devices/services,
490
- `passive-structure` for data/files) and their own icon, so they behave like first-class ArchiMate elements in
491
- diagrams, validation, and the construction UI. They are treated as extended/custom types, not official
492
- ArchiMate, and the validator uses their aspect/layer for relationship advice.
493
-
494
- `elements:[{id,label,layer?,type?,aspect?,icon?,iconUrl?}]` — `layer` (any string, not just the 7 official
495
- names) picks the fill colour (`ArchimateLayer#color`, extendable via `opts.layerColors`), or derives from
496
- `type` when `layer` is omitted: first checked against the full ~55 official ArchiMate element names
497
- (`ArchimateNotation.elementType(type)?.layer` — most, e.g. `'goal'`/`'node'`/`'resource'`, do NOT start
498
- with their own layer's name), then the prefix of `type` before its first `-` for anything not in that
499
- catalog (`type:'business-my-widget'` → layer `'business'`, for ANY `'<layer>-whatever'` string, official
500
- name or not). **Icons:** an element's own `icon`/`iconUrl` always wins; otherwise a recognised `type`'s own
501
- representative icon (`ArchimateElementType#icon` — a glyph echoing the real pictogram's idea, e.g. Goal's
502
- 🎯, Location's 📍, Application Component's 🧩 — not a redraw of the actual vector artwork); otherwise a
503
- generic per-layer one (`ArchimateLayer#defaultIcon`) — either can be overridden diagram-wide via
504
- `opts.defaultIcons`. **Badges are the notation's own glyphs, not emoji.** In the real notation a corner
505
- badge IS the element's standalone glyph drawn small — a Business Process is a box with a little arrow in
506
- the corner, and that same arrow alone is the element without its box. So the badge is rendered from the
507
- type's own `notationShape` (as line art, via the generic `node.iconShape` graphDiagram field), one vector
508
- glyph per type used at two sizes. Every official type in both catalogs has one, so ArchiMate rendering
509
- never falls back to an emoji; `opts.iconSet` (`'glyph'|'emoji'`, default `'glyph'`) switches the whole
510
- diagram to the Unicode stand-in set instead, and THAT is where `iconChoiceIndex`/`iconChoices` apply — the
511
- standard glyph has no alternatives to choose between, the emoji do. **Icon choice sets:** a type doesn't offer just the one icon —
512
- `ArchimateElementType#iconChoices`/`ArchimateNotation.iconChoices(typeName)` return a deduplicated list with
513
- the official ArchiMate-standard glyph always at index 0, followed by a few generic aspect-wide alternatives
514
- (`active-structure`/`behavior`/`passive-structure`/`motivation`/`other` — e.g. behavior types alternate
515
- between ▶️/🔄/⚡) and finally the type's own layer default; an unrecognised/absent type falls back to
516
- `GENERIC_ICON_CHOICES` (every aspect's alternatives pooled together). Which choice is used as an element's
517
- *default* (when it sets no `icon` of its own) is a diagram-wide setting, `opts.iconChoiceIndex` (default
518
- `0`, the standard icon) — an out-of-range index falls back to choice 0 rather than throwing. This is what a
519
- "choice of icons per type, with the ArchiMate-standard one as the default" UI (a `<select>` of
520
- `iconChoices(type)`, e.g. the Properties dialog's own `icon` field in `test/charts.html`) is built on; an
521
- element's own explicit `icon` still always wins over any diagram-wide choice. `iconChoiceIndex` isn't just a
522
- one-shot build option — `engine._archimateSetIconChoiceIndex(newIndex)` changes it LIVE after the diagram's
523
- already drawn, re-deriving the icon for every node that's still on its old auto-derived default (same "was
524
- it ever customised" check the Properties dialog's own layer/type-change handler uses) and leaving any
525
- individually-picked icon alone; `test/charts.html`'s `showDiagramPropertiesDialog` (opened via
526
- `onCanvasDoubleClick` or a canvas context-menu item) is a small UI for this. It's kept on `engine.
527
- _archimateOptions`, not on `_chartState` — `archimateDiagram`'s own settings (`iconChoiceIndex`/
528
- `layerColors`/`defaultIcons`/`relationshipStyles`) aren't part of `graphDiagram`'s flat `options` (they're
529
- destructured out before its own options are built), and `_chartState` itself gets overwritten wholesale on
530
- every `graphDiagram`/`_rerenderGraph` call, so anything meant to survive a redraw lives on the engine
531
- instance instead. **Box corner style**
532
- (`ArchimateElementType#cornerStyle`): every Motivation-layer
533
- element is chamfered (all 4 corners cut at 45°) regardless of anything else; `'behavior'` aspect
534
- (Process/Function/Service/Event/Interaction-like) gets rounded corners — an explicit `element.aspect` wins,
535
- else a recognised `type`'s own aspect (so `type:'business-process'` is rounded with zero extra config);
536
- everything else is sharp (correct for active/passive-structure elements). An element's own `cornerStyle`
537
- (`'sharp'|'rounded'|'chamfered'`) overrides all of that directly — and works on any plain `graphDiagram`
538
- `shape:'rect'` node too, not just via `archimateDiagram`.
539
-
540
- `relationships:[{source,target,kind?}]` — `kind` (`composition`/`aggregation`/`assignment`/`realization`/
541
- `specialization`/`serving`/`access`/`influence`/`triggering`/`flow`/`association`) picks the notation
542
- (`ArchimateRelationshipType`, extendable via `opts.relationshipStyles`); an explicit `lineStyle`/
543
- `arrowhead`/`arrowheadStart` on one relationship still overrides its kind's default.
544
-
545
- **Standard alignment (advisory warnings, not blocking).** `archimateDiagram` validates the model against
546
- the ArchiMate metamodel and warns where it deviates. `opts.invalidRelationships` says how an invalid
547
- relationship shows: `'highlight'` (the default: a red dashed line and a tooltip), `'tooltip'` (the warning
548
- on hover only) or `'off'` (nothing checked); `opts.validate: false` also turns checking off. The ArchiMate
549
- editor takes it from the "Invalid relationships" setting, which a diagram, its model or the tool sets
550
- (see the settings cascade below). Warnings are delivered three ways:
551
- - `opts.onValidationWarning(warning)` is called once per warning during render;
552
- - `engine._archimateWarnings` holds the full list after render;
553
- - invalid relationships carry a tooltip explaining the issue, and with `'highlight'` a red dashed stroke.
554
- Relationships between ArchiMate types are checked against the specification's own relationship table
555
- (The Open Group, ArchiMate 3.2 Specification, Appendix B; `ArchimateValidator.specificationKinds`), an
556
- ArchiMate 4 type looked up as its 3.2 type. `src/archimateRelationshipTable.js` holds the table, generated
557
- by `scripts/generate-archimate-relationships.mjs` from the machine-readable copy Archi ships. Custom and IT
558
- types, which the table does not name, fall back to `ArchimateValidator.RULES`: data, each rule naming its
559
- `kinds` and constraining `fromAspect(s)`, `toAspect(s)`, `aspects`, `sameAspect`, `sameLayer`, `fromTypes`
560
- or `toTypes`. Also checked: v4 Common Domain elements (`process`/`function`/`service`/`interaction`/`event`)
561
- needing an explicit `layer`, and retired v3.2 types used under v4.
562
-
563
- ```js
564
- ArchimateValidator.allowedKinds({ type: 'business-actor' }, { type: 'business-role' }, '4');
565
- // → ['assignment', 'serving', 'triggering', 'flow', 'association', 'association-directed']
566
- ``` `RenderEngine.ArchimateValidator` exposes the same checks for construction UIs:
567
- `ArchimateValidator.validateModel(elements, relationships, version)`,
568
- `ArchimateValidator.validateRelationship(source, target, kind, version)`, and
569
- `ArchimateValidator.allowedKinds(source, target, version)` — the latter powers the filtered relationship-kind
570
- dropdown in `test/charts.html`'s Add Edge and Edge Properties dialogs.
571
-
572
- **Specializations.** `RenderEngine.ArchimateSpecializationRegistry` (src/archimateSpecializations.js)
573
- stores user-defined specializations (`{id, name, baseTypes[], icon?, color?, description?}`). Tag an
574
- element with `specialization: '<id-or-name>'` and `archimateDiagram` applies the specialization's icon
575
- and colour unless the element already has its own. `registry.serialize()` / `registry.deserialize(array)`
576
- make persistence trivial; `test/charts.html` persists the catalog in `localStorage` and exposes a
577
- "Specializations…" manage dialog.
578
-
579
- **Colours, glyphs and line weights are read out of the spec's own artwork, not eyeballed.** The
580
- specification PDF embeds its "Appendix A: Summary of Language Notation" figures as LOSSLESS PNGs, so the
581
- palette below is the literal pixel value of each fill: Business `#FFFFAF`, Application `#AFFFFF`,
582
- Technology and Physical both `#AFFFAF` (the spec draws them the same green), Strategy `#F5DEAA`,
583
- Motivation `#CCCCFF`, Implementation & Migration `#FFE0E0`, Location `#FFBF80`. A few types carry a colour
584
- their own layer doesn't share — Plateau/Gap are green (`#E0FFE0`) among the pink Implementation elements,
585
- Grouping is unfilled — so `ArchimateElementType#ownColor` overrides the layer fill for those (element
586
- `color` > `opts.layerColors` > `ownColor` > layer). Elements are drawn with a thin BLACK outline
587
- (`strokeColor: '#000000'`, `strokeWidth: 1.2` — both plain graphDiagram node fields, defaulting to the
588
- usual white 2px elsewhere), which is what the real notation uses; the white stroke graphDiagram uses to
589
- separate overlapping blobs is a large part of why an ArchiMate diagram drawn with plain defaults reads as
590
- much brighter and softer than the spec. The relationship notations were checked glyph-by-glyph against the
591
- same appendix — all 11 kinds, plus the directed-association variant (`association-directed`, a single-barb
592
- `open-arrow-half`) and both junction connector forms the figure shows alongside them.
593
-
594
- **ArchiMate version: `opts.archimateVersion` (`'3.2'|'4'`, default `'4'`, `ArchimateNotation.versions()`).**
595
- ArchiMate 4 retires 19 of 3.2's element types (`src/archimate.js`'s own doc comment has the full reasoning):
596
- fifteen layer-specific behavior types (`business-process`/`application-process`/`technology-process`, and
597
- the same 3-layer split for `function`/`service`/`interaction`/`event`) collapse into five layer-INDEPENDENT
598
- "Common Domain" types (`process`/`function`/`service`/`interaction`/`event` — `ArchimateElementType#layer`
599
- is `null` for these five; the real layer must come from the element's own `layer` field, not the type name);
600
- four more (`contract`, `representation`, `constraint`, `gap`) are retired outright, each replaced by an
601
- existing type. Under the `'4'` default, every element is first run through `ArchimateNotation.
602
- migrateElementToV4` — a direct mirror of a jArchi "Migrate Model Elements from ArchiMate 3.2 to ArchiMate 4"
603
- script's own two mapping tables — a no-op for any other type (most of the ~55, e.g. `business-actor`,
604
- `application-component`, `goal`, `node`, are identical in both versions and all 11 relationship notations
605
- are unchanged), so 3.2-authored `elements` data still renders correctly, auto-upgraded and tagged with its
606
- own migration provenance as plain extra data: `ArchiMate4_Domain` (the original layer, e.g. `'Business'`)
607
- + `MigratedFrom` (the original type) for a behavior consolidation, or `LegacyType`/`Type` + `MigratedFrom`
608
- for a retired type — both visible in the hover tooltip/Properties dialog like any other custom field, same
609
- as `kind` already is for a relationship. An element that already carries its own `layer` keeps it; one that
610
- doesn't gets it filled in from the migration's own knowledge of where it used to live (needed since a
611
- Common Domain type's own layer is `null` — without this, a migrated element with no explicit `layer` would
612
- silently fall through to 'other'). Under `'3.2'`, no migration happens at all — every `ArchimateNotation`
613
- lookup `_archimateBuildNode`/`_archimateBuildEdge` make (`elementType`, `elementTypeNames`, `typesInLayer`,
614
- `iconChoices`, `elementLayer` — all take an optional `version` parameter, default `'4'`) resolves against
615
- the original 3.2 catalog instead. See the ArchiMate Diagram demo's own version `<select>` in
616
- `test/charts.html`, which re-runs `archimateDiagram` over the SAME `elements`/`relationships` for either
617
- choice (not a live bidirectional toggle — a v4-migrated type like `'process'` was never a v3.2 catalog entry
618
- to begin with, so round-tripping back through the ALREADY-migrated data wouldn't resolve correctly; picking
619
- a version is a "how should this diagram be built" choice, made fresh from the original input each time).
620
-
621
- **Distinct notation shapes: `opts.useDistinctShapes` (boolean, default `false`).** The plain box + corner
622
- icon isn't the only ArchiMate notation — the spec's own Appendix A ("Summary of Language Notation") gives
623
- many types a real distinct outline instead (Process = a plain arrow, Interface = a lollipop, Node = a 3D
624
- cube, ...). `ArchimateElementType#notationShape` (`src/archimate.js`) names that shape (a `graphDiagram`
625
- `shape` value — see `_nodeShapeEl`) for every type that has one; `undefined` for a type the spec only ever
626
- shows as a plain box (Business Object, Work Package, Deliverable, ...), or whose real glyph is still too
627
- compound to render cleanly as a single node shape here (System Software's box+circle is the one gap left).
628
- 32 shapes implemented, verified against rendered output (not just the spec image) — see
629
- `test/archimate.test.js`'s own `notationShape` describe block for the full type→shape mapping:
630
- `arrow` (Process), `chevron` (Function, points up with a notched bottom — distinct from arrow, which points
631
- right), `half-circle` (Interaction), `flag` (Event/Value Stream/Implementation Event), `pill` (Service, a
632
- fully-rounded stadium box), `double-circle` (Collaboration), `lollipop` (Interface), `cylinder` (Role/
633
- Stakeholder — a HORIZONTAL drum, left/right elliptical caps, not top/bottom), `cube` (Node, a real 3-face
634
- isometric box), `target` (Goal/Outcome/Course of Action — a bullseye with a dark centre dot), `hexagon`
635
- (Material — flat top/bottom, pointed left/right), `pin` (Location), `cloud` (Meaning), `document` (Artifact),
636
- `parallelogram` (Requirement), `constraint` (Requirement's parallelogram plus its own internal divider
637
- line), `gear` (Equipment — two overlapping cogs), `ellipse` (Value), `battery` (Resource), `component`
638
- (Application Component — a box with two connector tabs on the left edge), `factory` (Facility), `device`
639
- (Device — a monitor on a stand), `steps` (Capability — an ascending staircase), `wheel` (Driver — a
640
- steering wheel), `magnifier` (Assessment), `stack` (Plateau — three bars), `gap-circle` (Gap — a circle
641
- crossed by two lines), `principle` (Principle — a rounded square with "!"), `double-arrow`
642
- (Communication/Distribution Network), `double-arrow-dashed` (Path), `junction` (a small solid dot, not the
643
- plain default circle — no centred initials text), plus the existing `person` (Business Actor). Same
644
- "representative, not pixel-perfect" calibration as the corner-badge icons — cross-checked against the spec,
645
- a few curves/angles simplified for a clean SVG path, not a redraw of the actual vector artwork. When a
646
- distinct shape is in use: the label moves below it (there's no "inside the box" for most of these,
647
- `labelPosition` defaults to `'below'` instead of `'inside'`), and no default corner icon OR centred-initials
648
- fallback is shown (the shape itself already says what the element is — every notationShape case sets
649
- `suppressInner`) — an element's own explicit `icon`/`iconUrl` still wins regardless. `useDistinctShapes` is
650
- a diagram-wide DEFAULT (off, so nothing changes for an existing diagram that hasn't opted in); an individual
651
- element's own `useDistinctShape` (true/false) overrides it either way. A type with no implemented
652
- `notationShape` always renders as the plain box, whichever way the setting goes. Every distinct shape also
653
- honours an explicit `width`/`height` override the same way the plain box does (`_nodeSize` supports this for
654
- any shape now, not just `'rect'`/`'table'` — and the generic `'below'` label offset and the plain
655
- `'person'`/`'diamond'`/default-circle shapes were both fixed to size off the box's own smaller dimension
656
- instead of `max(w,h)/2`, so a wide-but-short box no longer bleeds its label or an oversized circle into the
657
- row below it in a dense grid). See the ArchiMate Diagram demo's own checkbox in `test/charts.html`, and the
658
- dedicated **ArchiMate Notation Reference** demo right after it — every type the current version's catalog
659
- has, one row per layer, built entirely from `ArchimateNotation.layers()`/`.typesInLayer()` rather than a
660
- hand-picked sample, with the same box/shape toggle.
661
-
662
- **Adding elements/relationships interactively:** archimateDiagram only enriches the `elements`/
663
- `relationships` it's first called with — a node added afterward (e.g. via `editable`'s own "Add Node", or a
664
- caller's own canvas action) doesn't automatically get archimateDiagram's layer colour/corner style/icon.
665
- Use `engine._archimateBuildNode(element, opts)` and `engine._archimateBuildEdge(relationship, opts)` to
666
- enrich a new item exactly the same way archimateDiagram does internally — see the ArchiMate Diagram demo's
667
- `canvasContextMenuItems`/`onEdgeAdd` in `test/charts.html`. The built-in drag-to-connect gesture now acts as
668
- a **magic connector** for ArchiMate diagrams: it picks the first relationship `kind` that
669
- `ArchimateValidator.allowedKinds(source, target, version)` considers valid and immediately applies the
670
- matching line/arrowhead style, falling back to a plain `{source,target}` edge only when no validator is
671
- available. The right-click edge menu also offers **Invert Direction** to swap source/target in place.
672
-
673
- See that same demo for all 11 relationship kinds, business/application/technology/motivation layers, a
674
- chamfered Motivation goal, and rounded process/service boxes.
675
-
676
- **Properties dialog fields are typed, not one-size-fits-all text boxes** (`test/charts.html`'s
677
- `showPropertiesDialogUnsafe`/`addRow`): `'text'` (default) | `'textarea'` (`description`) | `'color'` (a
678
- native `<input type=color>` swatch/picker for `color`, in place of a raw hex string) | `'select'` (a closed
679
- list — `layer`, and `icon` sourced from `ArchimateNotation.iconChoices(subject.type)`) | `'select-editable'`
680
- (an `<input list=datalist>` that still accepts free text — `type`, since archimateDiagram deliberately
681
- doesn't enforce a closed type catalog). The dialog also groups a node's fields into an **Object** section
682
- (`name`/`description`/`type` — the ArchiMate concept itself, the same regardless of how any one diagram
683
- draws it) and a **Diagram** section (`label`/`layer`/`color`/`icon`/`group`/`size` — this view's own
684
- presentation of it), with any remaining extra data under "Other"; this is a form-level grouping only —
685
- `graphDiagram` itself still renders one flat node object, there's no separate model/view split under the
686
- hood. The existing layer↔type cascade (`ArchimateNotation.typesInLayer`/`.elementType`) now also refreshes
687
- `icon`'s own `<select>` options to the newly-chosen type's `iconChoices` whenever `type` changes, keeping the
688
- current icon selected if it's still offered.
689
- - `dataModelDiagram(entities, relationships, opts)` — conceptual/logical/physical entity-relationship diagram,
690
- the same "thin transform over `graphDiagram`" shape as `archimateDiagram`. **`src/datamodel.js`** owns the
691
- notation data (`DataModelLevel`/`NotationStyle`, queried through the `DataModelNotation` registry — also
692
- re-exported from the package root and as `RenderEngine.DataModelNotation`), its own separate OO file/
693
- registry for the same reason `archimate.js` is: this domain's vocabulary shouldn't live inside RenderEngine
694
- itself.
695
- **Three levels, one model — the cross-mapping.** An entity is ONE object: `{id, name, description?, x?, y?,
696
- color?, conceptual?, logical?, physical?}` — `name`/`description` are shared across every level (the
697
- entity's own identity, unchanged by fidelity), while `entity.logical.attributes`/`entity.physical.
698
- attributes` (`[{name, type?, isPK?, isFK?}]`, physical's own sub-object also taking `tableName`) hold that
699
- level's own detail — the same object/visual split `archimateDiagram` draws between an element and its
700
- diagram, just spanning "levels" instead of "diagrams". `opts.level` (`'conceptual'|'logical'|'physical'`,
701
- default `'logical'`) picks which one's on screen; an entity with no attributes at the current level
702
- (typical for `'conceptual'`) always renders as a plain box, regardless of notation. Re-calling
703
- `dataModelDiagram` with a different `level` over the SAME `entities`/`relationships` re-renders that same
704
- model at different fidelity — there's only ever one model, never three separately-maintained diagrams that
705
- could drift apart (see the Data Model demo in `test/charts.html`, whose Level/Notation `<select>`s just call
706
- it again).
707
- **Three notations, one cardinality vocabulary.** `relationships:[{source,target,name?,sourceCardinality?,
708
- targetCardinality?,identifying?}]` — cardinality is stored once as a UML-style multiplicity string
709
- (`'0..1'|'1'|'0..*'|'*'|'1..*'`, `DataModelNotation.cardinalities()`), and `opts.notation`
710
- (`'chen'|'crowsfoot'|'uml'`, default `'crowsfoot'`) picks how each end renders it: **Chen** turns the
711
- relationship itself into its own `shape:'diamond'` node with two plain edges (not one direct edge), each
712
- labelled with a plain `'1'`/`'N'` text cardinality; **Crow's Foot** renders cardinality as an arrowhead-style
713
- glyph at each entity end (`crowsfoot-one`/`-one-optional`/`-many`/`-many-mandatory` — new SVGRenderer marker
714
- defs, fork for "many", tick for "one", hollow circle for "optional", usable as a plain edge `arrowhead`/
715
- `arrowheadStart` outside dataModelDiagram too); **UML** shows the multiplicity string itself as a text label
716
- near each end instead, via the new generic `edge.sourceLabel`/`edge.targetLabel` fields (independent of the
717
- existing midpoint `edge.label` — see graphDiagram's own doc comment). `relationship.identifying === false`
718
- renders a dashed line (a non-identifying relationship, in real ERD terms) regardless of notation;
719
- unset/`true` stays solid. None of the three notations are pixel-perfect renditions of the real, full
720
- notations (same calibration as `archimateDiagram`'s own icons) — see `src/datamodel.js`'s own doc comment.
721
- - `railroadDiagram(diagramString)` — renders an EBNF grammar string as a railroad diagram. No node/edge
722
- data at all.
723
-
724
- **Y-axis ticks.** Most cartesian chart types only draw a bare axis line — no numeric tick marks
725
- or gridlines. `barChart` and `groupedBarChart` are wired up to `_drawYAxisTicks` (a shared private
726
- helper built on `niceTicks`/`formatValue`), which draws left-edge tick marks, value labels, and
727
- light horizontal gridlines, and returns the "nice" (rounded-up) domain max the chart's own marks
728
- should scale against instead of the raw data max — see `barChart`'s own doc comment in
729
- `src/index.js` for why that matters (so the tallest bar actually reaches the top tick). Extending
730
- this to `line`/`area`/`stackedBar`/etc. is the same three-line change: call
731
- `this._drawYAxisTicks(x, y, width, height, rawMax)` before drawing marks, and scale against its
732
- return value instead of the raw max — not done yet for the rest, tracked as a follow-up.
733
-
734
- ## Common options
735
-
736
- Not every method takes every option below — the table above calls out what's notable per type —
737
- but these recur across most 2D chart types:
738
-
739
- ```javascript
740
- {
741
- title: 'Chart Title',
742
- xAxisLabel: 'X Axis', yAxisLabel: 'Y Axis',
743
- colors: ['#5470c6', '#91cc75'], // palette, indexed by series/category/segment
744
- x: 70, y: 60, width: 500, height: 300, // chart box (defaults from container size)
745
- showLegend: true, // or `legend:` depending on method — see table above
746
- }
747
- ```
748
-
749
- Data-resolution options (used with `register()`/`refreshChart()`, see below):
750
-
751
- ```javascript
752
- { dataPath: 'metrics.sales', dataMapper: (v) => v * 1.1 }
753
- ```
754
-
755
- ### `colorCol` — plain field, or a compiled formula
756
-
757
- `scatterChart`/`bubbleChart`/`bubbleChart3D`'s `colorCol` names which field to color-group points
758
- by (falling back to a `category`/`group` field if present, and to one flat color if neither is
759
- given). It also accepts a **compiled formula** over the point's own fields, instead of just a field
760
- name — e.g. bucketing points by whether a value crosses a threshold, without pre-computing a
761
- derived field before calling into RenderEngine:
762
-
763
- ```javascript
764
- engine.scatterChart(data, {
765
- colorCol: { formula: 'value > threshold ? 1 : 0', params: ['value', 'threshold'] },
766
- });
767
- ```
768
-
769
- `params` are field names read off each data point, in the order the formula uses them. The formula
770
- is a small, dependency-free scalar-expression language (`src/kernelExpr.js`) — arithmetic,
771
- comparisons, `&&`/`||`/`!`, `? :`, and `abs floor ceil round sign sqrt exp log sin cos tanh min max
772
- pow clamp` — compiled once per `colorCol` object with `new Function` from an AST the parser built,
773
- never from the formula text directly, so a bad or malicious-looking formula string is refused at
774
- parse time rather than reaching `eval`. Every distinct value the formula returns gets its own
775
- palette color, exactly the way a plain field's distinct values already do.
776
-
777
- ## Data & utility methods
778
-
779
- ```javascript
780
- engine.setGlobalData(data) // set a global object referenceable by dot-path
781
- engine.resolvePath(path, mapper?) // e.g. resolvePath('metrics.sales.0.value')
782
- engine.register(id, type, options) // remember a {type, options} pair for a container id
783
- engine.refreshChart(id) // re-invoke it — resolves options.data or dataPath+dataMapper fresh each time
784
- engine.niceTicks(maxVal, count=4) // readable axis tick values
785
- engine.formatValue(v) // 1234567 -> "1.23M"
786
- engine.bindTooltip(element, text)
787
- engine.bindRichTooltip(element, {title, rows:[{k,v}]})
788
- engine.adjustColor(hex, amount) // lighten/darken, used internally for 3D shading
789
- ```
790
-
791
- `register`/`refreshChart` is the registration mechanism `addDataPoint`/`addGraphNode` (next section)
792
- build on.
793
-
794
- ## Interactivity
795
-
796
- ```javascript
797
- engine.hitTest(x, y) // shape under a point (manual on Canvas, native on SVG)
798
- engine.enterState(shape, 'hover') // apply a named visual state
799
- engine.leaveState(shape, 'hover')
800
- engine.resetView() // reset SVG pan/zoom or 3D orbit camera to identity
801
- ```
802
-
803
- SVG-rendered charts get wheel-zoom and drag-empty-space-to-pan for free; 3D-orbit charts
804
- (`bubble3D`, `bar3DOrbit`, `stackedBar3DOrbit`, `graphDiagram3D`) get drag-to-rotate/wheel-to-zoom
805
- instead — the two are mutually exclusive per chart, never combined.
806
-
807
- ## Getting a chart out — clipboard and files
808
-
809
- ```javascript
810
- import { copyImage, downloadPng, downloadSvg, toPngBlob } from 'renderengine';
811
-
812
- await copyImage(container, { name: 'sales-by-region' }); // → {via:'clipboard'} | {via:'download', filename}
813
- await downloadPng(container, { scale: 2 });
814
- downloadSvg(container);
815
- const blob = await toPngBlob(container, { scale: 2, background: '#ffffff' });
816
- ```
817
-
818
- Pass the container, the `<svg>` or the `<canvas>` — the drawable is found either way. Options are
819
- `scale` (default 2, retina-sharp), `background` (default `'#ffffff'`, `null` for transparent) and
820
- `name` (the download's filename stem).
821
-
822
- `copyImage` **falls back to downloading the PNG** where writing images to the clipboard is
823
- unavailable — Firefox, and any page not on a secure context — so the button always does something.
824
- The returned `via` says which happened, so a caller can tell the user the truth rather than claiming
825
- a copy that did not occur.
826
-
827
- Three things this handles that a hand-rolled `XMLSerializer` + `canvas` will get wrong, each
828
- invisible until someone pastes the result somewhere:
829
-
830
- | | |
831
- |---|---|
832
- | **No intrinsic size** | An SVG sized by CSS (`width: 100%`) has no `width` attribute, and an `Image` loading that markup is 0×0 — a blank paste. Explicit pixel dimensions are set from the laid-out box. |
833
- | **Transparent background** | Diagrams draw no background, so a PNG pasted into anything dark shows dark-on-dark labels. A background rect goes in as the first child, behind the drawing. |
834
- | **Namespace loss** | Markup without `xmlns` will not load as an image at all; `xlink:href` (markers, patterns) needs its own, added only when something uses it. |
835
-
836
- Rasterizing is capped at `MAX_IMAGE_PIXELS` (32M). Over that, the **scale** is reduced rather than
837
- the diagram's own dimensions — halving the scale still shows the whole diagram, whereas cropping or
838
- shrinking it loses content or legibility. `imageSize()` and `prepareSvgMarkup()` are exported
839
- separately, and are pure, if you want the decisions without the DOM.
840
-
841
- **The exported image does not inherit the page's theme.** Serialized markup loaded as an image sees
842
- no author stylesheet, so the drawing falls back to the presentation attributes the engine itself
843
- emits — dark text and axes — rather than whatever the workspace's dark theme was overriding them
844
- with. That is the right result for something being pasted into a document on white paper, but it
845
- does mean a copy taken from a dark-themed app comes out light. To export a genuinely dark image,
846
- set `background` to the dark colour *and* render the chart with explicitly dark-friendly colours
847
- rather than relying on CSS.
848
-
849
- ## Dynamically adding data points in a bounded window
850
-
851
- `addDataPoint`/`addGraphNode` are real `RenderEngine` methods — the engine owns the append/trim
852
- mechanics, not the caller. Internally they delegate to a small class hierarchy in
853
- `src/datawindow/` (see `ARCHITECTURE.md`'s "Data windows — streaming" section for the full design);
854
- this section is just how to call it.
855
-
856
- ### 1. Register, then push
857
-
858
- ```javascript
859
- engine.register('live-chart', 'line', { data: initialSeries, title: 'Live feed' });
860
-
861
- let tick = 0;
862
- setInterval(() => {
863
- engine.addDataPoint('live-chart', () => ({ label: `T${++tick}`, value: randomValue() }), { maxPoints: 50 });
864
- }, 1500);
865
- ```
866
-
867
- - `register(id, catalogKeyOrMethodName, options)` — same registration mechanism `refreshChart` has
868
- always used; `options.data` is the chart's current dataset.
869
- - `addDataPoint(id, pointOrFactory, { maxPoints })` — appends one point (dropping the oldest once
870
- past `maxPoints`; `0` = unbounded) and re-renders via `refreshChart`. `pointOrFactory` can be a
871
- literal point, or a factory — required for multi-series charts (`line`/`area`/`pipe`), where it's
872
- called once per series so each series can get its own value, and useful for shapes like
873
- `candlestick` where the next point depends on the last one:
874
-
875
- ```javascript
876
- // candlestick — factory receives the current data array, so the next bar
877
- // can continue from the previous bar's close:
878
- engine.addDataPoint('live-candles', (data) => {
879
- const last = data[data.length - 1];
880
- const open = last ? last.close : 100;
881
- const close = +(open + (Math.random() - 0.5) * 6).toFixed(2);
882
- return { label: `T${++tick}`, open, high: Math.max(open, close) + 1, low: Math.min(open, close) - 1, close };
883
- }, { maxPoints: 100 });
884
- ```
885
-
886
- Every `CHART_CATALOG` entry declares a `streamShape` that decides how `addDataPoint` treats its
887
- data — `addDataPoint` is a **no-op** when it's `'none'`:
888
-
889
- | `streamShape` | Chart types | Behavior |
890
- |---|---|---|
891
- | `flat` | `bar`, `pie`, `scatter`, `bubble`, `candlestick`, `stackedBar`, `funnel`, `waterfall`, `histogram`, `qq`, `lag`, `treemap`, `sunburst`, `outline`, `bar3D`, `bubble3D` | one new record appended to the outer array |
892
- | `series` | `line`, `area`, `pipe` | every series' own `values` grows by one point |
893
- | `grouped-sample` | `boxPlot`, `density`, `violin` | exactly ONE randomly-chosen group's `values` grows (a new sample observation belongs to one group, unlike a shared time axis) |
894
- | `none` | `heatmap`, `radar` (fixed matrix/axis-set), `bar3DOrbit`/`stackedBar3DOrbit` (fixed grid), `gauge-dial`/`gauge-bar`/`bullet` (a value to replace, not a window) | `addDataPoint` no-ops — for gauges, just call `dialGauge(newValue)`/`barGauge(newValue)`/`bulletGauge(newValue)` again directly |
895
-
896
- ### 2. Graphs — `addGraphNode`
897
-
898
- `graphDiagram`/`graphDiagram3D` aren't `CHART_CATALOG` entries (see ARCHITECTURE.md), so they get
899
- their own method rather than going through `streamShape`. Register with `{nodes, edges}` instead of
900
- `{data}` — `refreshChart` detects the graph shape via `options.nodes`:
901
-
902
- ```javascript
903
- engine.register('live-graph', 'graphDiagram', { nodes: initialNodes, edges: initialEdges });
904
-
905
- setInterval(() => {
906
- const parent = engine.registry['live-graph'].options.nodes[Math.floor(Math.random() * n)];
907
- engine.addGraphNode('live-graph', { id: `n${++tick}`, label: `Node ${tick}` }, [{ source: parent.id, target: `n${tick}` }], { maxNodes: 30 });
908
- }, 1500);
909
- ```
910
-
911
- `addGraphNode(id, node, newEdges, { maxNodes })` drops the oldest nodes once past `maxNodes`, and
912
- any edge left dangling off a removed node along with them.
913
-
914
- ### Chained form — `engine[id].addGraphNode(...)`
915
-
916
- Once an id is `register()`ed, `engine[id]` (or `engine.someId` when the id happens to be a valid
917
- identifier — most DOM ids, e.g. `chart-container`, contain a hyphen, so bracket notation is what
918
- you'll actually type) returns a handle scoped to that container, dropping the repeated `id` first
919
- argument:
920
-
921
- ```javascript
922
- engine.register('live-graph', 'graphDiagram', { nodes: initialNodes, edges: initialEdges });
923
-
924
- setInterval(() => {
925
- const parent = engine.registry['live-graph'].options.nodes[Math.floor(Math.random() * n)];
926
- engine['live-graph'].addGraphNode({ id: `n${++tick}`, label: `Node ${tick}` }, [{ source: parent.id, target: `n${tick}` }], { maxNodes: 30 });
927
- }, 1500);
928
- ```
929
-
930
- Both forms work interchangeably — `engine.addGraphNode(id, ...)` and `engine[id].addGraphNode(...)`
931
- call the same method underneath. The handle also exposes `register`/`refreshChart`/`addDataPoint`
932
- the same way. It only resolves for an id already in `engine.registry` (i.e. `register()` must still
933
- be called the normal way first) — an unregistered id falls through to a real, `undefined` property
934
- instead of a handle, same as before this existed.
935
-
936
- ### 3. If you never `register()` a container
937
-
938
- `addDataPoint`/`addGraphNode` require a prior `register()` call (they look up the chart's type and
939
- current data from the registry). For a one-off container you don't want to register, own the array
940
- yourself and re-call the chart method directly — `createDataWindow(streamShape, data, {max})` (also
941
- exported, see `src/datawindow/index.js`) gives you the same append/trim classes standalone:
942
-
943
- ```javascript
944
- import { createDataWindow } from 'renderengine';
945
-
946
- const window = createDataWindow('flat', initialData, { max: 50 });
947
- setInterval(() => {
948
- const data = window.push(() => ({ label: `T${++tick}`, value: randomValue() }));
949
- engine.lineChart(data, { title: 'Live feed' });
950
- }, 1500);
951
- ```
952
-
953
- ### Performance guidance
954
-
955
- - Every tick is a full `clear()` + redraw, not a diff — cheap for tens–low-hundreds of points per
956
- series, but don't drive it faster than the container actually needs to look "live." 1–2 second
957
- intervals read as real-time to a human; sub-100ms intervals mostly burn CPU redrawing pixels
958
- nobody perceives changing.
959
- - If your update source fires faster than you want to redraw (e.g. a WebSocket pushing every
960
- message), buffer incoming points and flush `addDataPoint` on a `requestAnimationFrame` or fixed
961
- interval instead of calling it per message.
962
- - **Canvas** renderer for high point counts or high-frequency updates (5,000+ point ceiling vs
963
- SVG's ~1,000+); **SVG** if you need native per-point tooltips/hit-testing more than raw throughput.
964
- - `graphDiagram`'s force layout (`_layoutForce3D`, `iterations` default 150) re-settles the whole
965
- graph on every redraw — fine for occasional `addGraphNode` calls, but don't drive it on a
966
- sub-second timer with a large node count; pin node positions (`arrange`-style layouts, see
967
- `ARCHITECTURE.md`) if you need cheap, jitter-free incremental adds.
968
-
969
- See `examples/csv-chart-builder.html`'s "Live updates" panels (section 4 for charts, section 8 for
970
- graphs) for a full working implementation of all of the above, including the per-shape synthetic
971
- value generators (`LIVE_APPENDERS`) that decide *what* value to invent each tick — legitimately
972
- demo-specific, unlike the append/trim mechanics above.
973
-
974
- ## ArchiMate Diagram Editor (OO component)
975
-
976
- **`examples/archimate.html` is the canvas editor now** (`ArchimateCanvasPart`, type `archimate-canvas`, below under
977
- "Diagram templates and palette categories"); this section's graph-engine editor is kept in
978
- `examples/archimate-legacy.html` until the canvas editor has replaced it.
979
-
980
- **In a workbench: `ArchimateEditorPart`.** The legacy modeller is a workbench part (type
981
- `archimate-editor`, workspace bundle) built on `EditorPart`: it takes the model as part of its JSON
982
- spec, fills its pane, and sits in a `<ws-workspace>` beside any other parts. `examples/archimate-legacy.html`
983
- is this, and nothing else:
984
-
985
- ```javascript
986
- const { el, registerBuiltinParts, ArchimateEditorPart } = RenderEngineWorkspace;
987
- const ws = document.body.appendChild(el({ tag: 'ws-workspace', attrs: { label: 'ArchiMate Modeller', theme: 'light' } }));
988
- registerBuiltinParts(ws);
989
- ws.registerPart(ArchimateEditorPart);
990
- ws.addParts([{ id: 'modeller', type: 'archimate-editor', title: 'Model', elements: [...], relationships: [...], views: [...] }]);
991
- ws.registerPerspective('Modelling', { type: 'part', partId: 'modeller' });
992
- ws.activatePerspective('Modelling');
993
- ws.getPart('modeller').model; // the ArchimateModel (an import replaces it)
994
- ```
995
-
996
- Spec options: `elements`, `relationships`, `views` and `name` (as `ArchimateModel` takes them),
997
- `features` (toolbar controls; all on unless set `false`) and `analysis` (`true` adds the analysis panel).
998
- The part builds the `ArchimateModel` and hands it to `ArchimateDiagramEditor` (its `model` option), with
999
- `fullPage: false` and region ids unique to the part, and `ARCHIMATE_SERVICES` (the editor's classes and
1000
- dialogs, imported directly) as its `workspace`.
1001
-
1002
- **View settings, warnings and labels on the editor's canvas.** A host hands the editor the current view's
1003
- resolved settings as `viewSettings: () => settingsManager.getEffectiveSettings(view, modelId)`
1004
- (`editor.viewSettings()`, {} without one). The base editor draws the view's background, title band and
1005
- watermark round the drawing from them (`applyViewDecorations` on the element holding the `<svg>`; a view
1006
- without a background goes back to the page's own). `editor.edgeWarning(edge)` says what is wrong with a
1007
- line, `{ message, highlight }` or null: the message is the line's tooltip, and with `highlight` the line is
1008
- drawn red and dashed unless it has its own colour or dashes (class `edge-warning`). The ArchiMate canvas
1009
- editor warns about a relationship the ArchiMate rules do not allow for its version (`ArchimateValidator`), as
1010
- the view's `invalidRelationships` setting says: 'highlight' (the default), 'tooltip' or 'off'. A label
1011
- expression (`${name} (${type})` or `{name}`, on the element's `label` or the instance's `labelOverride`) is
1012
- resolved by the instance (`ElementInstance#getDisplayName`, through `LabelHelper.resolveNodeLabel`), so the
1013
- tree, the dialogs and both editors show the same text.
1014
-
1015
- **Properties: the shape, and what it stands for.** The base editor's shape dialog (`editTemplateProperties`:
1016
- name, position, stacking, colours, text) and line dialog (label, sides, then only the style the line sets for
1017
- itself, `edge.styleProperties`) each end with a subclass's own section: `conceptFields(template)` and
1018
- `edgeConceptFields(edge)` give FormDialog fields, and `conceptCommand(template, values)` /
1019
- `edgeConceptCommand(edge, values)` the command applying them; the whole edit is one undo step. The ArchiMate
1020
- canvas editor's sections edit the model element (its own name, type, specialization among those registered for its
1021
- type, icon among its type's choices, documentation, typed properties) and the
1022
- relationship (kind, among those the rules allow and its own; documentation; typed properties), the same on every
1023
- view, through `EditViewItemCommand`; an empty box or list is unset, and a new type drops a stored layer. An
1024
- ArchiMate line's dashes and arrows come from its kind and a plain line's from its type, so neither is offered.
1025
-
1026
- **Type pictures.** The model tree shows each element with its type's glyph in its layer colour, as the palette's tiles
1027
- do, and a folder of one type in the Layers tree too (`typeGlyph(typeName, { size, version })` in
1028
- `archimate/typeGlyph.js`, the one drawing both use; a 3.2 type under ArchiMate 4 is drawn as 3.2 draws it).
1029
- `TreeBuilder`'s `iconFor(node)` gives a node a picture in place of its `icon` text.
1030
-
1031
- **Nesting.** In the ArchiMate canvas editor, dropping a box wholly inside an element's box nests it there (Archi's
1032
- nested elements): it moves with its parent, is raised above it, and an Archi file writes it inside it (whatever order
1033
- they were added in). Dragged out again, it is taken out. The move and the nesting are one undo step. On a canvas, a
1034
- block holds boxes the same way. Nesting one element in another then asks which relationship it stands for, among
1035
- Archi's nesting kinds (`NESTING_KINDS`: composition, aggregation, assignment, realization, specialization) that the
1036
- rules allow from parent to child, and none when the two already have one; **Add** puts it in the model, drawn as no
1037
- line (the nesting shows it), as its own undo step; **No relationship** adds none. The shared editor does the
1038
- finding: after a move, `DiagramEditor#nestingChanges(ids)` gives each moved shape's new parent (the innermost shape
1039
- that wholly holds it and `canHold` it), and a subclass supplies `parentOf`, `canHold`, `nestCommand`
1040
- (`NestInstanceCommand`) and `nested(changes)`; the template editor has no nesting. Model checks: **Check model…**
1041
- warns of an element drawn inside another with no such relationship (`nested-without-relationship`), and a
1042
- relationship a nesting shows is not reported as unused (`viewNestings(view)`, `nestingRelationships` in
1043
- `diagrams/nesting.js`).
1044
-
1045
- **Format painter.** The toolbar's **🖌** turns it on (Archi's Format Painter): with a shape selected it holds that
1046
- shape's format, otherwise the first shape clicked gives it; every shape clicked after gets it (one undo step each),
1047
- and nothing is selected or moved meanwhile. **Esc** or 🖌 again turns it off. A shape's menu has **🖌 Copy format**
1048
- and, once one is held, **Paste format** (to the selection, when the shape is part of it). A format is what
1049
- `Template#format()` reads from the shape's `static formatKeys` (paint and text style here; an ArchiMate shape's or
1050
- group's whole appearance), as the shape holds it: a colour it takes from elsewhere (an ArchiMate layer colour) is
1051
- left unset, so the shape painted gets its own default. `DiagramEditor#copyFormat(id)`, `pasteFormat(ids)` (each
1052
- shape takes the keys it has; a locked one stops it) and `setFormatPainter(on)`.
1053
-
1054
- **Properties of several shapes at once.** With more than one shape selected, a selected shape's menu offers
1055
- **Properties of N shapes** (double-clicking one opens it too): one dialog with the colours and text style
1056
- (`appearanceFields(template)`, the same fields as one shape's dialog) in which every field starts on **Keep**,
1057
- greyed out, showing the value the shapes share (or empty, with "Differs between them"). Untick Keep and what you
1058
- set goes to every shape; the rest stays as each one has it. The whole edit is one undo step; a locked shape in the
1059
- selection stops it. `DiagramEditor#editSelectionProperties(ids?)` opens it and
1060
- `selectionPropertiesCommand(templates, values)` is the edit (a value left `undefined` changes nothing); a
1061
- subclass adds its own section with `manyConceptFields(templates)` and `manyConceptCommand(templates, values)`.
1062
- The ArchiMate canvas editor's, when every selected shape is an element: their **type**, their **specialization**
1063
- when they share a type, and **Set property** (a name and value, set where an element has that property and added
1064
- where it doesn't), from `manyElementsFields` and `manyElementsEditCommands` in `archimate/elementProperties.js`.
1065
- `PropertyListEditor` (`PropertyListEditor.field(properties, { key })`) is the typed-property list both
1066
- ArchiMate dialogs use. Both editors fill the specialization catalog the same way:
1067
- `ArchimateSpecializationRegistry.loadFrom(store)` reads the saved catalog (`SPECIALIZATIONS_STORE`, this browser's),
1068
- or registers the defaults and saves them.
1069
-
1070
- **Boxes, the tree and the menus on the canvas.** Every ArchiMate view adds boxes besides elements through
1071
- `view.addBox(kindKey, at)` (one undo step), from the kinds its class lists in `BOX_KINDS`: Archi's Note and Image
1072
- on an ArchiMate view (`DIAGRAM_BOX_KINDS`), Block, Sticky and Image on a canvas (`CANVAS_BOX_KINDS`);
1073
- `canvasBoxMenuItems(view, at)` offers them on right-click in either editor. The canvas editor's element menu adds
1074
- **Generate View For This** and **Add Missing Relationships (n)** (`missingRelationshipsOf(boxId)`, drawn as one
1075
- step by `addMissingRelationships`), and ⚙ Settings → **Model settings…** when it has a `settingsManager`. The base
1076
- editor reports selection changes (`editor.onSelectionChange(({ templateId, edgeId }) => …)`, which returns an
1077
- unsubscribe); the canvas part keeps the model tree in step with it both ways and shows a canvas's hint, or the
1078
- selected box's. `SettingsManager#onSettingsChange` returns an unsubscribe too.
1079
-
1080
- **Specializations and viewpoints of your own.** ⚙ Settings → **Specializations…** and **Viewpoints…**, in both
1081
- editors, list the user's catalog with Edit and Delete and "+ New …". A specialization has a name, an icon, a
1082
- colour, a description and the element types it applies to (none: every type); it is saved in
1083
- `SPECIALIZATIONS_STORE` and offered on elements of those types (`specializationCatalogDialog({ version, store,
1084
- onChange })`). A viewpoint has a name (not a standard one's), a description and the element types it allows (at
1085
- least one); it is saved in `CUSTOM_VIEWPOINTS_STORE` (`CustomViewpoints.load()`, `save(list)`,
1086
- `dialog(options)`) and offered after the standard viewpoints, where it fades or hides the rest and limits the
1087
- palette like any other. There is no "Custom" placeholder any more. `CatalogDialog` is the list editor both use
1088
- (`{ title, noun, items, label, fields, validate, save, remove, onChange }`; deleting asks first), and FormDialog's
1089
- `checklist` field (`choices` `[[value, text, group]]`, reads as the chosen values) is the element type list,
1090
- grouped by layer as the palette groups them (`elementTypeChoices(version)`, from `elementTypeGroups`).
1091
-
1092
- **An element's menu in the model tree** (both editors; `elementMenuItems(element, host)`): **Properties…** edits the
1093
- element itself, the same on every view (`editElementProperties`: its name, type, specialization, icon,
1094
- documentation and typed properties, one undo step on the open view); **Show in View (n)** lists the views it is on
1095
- and opens the chosen one with it selected; **Add to "…"**, when the open view lacks it, shows it there in the middle
1096
- of what is in sight (`visibleCentre()`) with its relationships to what is shown; **Generate View For This**. The
1097
- element fields are one definition (`elementPropertyFields`, `elementEditCommand`), which the canvas editor's shape
1098
- dialog also uses for its Element section.
1099
-
1100
- **Pictures, analysis and filters on the canvas.** A picture file dropped on a view, or pasted, is an image box
1101
- there (`editor.addImage(dataURL, point)`, one undo step, selected; the base editor's `enableImageDrop`). With
1102
- `analysis: true` in its spec the canvas part shows the selected element's analysis (views it is in, its
1103
- relationships) under the palette, for an element chosen on the canvas or in the tree. The tree's own filters (its
1104
- "Filters…" dialog: text, types, specializations, properties) dim what they leave out on the canvas too:
1105
- `modelTree.matchingElementIds()` (null with no filter on) and `modelTree.onFilterChange(fn)` feed
1106
- `editor.setElementFilter`. The analysis names an element's layer as the tree files it (`treeLayerOf`).
1107
-
1108
- **Groups on the canvas.** An Archi group (a model folder with bounds on the view, `view.placedFolders`) is a
1109
- `ViewGroupTemplate`: Archi's box with a name tab (`TabbedBoxGlyph`), placed by its centre while its layout entry
1110
- (`view.folderLayout[id]`: top-left x/y, width, height, zIndex, `appearance`) keeps the corner, so the editor's
1111
- move, resize, restack and paint commands edit the view and undo there; renaming it renames the folder. Its paint
1112
- is imported into `appearance` and written back as boxes' is (a change written, the rest as the file had it). A
1113
- line to a group is drawn. Moving a container moves what it holds, however deep: the base editor's
1114
- `moversFor` adds `containedIn(id)` (none in the base) for each mover; the canvas editor's groups hold the
1115
- elements in their folder, the notes and pictures Archi drew in them (`instance.archi.parent`) and the groups
1116
- inside them, and a canvas block holds the boxes Archi drew in it. Deleting a group takes it off the view
1117
- (`RemoveGroupFromViewCommand`); what it held stays.
1118
-
1119
- **Derived relationships on the canvas.** With `nodeOptions.showDerived` (⚙ Settings → Show derived, off by
1120
- default; the canvas part's spec `showDerived`), the ArchiMate canvas editor draws each relationship
1121
- `DerivedRelationships.compute(model, { maxHops: 2 })` derives between two elements on the view, as a
1122
- `DerivedEdge`: dotted, labelled with its chain ("composition→assignment"), with "Derived: …" as its tooltip. It
1123
- is drawn only: `edge.editable` is false, so the editor neither selects, routes nor deletes it (deleting a box
1124
- leaves it alone). Any `Edge` can say so (`editable`, default true) and add to its tooltip (`tooltip`).
1125
-
1126
- **Selecting several shapes.** In the base editor (so the template editor and the ArchiMate canvas alike): drag a
1127
- box on empty canvas to select the shapes wholly inside it (Ctrl or Cmd adds them to the selection;
1128
- `editor.templatesInside(rect)`, `editor.selectTemplates(ids, { extend })`); Shift- or Ctrl-click adds one;
1129
- **Ctrl+A** selects every shape that is drawn (`selectableIds()`). Whatever is selected acts together, each as one
1130
- undo step: dragging any of them moves them all (with their groups and what they hold); the arrow keys nudge
1131
- them a grid step, one pixel without a grid, ten times as far with Shift (`nudgeSelection(dx, dy)`); **Delete**
1132
- removes them once confirmed, and every line joined to any of them once (`deleteSelection()`,
1133
- `deleteTemplatesCommand(ids)`; a subclass gives the per-shape part, `removeTemplateCommand(id)`, and the step's
1134
- name, `deleteLabel(ids)`); grouping, locking and the Arrange menu already took the selection. A fixed shape among
1135
- them stops a move or a delete.
1136
-
1137
- **One undo history per model.** As in Archi, a model has one undo stack, `model.history` (a `ModelHistory`),
1138
- that all its views share (`view.history` is it): every edit on any view, and the model's own changes, in the order
1139
- made. Adding a view (New, Duplicate, Generate View, a new canvas, a model template), renaming one and deleting one
1140
- are undo steps too (`AddViewCommand`, `RenameViewCommand`, `RemoveViewCommand`), as are a view's name and
1141
- documentation from its Properties…. Each step remembers the view in front when it was made, and undoing or redoing
1142
- it brings that view to the front, so the change is seen (Ctrl+Z on one tab can undo an edit made on another).
1143
- Unsaved changes follow the one history (`isDirty()`). A model a part has just made or opened starts with nothing
1144
- to undo.
1145
-
1146
- ```js
1147
- const model = new DiagramModel();
1148
- const a = model.createView('A'); // one undo step
1149
- model.renameView(a.id, 'First'); // another
1150
- model.history.undo(); // 'A' again
1151
- model.history.undo(); // the view is gone
1152
- ```
1153
-
1154
- **Find and replace.** Every `EditorPart` (the ArchiMate canvas part and the template editor) has an **Edit** menu
1155
- after File: **Undo**, **Redo** and **Find and replace…**, over the whole model (`model.search()`, a
1156
- `ModelSearch`, as `checker()` and `report()` are). The dialog (`FindReplaceDialog`) lists what matches as you
1157
- type, with **Match case**, **Whole words**, **Regular expression** (`$1` in the replacement is its first group)
1158
- and **Look in** names, documentation and property values; choosing a match shows it (its view, then the item
1159
- selected), and **Replace all** replaces every match as one undo step on the model's history. What is
1160
- searched: the model's name, documentation and properties and each view's title (never emptied), documentation and
1161
- properties (`ModelSearch`); in the template editor also each shape's name and line's label
1162
- (`DiagramModelSearch`); in an ArchiMate model also every element's and relationship's name, documentation and
1163
- properties, and the notes on each view (`ArchimateModelSearch`). A property whose value is a number or true/false is
1164
- left out. A notation's search adds its own texts with `viewItemTargets(view)` and `conceptTargets()`.
1165
-
1166
- ```js
1167
- const search = model.search();
1168
- search.find('customer', { wholeWord: true, fields: ['name'] }); // [{ kind: 'element', id, owner, fieldLabel: 'Name', text, count, target }, ...]
1169
- const cmd = search.replaceCommand('(\\w+) DB', 'DB of $1', { regex: true }); // a ReplaceTextCommand, or null
1170
- model.currentView.history.execute(cmd); // one undo step
1171
- ```
1172
-
1173
- **Transform names, rename a property, fit to text.** The **Edit** menu has **Transform names…**: Title Case,
1174
- Sentence case, split camelCase (OrderService → Order Service), camelCase, remove extra spaces, UPPER or lower case,
1175
- on every name in the model or on those containing some text (`NAME_TRANSFORMS`, `transformName(key, name)` in
1176
- `diagramModel/nameTransforms.js`; `model.search().transformCommand(key, text)`), one undo step, a view never left
1177
- without a name. **Rename property…** renames a property everywhere it is (the model, its views, and an ArchiMate
1178
- model's elements and relationships), one undo step; where something has both names, the old value fills the new one
1179
- only if that is empty (`search.renamePropertyCommand(from, to)`, `search.propertyNames()`, over
1180
- `search.propertyHolders()`, which a notation's search extends). Several elements' Properties also has **Remove
1181
- property** (one any of them has). A shape's menu has **Fit to text**: the shape (or the selection it is part of) sized
1182
- to its label, keeping its top-left corner, one undo step (`DiagramEditor.textSize(text, fontPixels, {padding, min})`
1183
- estimates it; `editor.textFit(template)` gives the room a shape needs, more for an ArchiMate element's badge).
1184
-
1185
- **HTML report.** File → **Export HTML report** (every `EditorPart` whose model has `report()`: the ArchiMate
1186
- canvas part and the template editor) saves the whole model as one HTML file that opens in any browser, with nothing
1187
- else to load: a tree on the left (Elements by layer, Relationships by kind, Views), a search box that keeps what
1188
- matches by name, type or documentation, and a page per item. A view's page has its picture, and clicking an element
1189
- on it opens that element's page; an element's page has its type, layer, documentation, properties, relationships
1190
- (each with the element at the other end) and the views it is on; a relationship's links its source and target. The
1191
- address says which page (`#element:id`), so Back and links work.
1192
-
1193
- The parts, each usable alone:
1194
-
1195
- ```js
1196
- import { ArchimateModel } from './src/charting/diagrams/ArchimateModel.js';
1197
- import { reportHtml, reportFileName } from './src/charting/report/reportHtml.js';
1198
-
1199
- const model = new ArchimateModel({
1200
- name: 'Shop',
1201
- elements: [{ id: 'a', name: 'Customer', type: 'business-actor' }, { id: 'b', name: 'Order app', type: 'application-component' }],
1202
- relationships: [{ id: 'r', kind: 'serving', source: 'b', target: 'a' }],
1203
- views: [{ id: 'v', name: 'Main', elementIds: ['a', 'b'] }],
1204
- });
1205
- const data = model.report().data(); // plain JSON: { title, home, sections, items }
1206
- data.items['element:a'].lists; // [{ title: 'Relationships', rows: [{ key: 'relationship:r' }] }, { title: 'Views', ... }]
1207
- const html = reportHtml(data); // the file's text
1208
- reportFileName(data.title); // 'shop-report.html'
1209
- ```
1210
-
1211
- `model.report({ pictures, title })` is a `ModelReport` (`src/charting/diagramModel/`): the model's page and a page
1212
- per view, for any `ViewSetModel`; `ArchimateModel` returns an `ArchimateModelReport`, which adds the elements and
1213
- relationships. A subclass adds items with `conceptItems()`, tree sections with `conceptSections()`, and a view's
1214
- facts and lists with `viewFacts(view)` / `viewLists(view)`. Pictures come from the editor:
1215
- `DiagramEditor#viewPicture()` is the current view as SVG cropped to what is drawn (no grid, selection or zoom), with
1216
- a box per shape whose `reportRef(template)` names its page (`{kind: 'element', id}` in the ArchiMate editor);
1217
- `part.viewPictures()` shows each view in turn to collect them. The page is written from `el()` specs by
1218
- `toHtml(spec)` (`src/workspace/dom.js`: the same spec, as escaped HTML text), and its script is `reportViewer`.
1219
-
1220
- **The File menu, grouped.** Every editor part's File menu (`EditorPart#fileMenuItems`) is: **New** ▸ (a view or
1221
- diagram, a canvas, from a template, a model), Open…, Save, Save As… (with a ModelsPart also Open… in a new tab and
1222
- Close model), **Import** ▸ (each format that imports, then the part's own: an Archi template, CSV with mapping),
1223
- **Export** ▸ (each format, the part's own, then the HTML report and the pictures), the part's own groups (the canvas
1224
- part's **Templates** ▸), and **This view** ▸ (its properties, duplicate, delete). A group of one item is that item
1225
- (named as the group says it: "Export HTML report"). A part adds to it with `newItems()`, `ownImportItems()`,
1226
- `ownExportItems()` and `ownFileItems()`; `EditorPart.menuGroup(label, items, { prefix })` and
1227
- `EditorPart.tidyMenu(items)` make the groups; `modelFileMenuItems(formats, { only: 'export' | 'import' })` gives
1228
- one direction's items.
1229
-
1230
- **Open, save, and unsaved changes.** An editor part with file formats (`EditorPart`, the ArchiMate canvas part) has
1231
- **Open…**, **Save** and **Save As…** at the top of File (**Ctrl+O**, **Ctrl+S**, **Ctrl+Shift+S**), kept by its
1232
- `part.file` (`ModelFile`). Open asks for a file and takes the first of the part's formats whose extension and
1233
- contents fit (`format.acceptsName(name)`, `format.recognizes(data)`: a `.xml` is Archi's or the Exchange Format by its
1234
- namespace); Save writes back to that file where the browser can (the File System Access API: Chrome and Edge),
1235
- otherwise it is Save As, which asks where (or, with no save dialog in the browser, downloads) in the open file's
1236
- format or the part's `saveFormat()` (Archi's for the canvas part). The part knows whether the model changed since it
1237
- was opened or saved: `model.markSaved()` / `model.isDirty()` / `model.onEdit(fn)` (`ViewSetModel`), from the model's
1238
- one history (`CommandHistory#stateToken`, the same whenever it is back in a state, so undoing back is clean again),
1239
- views added, renamed or deleted included. `part.fileLabel` ("shop.archimate •") shows in the part's toolbar and the page
1240
- title, and leaving the page with changes unsaved asks first. An import is not a file: Save then asks where.
1241
- `file.open()` is `file.choose()` (ask for a file and read it as a model, opening it nowhere yet) then
1242
- `file.adopt(chosen)`.
1243
-
1244
- **Several models open at once.** A `ModelsPart` (type `models`, one of `BUILTIN_PARTS`) holds one editor part per
1245
- model, each a tab of a work area (`WorkArea`), so each keeps its own file, unsaved state and undo history, and copy
1246
- and paste go between them. Its spec names the editor part type and what to open: `{ id, type: 'models', editorType:
1247
- 'archimate-canvas', models: [editor part spec, ...], newModel: editor part spec }`. An editor part it hosts
1248
- (`part.modelsHost`) has **New model**, **Open… in a new tab** (with file formats: the file is read first, and the
1249
- tab is made only for a model) and **Close model** at the top of File. A tab shows what the model is called (its file's name, else the model's name, as the toolbar does) and
1250
- • with unsaved changes; closing one that has them asks first (its × too); the last one closed leaves a new, empty
1251
- model; the page title follows the model in front. Keys an editor listens for on the page (undo, delete, Ctrl+S, ...)
1252
- act only for the one on screen (`Part#isShown()`, `DiagramEditor#isShown()`). `examples/archimate.html` and the
1253
- template editor's testbed host their editor this way:
1254
-
1255
- ```js
1256
- ws.addParts([{ id: 'models', type: 'models', editorType: 'archimate-canvas', newModel: { analysis: true },
1257
- models: [{ id: 'modeller', title: 'Sample model', elements: [...], views: [...] }] }]);
1258
- ws.registerPerspective('Modelling', { type: 'part', partId: 'models' });
1259
- ```
1260
-
1261
- **Align, distribute, match size, in every editor.** With two or more shapes selected, a shape's menu and the
1262
- toolbar's **Arrange** menu (there always, greyed out with too few) offer **Align** (Left, Centre, Right, Top,
1263
- Middle, Bottom), **Distribute** (Horizontally, Vertically; three or more) and **Match size** (Width, Height, Width
1264
- and height). Align and match size follow the primary shape (the one selected last), as in Archi; distribute keeps
1265
- the outermost two and makes the gaps between neighbours equal; match size keeps each shape's top-left corner. Each
1266
- is one undo step; what a moved container holds moves with it; a locked shape stops it. `DiagramEditor` has
1267
- `alignSelection(edge)`, `distributeSelection(direction)`, `matchSelectionSize(dimension)` and
1268
- `alignMenuItems()`; where the shapes go is worked out from their boxes alone by `align`, `distribute` and
1269
- `matchSize` (`src/charting/arrangement/alignment.js`), which return just the boxes that change:
1270
-
1271
- ```js
1272
- import { align, distribute, matchSize } from './src/charting/arrangement/alignment.js';
1273
- const boxes = [{ id: 'a', left: 0, top: 0, width: 100, height: 50 }, { id: 'b', left: 150, top: 40, width: 60, height: 30 }, { id: 'c', left: 400, top: 10, width: 120, height: 80 }];
1274
- align(boxes, 'top', boxes[1]); // [{ id: 'a', left: 0, top: 40 }, { id: 'c', left: 400, top: 40 }]
1275
- distribute(boxes, 'horizontal'); // [{ id: 'b', left: 220, top: 40 }]
1276
- matchSize(boxes, 'width', boxes[0]); // [{ id: 'b', width: 100, height: 30 }, { id: 'c', width: 100, height: 80 }]
1277
- ```
1278
-
1279
- **Import CSV with mapping.** File > **Import CSV with mapping…** (the ArchiMate canvas part) adds a spreadsheet's
1280
- rows to the open model (it never replaces it): choose which column holds the name, the type (any case, a title, or
1281
- CamelCase as Archi's elements.csv writes it; or one type for every row), an id, the documentation and which columns
1282
- are properties, and optionally one whose cells name (or give the ids of) elements each row's element is related to,
1283
- with the kind and direction. A row whose id, or name and type, is an element the model has updates its
1284
- documentation and properties instead of making a second; rows naming the same element are one; a relationship the
1285
- model has is not added twice, nor one the rules do not allow. The new elements can go on the open view, in a grid.
1286
- One undo step; rows that could not be read (no name, an unknown type, an element to relate to that is not there)
1287
- are listed after. The dialog suggests the columns from their names (Name, Type, ID, Documentation; the rest as
1288
- properties), so Archi's elements.csv imports as it is. The CSV reader (`parseCsvTable`, `parseCsvRecords`,
1289
- `guessDelimiter` in `src/csvTable.js`, which the CSV file format uses too) takes quoted cells with line breaks
1290
- and commas, semicolons or tabs. `planCsvImport(table, mapping, model)` (`archimate/csvMapping.js`) plans it and
1291
- `ImportRowsCommand` carries it out:
1292
-
1293
- ```js
1294
- const table = parseCsvTable(text);
1295
- const plan = planCsvImport(table, { name: 'Name', type: 'Type', properties: ['Owner'], related: 'Uses', relatedKind: 'serving', relatedDirection: 'in' }, model);
1296
- model.history.execute(new ImportRowsCommand(model, plan, { view: model.currentView })); // plan.problems: [{row, message}]
1297
- ```
1298
-
1299
- **draw.io export.** File > **Export draw.io (.drawio)** (the ArchiMate canvas part) saves every view as a page of one
1300
- draw.io (diagrams.net) file, in draw.io's own ArchiMate 3 shapes: each box with its place, size, colours (its
1301
- layer colour when it has none of its own) and text placement, a box nested in another as its child cell, notes,
1302
- groups and canvas boxes; each line with its relationship's look (access by its access type) and its bendpoints. An
1303
- element's or relationship's id, type, documentation and properties go on its cell's object, so draw.io keeps them.
1304
- It is export only (`ModelFileFormat#imports` is false: no Import item, and Open… does not offer it).
1305
- `toDrawioXML(model, { viewIds })` (`src/archimateDrawio.js`) writes it; `DRAWIO_ELEMENT_STYLES` and
1306
- `DRAWIO_RELATIONSHIP_STYLES` are the type-to-shape tables.
1307
-
1308
- **Roadmaps.** On the ArchiMate canvas, **View > Generate roadmap…** makes a new view of elements on a time line:
1309
- each element with a start and an end date (by default its "Start Date" and "End Date" properties; ISO dates, a
1310
- year and month, or day/month/year) is a bar from one to the other, under a header of years and months (short or
1311
- full names), with month columns behind; bars that overlap in time go in lanes below each other. It takes the
1312
- selected elements, or every element in the model with both dates; those without them are left out. The view comes
1313
- to the front; one undo step. `model.generateRoadmap(elementIds, { startProperty, endProperty, name, monthNames })`
1314
- returns `{ view, skipped }`; where everything goes is `roadmapPlan(items, options)` and dates are read by
1315
- `parseDate(text)` (`src/charting/diagrams/roadmap.js`):
1316
-
1317
- ```js
1318
- const { view, skipped } = model.generateRoadmap(model.elements().map(e => e.id), { startProperty: 'From', endProperty: 'To' });
1319
- ```
1320
-
1321
- A note's own fill colour is drawn (it was always yellow); one without keeps the yellow sticky look.
1322
-
1323
- **Add from text, in every editor.** Right-click the canvas → **Add from text…**: paste a list of names (from a
1324
- document, a spreadsheet, a mail) and choose their kind, and a shape is made for each, placed in a grid from where
1325
- you clicked, selected, one undo step. A name a line; a tab after it starts its documentation; blank lines are
1326
- skipped and bullets and numbering ("- ", "• ", "1. ", "2) ") taken off. A first line `types=<kind>,<kind>,...`
1327
- makes each tab-separated column that kind and each line a row of shapes (a kind by its key or its name, any case:
1328
- `types=Business Actor,application-component`). The template editor makes its palette's shapes; the ArchiMate
1329
- canvas makes new model elements with their boxes (the kinds its viewpoint allows; not on a canvas, whose boxes
1330
- are not elements). `parseShapeText(text, {defaultType, resolveType})` and `placeShapes(parsed, at)`
1331
- (`src/charting/textToShapes.js`) read and place the text; `DiagramEditor#addFromText(text, type, at)`,
1332
- `shapeTypes()` and `createShapesCommand(items)`, which a subclass overrides to make its own kind of shape.
1333
-
1334
- **Colour by, and legends, in every editor.** The **View** menu has **Colour by…** (a heat map): choose what to
1335
- colour by, then a colour for each of its values (a soft palette by default, `DiagramEditor.COLOUR_PALETTE`; "No
1336
- colour" leaves a value's shapes as they are), and whether to add a legend. Each shape takes its value's colour as
1337
- its own fill, so the colours are kept like any other (an Archi file included), and one undo puts them back. The
1338
- template editor colours by the kind of shape; the ArchiMate canvas by **Layer**, **Type**, **Specialization** or any
1339
- **property** its elements have (notes and pictures have no value and stay as they are). A legend is a title and a
1340
- box per value in its colour, in a row under the drawing (notes; on a canvas, stickies). **Add legend of the kinds
1341
- shown** adds one for the kinds of shape (ArchiMate: the element types) in the colour of each's first shape. A
1342
- locked shape keeps its colour. `colourKeys()`, `colourValue(template, key)`, `colourValues(key)`,
1343
- `colourByCommand(key, colours, { legend })`, `kindLegendEntries()` and `legendCommand(entries, title)`, which a
1344
- subclass overrides to say what its shapes are coloured by and what its legend boxes are:
1345
-
1346
- ```js
1347
- editor.colourByCommand('property:Lifecycle', { invest: '#98fb98', maintain: '#ffc300', retire: '#ffa07a' }, { legend: true });
1348
- ```
1349
-
1350
- **Layouts, in every editor.** The toolbar's **Arrange** menu has **Layout** (**Layout the view** with fewer than two
1351
- shapes selected): **Grid**, **Circle**, **Tree, top down**, **Tree, left to right** (along the lines between the
1352
- shapes: a parent centred over its children; a shape no line reaches is a root), **Force-directed** (lines pull,
1353
- shapes push apart; the same input gives the same result) and **By kind, a row each** (the shape class in the
1354
- template editor, the layer on the ArchiMate canvas). The shapes keep the top-left corner of the area they took;
1355
- what a container holds goes with it; one undo step; a locked shape stops it. On the ArchiMate canvas, an element
1356
- with parts on the view (the targets of its composition and aggregation relationships) has **Nest its parts inside**
1357
- in its menu: each part nested in it, theirs in them however deep, in a grid below its label, the parent grown to
1358
- hold them. `DiagramEditor#layoutShapes(kind, ids?)`, `layoutCategory(template)`, `partsOf(id)` and
1359
- `nestPartsInside(id)`; where shapes go comes from `layout(kind, boxes, {edges, categoryOf, gap})` and
1360
- `nestLayout(rootId, partsOf, boxOf)` (`src/charting/arrangement/layouts.js`), from the boxes alone:
1361
-
1362
- ```js
1363
- import { layout } from './src/charting/arrangement/layouts.js';
1364
- const boxes = [{ id: 'a', left: 0, top: 0, width: 100, height: 50 }, { id: 'b', left: 0, top: 100, width: 100, height: 50 }, { id: 'c', left: 0, top: 200, width: 100, height: 50 }];
1365
- layout('tree-down', boxes, { edges: [{ source: 'a', target: 'b' }, { source: 'a', target: 'c' }], gap: 40 });
1366
- // [{ id: 'a', left: 70, top: 0 }, { id: 'b', left: 0, top: 110 }, { id: 'c', left: 140, top: 110 }]
1367
- ```
1368
-
1369
- **Copy and paste, in every editor.** **Ctrl+C** copies the selection, **Ctrl+X** cuts it (one undo step), **Ctrl+V**
1370
- pastes (a little further along each time, selected), **Ctrl+D** duplicates without touching the clipboard; a shape's
1371
- menu has Cut, Copy and Duplicate and the canvas menu **Paste here** (its top-left corner where you clicked). The
1372
- clipboard is the page's (`DiagramClipboard`), so a copy pastes into another view, tab or model; an editor pastes only
1373
- copies of its own kind (`static clipboardKind`: 'diagram' for the template editor, 'archimate' for ArchiMate views).
1374
- An editor gives what a copy is (`copyPayload(ids)`) and how it is made again (`pasteCommand(payload, { dx, dy })`):
1375
- the template editor copies each shape (its class and `serialize()`) and the lines between them; an ArchiMate view
1376
- copies its items (`view.toJSON({ itemIds })`: boxes, the lines and relationships between them, the elements and
1377
- relationships they show) and pastes them as new boxes for the **same** elements (`view.pasteCommand(data)`,
1378
- `PlaceItemsCommand`), adding to the model what it lacks (a paste from another model) and to a view's element list
1379
- what it does not list. Groups are not copied (they are folders).
1380
-
1381
- **Mouse buttons, in every editor.** A left drag on empty canvas draws the selection box; the **right button drags
1382
- the view** (a right-click that does not move still opens the menu), as do the middle button and Shift+left (Alt+left
1383
- in the main ArchiMate editor). In the main ArchiMate editor (the graph engine) the box selects nodes too
1384
- (`graph.selectNodes(ids, { extend })`, kept on the engine across redraws and marked with a glow), and dragging one of
1385
- them moves them all as one undo step.
1386
-
1387
- **Emphasis: highlight, viewpoint, filter.** A shape or line can be drawn as usual, dimmed (opacity 0.3),
1388
- faded (0.2) or not at all (hidden), and only how it is drawn changes. Each reason is an `EmphasisRule`
1389
- (`template(template, editor)` and `edge(edge, editor)` give `null`, `'dimmed'`, `'faded'` or `'hidden'`);
1390
- `editor.emphasisRules()` lists the active ones, and the strongest level any of them gives wins
1391
- (`templateEmphasis`, `edgeEmphasis`; a line to a hidden shape is hidden). A dimmed or faded item is drawn in
1392
- a `<g opacity data-emphasis>` with its `data-template-id` or `data-edge-id`. The base editor's rule is the
1393
- highlight: **View → Highlight…** (`editor.setHighlight(text)`, `HighlightRule`) dims every shape whose name
1394
- does not contain the text, in any case, in the template editor and the ArchiMate canvas alike. The ArchiMate canvas
1395
- editor adds `ViewpointRule`: with `nodeOptions.viewpoint` (⚙ Settings → Viewpoint), elements of types the
1396
- viewpoint does not allow are faded, or hidden with `viewpointMode: 'hide'` (⚙ Settings → Outside the
1397
- viewpoint), a line with one end outside is faded and one with both outside hidden; notes and canvas boxes are
1398
- left alone. It also adds `ElementFilterRule` (`editor.setElementFilter(elementIds)`, null clears), which
1399
- dims the model elements a filter did not keep. The palette then offers only the types the viewpoint allows
1400
- (`new ArchimatePalette({ viewpoint })`).
1401
-
1402
- ```js
1403
- editor.setHighlight('order'); // the rest dimmed
1404
- canvasEditor.nodeOptions = { ...canvasEditor.nodeOptions, viewpoint: 'Organization', viewpointMode: 'fade' };
1405
- canvasEditor.templateEmphasis(appComponentShape); // 'faded'
1406
- ```
1407
-
1408
- **View menu, image export, model check, generated views.** These live in the `DiagramEditor` base, so the
1409
- template editor (the testbed) and both ArchiMate editors have them. The **View** menu
1410
- (`editor.viewMenuItems()`) has **Zoom to fit** (`editor.zoomToFit()`: everything drawn in sight, in the
1411
- drawing's own units; `ViewFit` computes it, as a viewBox for the graph engine or a scale and pan for the
1412
- template editor), **Reset zoom**, **Highlight…** (below), and, when the editor has a `model`, **Check model…**
1413
- (`editor.showModelCheck()`). The checks are the model's own (`model.checker(options)`): a `ViewSetChecker` for
1414
- any view set (empty views; an empty template is not one), and for an ArchiMate model an `ArchimateModelChecker`
1415
- that adds relationships the specification does not allow, likely duplicates (elements of one type whose names
1416
- have the same words, ignoring case, punctuation, word order and filler words such as "of" and "the":
1417
- `nameKey(name)` and `duplicateGroups(elements)` in `diagrams/duplicates.js`), elements and relationships on no
1418
- view, nested elements with no relationship, and names or documentation with unprintable characters (control
1419
- characters, zero-width spaces; tabs and line breaks are fine in documentation). Each check is a method listed by
1420
- `checks()`, so a subclass changes the set; choosing a finding shows it (`editor.revealFinding(finding)`: its view,
1421
- its shape, or in the ArchiMate tree), and a finding can offer actions beside it (`editor.findingActions(finding)`).
1422
- On the ArchiMate canvas, likely duplicates have **Merge…**: choose the element to keep (each choice says how many
1423
- views and relationships use it) and the others are merged into it, one undo step (`model.mergeCommand(keepId,
1424
- otherIds)`, a `MergeElementsCommand`). The kept element takes their relationships, their boxes on every view, the
1425
- documentation and properties it lacks and a specialization if it has none; a relationship left joining it to
1426
- itself, or the same as another, goes, its lines drawn as the other. The **File** menu (`EditorPart`'s, and the ArchiMate editor's) exports the current view
1427
- as an image: PNG, SVG or a copy to the clipboard (`editor.exportImage('png' | 'svg' | 'copy')`, without the grid
1428
- or selection; the ArchiMate editor's is off with `features.exportImage: false`). **Generate View For This**, on
1429
- an element's menu in the diagram or the tree, makes a view of the element with everything related to it
1430
- around it and every relationship between them (`model.generateView(elementIds, { name, related })`, Archi's
1431
- "Generate View For").
1432
-
1433
- ```js
1434
- model.checker({ version: '3.2' }).run(); // [{ code, severity, kind, ids, message }, ...]
1435
- model.generateView(['customer']); // a new current view: the customer and what it relates to
1436
- ```
1437
-
1438
- **Properties of an element, note or line.** Double-click it, or **Show/Edit Properties…** on its menu. The
1439
- dialog's Object section is the concept (name, type, documentation as "description", specialization,
1440
- properties), the same on every view; its Diagram section is how this view draws it: label, fill colour, line
1441
- colour, text colour and icon, and a **Text** section from `TextStyle` (font, text size, bold, italic, alignment,
1442
- position; the same fields the template editor shows). Bold and italic go to Archi in its font; Archi has no
1443
- underline, so it is offered for template shapes only. Save
1444
- keeps the changes where they live: the concept's in the model (a note's, image's or plain line's in the item
1445
- itself), the rest on the view's instance, as one undo step on the view (`EditViewItemCommand`;
1446
- `editor.editItemProperties(item, kind)` opens it, `viewItemChanges(kind, before, after)` sorts an edit). A
1447
- line's colour is this view's. "No colour" (`none`) draws no fill or no outline; an element's icon then stays
1448
- black.
1449
-
1450
- **Views.** Right-click a view's tab, or its node in the model tree, for **Properties…**, **Duplicate**
1451
- and **Delete** (the last view stays); the File menu starts with **New view…** and the current view's
1452
- items. Double-clicking empty canvas, or the canvas menu's **View Properties…**, opens the same
1453
- properties. `SettingsViewPropertiesDialog` edits the view's name (through `model.renameView`) and
1454
- documentation, then the view's level of the settings cascade: routing, grid size and grid shown under
1455
- Diagram, and background colour and image, a title band (text, position) and the watermark under
1456
- Appearance. A cascading setting may inherit, and shows the value and the level it would come from. The diagram draws the view's resolved settings (background, title band, watermark).
1457
- A filter (the tree's Filters…) is matched by model element, so it treats every view, a duplicate
1458
- included, the same way, and never dims notes or images.
1459
- A duplicate is independent: every element and relationship instance is copied with a new id, each
1460
- relationship re-linked to the copied ends, with the element filter, viewpoint, documentation, view
1461
- settings and folder layout; its undo history starts empty. View documentation round-trips through
1462
- Archi export and import. The editor follows its model, so the same happens however a view is
1463
- changed: `editor.model.duplicateView(id)` opens and shows the copy, deleting a view closes its tab.
1464
-
1465
- **Canvases and templates.** As in Archi, canvases (`CanvasView`) have a **Canvases** folder of their own in
1466
- the model tree, keeping Archi's sub-folders, and **Views** lists only the other views (`model.diagramViews()`,
1467
- `model.canvases()`, `model.templates()`). The **Templates** folder holds the model's own templates (canvases
1468
- marked `template`, saved in the `.archimate` file) and a **Library** of templates kept across models: a
1469
- `CanvasTemplateLibrary` (a `TemplateLibrary`, the base any kind of view's library extends: entries, built-ins
1470
- `DEFAULTS`, storage under `STORAGE_KEY`, change events), by default in this browser's storage (the editor's `templateLibrary` option takes
1471
- another, or `null` for none). Right-click Canvases for **New canvas…** and **New canvas from template**; a
1472
- canvas for **Save as model template…** and **Save to template library…**; a template for **New canvas from
1473
- this**; Templates or Library for **Import Archi template…**, which reads Archi's `.archicanvas` and
1474
- `.architemplate` files (a zip of a manifest, a thumbnail and the template's model, pictures included) into the
1475
- library. The File menu offers the same. `CanvasTemplateMenu` builds these items, as `ViewMenu` does for views;
1476
- its `askName`, `chooseFile` and `report` options replace the dialogs. A canvas made from a template gets new
1477
- ids and the template's boxes and lines; the boxes become the canvas's background (a locked `BackgroundGroup`,
1478
- behind everything added later; see "A background" under grouping and locking).
1479
-
1480
- **Drawing on a canvas: Archi's palette, lines and hints.** On a canvas, right-click empty space for **Add
1481
- Block**, **Add Sticky** and **Add Image…** (a picture file): the box is centred where you clicked, one undo step
1482
- (`canvas.addBox(kind, at, { name, iconUrl })`; the kinds, with Archi's classes and looks, are `CANVAS_BOX_KINDS`,
1483
- each a `CanvasBoxKind`; `canvasBoxMenuItems(view, at)` is the one menu definition both ArchiMate editors use).
1484
- A line drawn between two boxes is a canvas connection (Archi's `CanvasModelConnection`), not a relationship:
1485
- which a line is, is the view's rule (`view.linesAreRelationships(a, b)`: both ends model elements), so a line
1486
- from a note on an ArchiMate view is a plain line too (`view.connect(a, b)` draws one, `view.removeRelationship(id)`
1487
- takes it off; one undo step each). A **Hints** panel in the drawing's corner (`HintPanel`, folds to its header)
1488
- shows the canvas's hint, and a box's own when it is clicked (`canvas.hint`, `canvas.hintFor(id)`; `ArchiHint`
1489
- reads Archi's `hintTitle`/`hintContent` from any record and turns the HTML into plain text: it is never
1490
- inserted as HTML). **Edit Hint…** on a box, and **Edit Canvas Hint…** on empty canvas, set a hint as a title and
1491
- plain text, one undo step (`EditHintCommand`, on anything with an `archi` record; `ArchiHint.write` stores the
1492
- text as escaped HTML paragraphs, as Archi keeps it; `hintMenuItems(view, itemId)` is the one menu definition). In
1493
- the canvas editor, a canvas's side palette is Archi's (`CanvasPalette`, a kind of `ArchimatePalette`: Block,
1494
- Sticky, Image, clicked or dragged in), and its plain lines are drawn and edited like any line (`ViewLineEdge`).
1495
-
1496
- ```js
1497
- const goal = canvas.addBox('block', { x: 300, y: 200 }, { name: 'Goals' });
1498
- const idea = canvas.addBox('sticky', { x: 600, y: 200 }, { name: 'Idea' });
1499
- canvas.connect(idea.id, goal.id); // a CanvasModelConnection
1500
- canvas.hintFor(goal.id); // { title, content, text } or the canvas's
1501
- ```
1502
-
1503
- **Placing a template, and saving part of a canvas.** Right-click empty canvas for **Place template ▸** (the
1504
- model's templates, then the library's): its boxes and lines go where you clicked, as their own locked
1505
- background group, one undo step (`canvas.placeTemplate(source, { at, name })`, a `PlaceItemsCommand`), in front
1506
- of any earlier background and behind everything else. Right-click a box for **Save as template ▸** (model or
1507
- library): the box with its group (in the canvas editor, the selection with its groups), and the lines between
1508
- them (`canvas.toJSON({ itemIds })`, `model.saveAsTemplate(viewId, title, { itemIds })`; `Arrangement#only(ids)`
1509
- keeps their groups and locks). `CanvasTemplateMenu` builds these items (`placeTemplateItems(viewId, at)`,
1510
- `saveAsTemplateItems(viewId, { itemIds })`) for both ArchiMate editors; the template editor's base offers the
1511
- hooks (`shapeMenuExtras(id)`, `canvasMenuItems(at)` for empty canvas).
1512
-
1513
- ```js
1514
- canvas.placeTemplate(library.get('builtin-kanban').view, { at: { x: 400, y: 300 } }); // locked, behind
1515
- model.saveAsTemplate(canvas.id, 'Lanes', { itemIds: [laneA, laneB] }); // a model template of two boxes
1516
- ```
1517
-
1518
- The library starts with built-in templates, so there is something to choose before anything is imported:
1519
- SWOT analysis, PESTLE analysis, Stakeholder map (power and interest), Priority matrix (urgent and
1520
- important), Retrospective, Kanban board and Architecture decision (`DEFAULT_CANVAS_TEMPLATES`). They are
1521
- written for RenderEngine; each block carries a hint (Archi's `hintTitle`/`hintContent`), so a canvas made
1522
- from one explains itself in Archi too. Built-ins come first, are marked `builtIn`, are not stored, and cannot
1523
- be renamed or removed; `new CanvasTemplateLibrary({ defaults })` replaces them, and `defaults: []` gives none.
1524
- `canvasTemplate({ id, name, description, blocks })` makes an entry from a list of blocks.
1525
-
1526
- ```js
1527
- const library = new CanvasTemplateLibrary(); // or { store: { load(fallback), save(value) } }
1528
- model.newCanvasFromTemplate(library.get('builtin-swot').view, 'Our SWOT');
1529
- const [bmc] = library.importArchiTemplate(bytes); // an .archicanvas file's bytes
1530
- const canvas = model.newCanvasFromTemplate(bmc.view, 'Our business model'); // current view
1531
- model.saveAsTemplate(canvas.id, 'Business model template'); // a model template; current view unchanged
1532
- library.add({ view: canvas.toJSON(), name: 'Mine' }); // a library template
1533
- ```
1534
-
1535
- Archi's own canvas templates are not bundled (their content is licensed CC BY-SA); import them from an
1536
- Archi install's `plugins/com.archimatetool.canvas_<version>/templates` folder.
1537
-
1538
- **`ArchimateDiagramEditor`** is a reusable, self-contained class that encapsulates the complete lifecycle
1539
- of an ArchiMate modeling UI: DOM structure creation, toolbar UI generation, model management, tree
1540
- navigation, diagram rendering with styling, export/import, and control wiring. It is exported from both
1541
- `RenderEngine` and `RenderEngineWorkspace`.
1542
-
1543
- ### Self-contained design
1544
-
1545
- The class owns **everything**: DOM structure, CSS styling, toolbar controls, event handling, and diagram
1546
- rendering. The host HTML needs only:
1547
- - One outer container div (e.g., `<div id="app"></div>`)
1548
- - Model data (elements, relationships, views)
1549
- - Feature configuration
1550
-
1551
- ### Setup (with automatic toolbar)
1552
-
1553
- ```javascript
1554
- new RenderEngineWorkspace.ArchimateDiagramEditor({
1555
- elements: [...],
1556
- relationships: [...],
1557
- views: [...],
1558
- document,
1559
- appContainerId: 'app', // Class generates toolbar, main, tree, diagram divs
1560
- features: { // Toolbar is auto-generated based on these flags
1561
- version: true,
1562
- viewpoint: true,
1563
- distinctShapes: true,
1564
- showDerived: true,
1565
- exportCSV: true,
1566
- importCSV: true,
1567
- exportArchimate: true,
1568
- importArchimate: true,
1569
- },
1570
- workspace: window.RenderEngineWorkspace,
1571
- }).render();
1572
- ```
1573
-
1574
- ### Setup (legacy: manual HTML + wireControls)
1575
-
1576
- If you prefer to supply your own HTML with existing controls, omit `features` and use `wireControls()`:
1577
-
1578
- ```javascript
1579
- const editor = new RenderEngineWorkspace.ArchimateDiagramEditor({
1580
- elements: [...],
1581
- relationships: [...],
1582
- views: [...],
1583
- document,
1584
- appContainerId: 'app', // or provide pre-made divs:
1585
- toolbarContainerId: 'toolbar', // treeContainerId: 'tree',
1586
- treeContainerId: 'tree', // diagramContainerId: 'diagram'
1587
- diagramContainerId: 'diagram',
1588
- workspace: window.RenderEngineWorkspace,
1589
- });
1590
-
1591
- // Wire controls for manually-created HTML
1592
- editor.wireControls();
1593
- editor.render();
1594
- ```
1595
-
1596
- ### Core methods
1597
-
1598
- - **`render(options)`** — Render the current view's diagram with all options, styling, and position persistence. Called automatically on control changes if auto-toolbar is enabled.
1599
- - **`savePositions()`** — Save element positions before a view switch.
1600
- - **`exportCSV()`** — Export model to CSV format (elements + relationships). Returns a `Blob`.
1601
- - **`importCSV(elementsCSV, relationshipsCSV)`** — Import the two CSV tables and replace the model: its elements and relationships, with one view of them all ("Imported elements"; a CSV holds no views). `importCSVText(text)` takes the one file `exportCSV()` writes.
1602
- - **`replaceModel(model)`** — Edit another `ArchimateModel` in place of this one (every import ends here), closing the old views' tabs.
1603
- - **`exportArchimate()`** — Export the model with every view as `.archimate` XML. Returns XML string (pictures stay `data:` URLs).
1604
- - **`exportArchimateFile()`** — Export as Archi saves it: `toArchiFile`'s `{ data, archive, mimeType }`, a zip archive when a view holds pictures. The File menu's Export uses it.
1605
- - **`importArchimate(file)`** — Import an Archi file, as XML text or as the file's bytes (plain or archived), and replace the entire model.
1606
- - **`exportExchange()`** — Export the model as an Open Group Exchange Format file (XML text); its ArchiMate views are written, canvases and sketches are not. The File menu's "Export Exchange Format (.xml)" (feature `exportExchange`) downloads it.
1607
- - **`importExchange(xml)`** — Import an Exchange Format file and replace the entire model (File menu, feature `importExchange`).
1608
- - **`fileFormats`** (constructor option, default `ARCHIMATE_FILE_FORMATS`) — The file formats the File menu exports and imports; each is offered when its `features` flags are on (`export<featureName>`, `import<featureName>`).
1609
- - **`wireControls(options)`** — Wire up control event listeners for manually-created HTML controls (version, viewpoint, distinct shapes, show derived, manage dialogs, filter). Auto-renders on change. Optional callbacks for custom handlers.
1610
- - **`setupImageDragDrop()`** — Enable drag-drop and paste image support on diagram.
1611
- - **`setDependencies(deps)`** — Inject dialog functions and custom callbacks (dependency injection).
1612
- - **`saveSpecializations()`** — Persist specialization registry to localStorage.
1613
- - **`getOption(controlId, defaultValue)`** — Read a control value from the DOM.
1614
-
1615
- ### Properties (read-only)
1616
-
1617
- - **`model`** — The `ArchimateModel` instance.
1618
- - **`tree`** — The `ModelTree` navigator.
1619
- - **`currentView`** — The active `ArchimateView`.
1620
- - **`currentEngine`** — The active `RenderEngine` instance for the view.
1621
- - **`diagrams`** — The `DiagramTabs` tab manager. `diagrams.containerFor(viewId)` is the element a view is
1622
- drawn in. Nothing it builds carries an id (it uses the classes `diagram-area`, `diagram-tab-bar`, and
1623
- `diagram-view` with `data-view-id`), and each `SVGRenderer` prefixes its line-end marker ids
1624
- (`renderer.markerId('arrow')`, `renderer.markerUrl('arrow')` → `url(#re0-arrow)`), so several editors can
1625
- share a page.
1626
-
1627
- ### Styling & View Properties
1628
-
1629
- The `render()` method applies:
1630
- - **Background color** and **background image** to the diagram container.
1631
- - **Title** (top or bottom, or hidden).
1632
- - **Watermark** with three repeat modes: `'none'` (centered), `'repeat'` (diagonal), `'tile'` (canvas background).
1633
-
1634
- All are properties of the current view's model object and persisted with the view.
1635
-
1636
- All styling (page layout, containers, toolbar controls, tree nodes) is **generated by the class via CSS**—
1637
- no stylesheets needed in host HTML.
1638
-
1639
- ### Feature flags (auto-toolbar mode)
1640
-
1641
- When a `features` config is provided, the class builds its toolbar from `menuBarItems()`, rendered by
1642
- the shared `MenuBar` (see "Toolbars and dialogs" below):
1643
-
1644
- - **`search`** — Regex-based element filter with magnifying glass icon (🔍) above the tree. Real-time filtering as user types. Accepts regex patterns (case-insensitive). Clear with backspace to restore full diagram. Also searches documentation with `{ searchDocs: true }` option.
1645
- - **⚙ Settings** — a menu, always present, as in the template editor (`settingsMenuItems()`). It holds
1646
- the notation settings below, each shown with its current value, then **Model settings…** (the model
1647
- settings dialog, `showModelSettings()`: routing, grid, watermark).
1648
- - **`version`** — Settings ▸ Version ▸ ArchiMate 4 / 3.2
1649
- - **`viewpoint`** — Settings ▸ Viewpoint (every built-in and custom viewpoint) and ▸ Viewpoint mode (fade/hide)
1650
- - **`distinctShapes`** — Settings ▸ Distinct shapes: on/off
1651
- - **`showDerived`** — Settings ▸ Show derived: on/off
1652
- - **`exportCSV`**, **`importCSV`**, **`exportArchimate`**, **`importArchimate`** — entries in the **File** menu,
1653
- after the view items (New view…, Properties…, Duplicate, Delete)
1654
-
1655
- An element's right-click menu orders it with the same **⬍ Order** submenu as the template editor
1656
- (Bring to Front, Send to Back, Raise, Lower; `DiagramEditor.ORDER_OPERATIONS`), each one undo step on
1657
- the view.
1658
-
1659
- Set any to `false` to leave that setting out of the menu. Values live on the editor: `getOption(key)` / `setOption(key, value)`,
1660
- keys `archimate-version`, `archimate-viewpoint`, `archimate-viewpoint-mode`, `archimate-distinct-shapes`,
1661
- `archimate-show-derived`. Hosts that supply their own controls with those element ids (legacy
1662
- `wireControls()` mode) are read from the DOM when the editor holds no value.
1663
-
1664
- **Search feature examples:**
1665
- ```javascript
1666
- // Basic search by name
1667
- setFilter('business') // Shows only business-layer elements
1668
-
1669
- // Regex patterns
1670
- setFilter('actor|process') // Shows actors and processes (alternation)
1671
- setFilter('.*service') // Shows elements ending with 'service'
1672
- setFilter('^Customer') // Shows elements starting with 'Customer'
1673
-
1674
- // Include documentation in search
1675
- setFilter('critical', { searchDocs: true })
1676
- setFilter('urgent', { searchDocs: true })
1677
-
1678
- // Clear filter
1679
- setFilter('') // Restores full diagram
1680
- ```
1681
-
1682
- **Display behavior:**
1683
- - **Diagram view:** Highlights matching elements with gold glow (others at 30% opacity)
1684
- - **Tree panel:** Hides non-matching nodes (shows only matches)
1685
- - **Combined effect:** Focus on matches while preserving diagram context
1686
-
1687
- **Visual styles:**
1688
- - **Diagram matches:** Gold glow + 110% brightness
1689
- - **Diagram non-matches:** 30% opacity (visible but de-emphasized)
1690
- - **Tree non-matches:** Hidden completely (CSS display: none)
1691
-
1692
- ### Example: Minimal setup (recommended)
1693
-
1694
- HTML: just an app container.
1695
- ```html
1696
- <!DOCTYPE html>
1697
- <html>
1698
- <head><meta charset="UTF-8"><title>ArchiMate</title></head>
1699
- <body>
1700
- <div id="app"></div>
1701
- <script src="renderengine-workspace.umd.js"></script>
1702
- <script>
1703
- new window.RenderEngineWorkspace.ArchimateDiagramEditor({
1704
- elements: [...],
1705
- relationships: [...],
1706
- views: [...],
1707
- document,
1708
- appContainerId: 'app',
1709
- features: {
1710
- version: true, viewpoint: true, distinctShapes: true,
1711
- showDerived: true, exportCSV: true, importCSV: true,
1712
- exportArchimate: true, importArchimate: true,
1713
- },
1714
- workspace: window.RenderEngineWorkspace,
1715
- }).render();
1716
- </script>
1717
- </body>
1718
- </html>
1719
- ```
1720
-
1721
- For a page of its own, `fullPage` (default `true`) fills the viewport; `false` fills the app container
1722
- instead, as inside a workbench part. `model` hands in an `ArchimateModel` rather than building one from
1723
- `elements`/`relationships`/`views`. See `examples/archimate-legacy.html` for the workbench host, and
1724
- `test/charts.html` for integration with the workspace shell and dialog handlers.
1725
-
1726
- ## Diagram templates and palette categories
1727
-
1728
- `DiagramEditor` shapes are `Template` subclasses (`src/charting/templates/`). Geometry is
1729
- centre-based — `(x, y)` is the centre of a `width × height` box — and every template accepts
1730
- `{ x, y, width, height, rotation, zIndex, fillColor, fillOpacity, strokeColor, strokeWidth, textColor }`
1731
- and a text style: `fontFamily`, `fontSize` (a bare number is points), `fontWeight: 'bold'`, `fontStyle: 'italic'`,
1732
- `textDecoration: 'underline'`, `textAlign` ('left' | 'center' | 'right') and `textPosition` ('top' | 'middle' |
1733
- 'bottom') (`TextStyle`; unset, the name is drawn at 12px at the shape's own label point). Unset sizes and
1734
- colours come from the class's `static defaults`. A shape's properties dialog (double-click it) edits its name,
1735
- place, colours and text style (Font, Text size, Bold, Italic, Underline, Alignment, Position) as one undo step;
1736
- `serialize()` keeps the style. `TextStyle` is the one definition every editor uses: `fields(style)` and
1737
- `fromValues(values)` for a FormDialog, `fromNode`/`toNode` for a graph-renderer node (whose position is
1738
- `verticalAlign`), `svgAttributes(style)` and `block(style, box, lines)` for drawing (the graph renderer, the
1739
- ArchiMate canvas shapes and the template shapes all place text with it).
1740
-
1741
- ```js
1742
- new RectangleTemplate('r1', 'Billing', { fontFamily: 'Georgia', fontSize: '14pt', fontWeight: 'bold', textAlign: 'left', textPosition: 'top' });
1743
- ```
1744
-
1745
- | Family | Classes |
1746
- |---|---|
1747
- | Boxes | `RectangleTemplate`, `RoundedRectangleTemplate`, `StadiumTemplate`, `PredefinedProcessTemplate`, `ClassTemplate`, `ComponentTemplate`, `SwimlaneTemplate`, `PackageTemplate`, `BoxTemplate` (3D) |
1748
- | Round | `CircleTemplate`, `EllipseTemplate`, `FinalStateTemplate`, `CloudTemplate` |
1749
- | Polygons | `DiamondTemplate`, `TriangleTemplate`, `PentagonTemplate`, `HexagonTemplate`, `OctagonTemplate`, `StarTemplate`, `ParallelogramTemplate`, `TrapezoidTemplate`, `ManualInputTemplate` |
1750
- | Other | `CylinderTemplate`, `DocumentTemplate`, `NoteTemplate`, `DataStoreTemplate`, `ActorTemplate`, `LifelineTemplate` |
1751
-
1752
- A template draws its outline in `shapeElements(doc)`, or in `points()` when it extends
1753
- `PolygonTemplate`. The base class adds the group, rotation, label and editor wiring, and
1754
- `renderPreview(size)` reuses the same outline for the palette tile.
1755
-
1756
- **Edge endpoints.** `outline()` returns the visible outline as a polygon, with curves sampled
1757
- (the default is the box). `boundaryPoint(dx, dy)` is where a ray from the centre in that
1758
- on-screen direction meets the outline, rotation included. For concave outlines it uses the
1759
- outermost crossing. A shape whose outline isn't its box overrides `outline()` to match what it
1760
- draws. `EdgeGeometry.computeEdgePath` uses `info.boundaryPoint` whenever a node supplies one,
1761
- for `'auto'` anchors and for pinned sides (`sourceSide`/`targetSide`). Nodes without one keep
1762
- the `shape`-based model. `EdgeGeometry.endDirections(d)` returns the direction in which a path
1763
- arrives at each end.
1764
-
1765
- **Selecting and bending edges.** Selection is editor state (`selectedEdgeId` /
1766
- `selectedTemplateId`). Selecting a shape deselects the edge, and clicking empty canvas clears both
1767
- (`clearSelection()`). Only the selected edge shows handles: end dots, solid waypoints, and a
1768
- hollow handle mid-segment. Click or drag a hollow handle, double-click the line, or right-click →
1769
- *Add waypoint here* to add a bend. It is inserted into the nearest segment, so the route keeps
1770
- its order. Drag a waypoint to move it. Double-click it or right-click → *Delete waypoint* to
1771
- remove it, or use *Straighten* to remove all of them. Points snap to the grid while it is shown
1772
- and are converted through pan/zoom. The programmatic forms are `addWaypoint(edgeId, {x, y})`,
1773
- `removeWaypoint(edgeId, index)` and `clearWaypoints(edgeId)`. Each is one undo step.
1774
-
1775
- **Reconnecting.** Drag either end dot of the selected edge onto another shape to move that
1776
- end. Waypoints and style are kept, and that end's side pin resets to automatic. Dropping it on
1777
- empty canvas, or on the shape at the other end, cancels. This is one undo step
1778
- (`ReconnectEdgeCommand`).
1779
-
1780
- **Line style levels.** A line's style resolves through `EdgeStyleCascade`: the line itself,
1781
- then each level in `editor.edgeStyleLevels` (most specific first, `[{ key, label, style }]`),
1782
- then the schema default. A value a level doesn't set is inherited. `EDGE_STYLE_PROPERTIES` is the
1783
- single list of cascading properties:
1784
-
1785
- | Key | Values (default first) |
1786
- |---|---|
1787
- | `routing` | `straight`, `orthogonal`, `curved`, `polyline` (bends around other shapes) |
1788
- | `attach` | `floating` (outline, aimed at the other end), `sides` (middle of the facing side) |
1789
- | `lineStyle` | `solid`, `dashed`, `dotted` (same dash patterns as `GraphDiagram`, via `LINE_STYLE_DASH`) |
1790
- | `strokeColor`, `strokeWidth` | `#666666`, `2` |
1791
- | `startMarker`, `endMarker` | `none`, or an `EdgeMarker` key |
1792
-
1793
- `LINE_ONLY_PROPERTIES` lists `label`, `sourceSide` and `targetSide`; these never cascade. A
1794
- pinned side beats `attach`. `editor.editLineStyle(levelKey)` opens the defaults dialog for one
1795
- level, and the edge's *Properties* dialog edits the line. In both, every cascading field offers
1796
- *Inherit*, labelled with the value and the level it would come from. Changes go through
1797
- `SetStyleCommand` (one undo step; `undefined` = inherit). The editor never stores the levels
1798
- itself, so the host decides what a level is and where it persists:
1799
-
1800
- ```javascript
1801
- new DiagramEditor({
1802
- diagramContainerId: 'canvas',
1803
- edgeStyleLevels: [
1804
- { key: 'view', label: 'Diagram', style: view.edgeStyle }, // per diagram tab
1805
- { key: 'model', label: 'Model', style: model.edgeStyle }, // shared by the model's diagrams
1806
- { key: 'tool', label: 'Tool', style: toolDefaults }, // e.g. persisted in localStorage
1807
- ],
1808
- });
1809
- ```
1810
-
1811
- `MenuBar`'s Settings menu adds one *Line style: … defaults* entry per level.
1812
-
1813
- **Edge markers.** `edge.startMarker` / `edge.endMarker` hold an `EdgeMarker` key:
1814
- `arrow`, `openarrow`, `openarrowhalf`, `trianglehollow`, `diamond`, `diamondhollow`, `circle` or
1815
- `crowsfoot` (`EDGE_MARKERS`). `getEdgeMarker(value)` ignores case and punctuation, so
1816
- `"Crow's Foot"` resolves too, and so do ArchiMate's own arrowhead names (`diamond-hollow`,
1817
- `open-arrow-half`, …). A marker's `shapes(tip, dir, size, color, background)` returns element
1818
- descriptors whose tip is on the boundary and whose body extends back along the arriving
1819
- segment, so nothing is hidden under the node; hollow markers are filled with `background`
1820
- (default white) so the line does not show through them.
1821
-
1822
- **Glyphs.** Every node shape the graph engine draws (`rect`, `circle`, `diamond`, `note`,
1823
- `image`, `icon`, `person` and the ArchiMate notation shapes `pill`, `cube`, `chevron`, …) is a
1824
- `Glyph` subclass in `src/charting/glyphs/`, one per file. `glyphFor(key)` returns it (an unknown key
1825
- gives the circle) and `GLYPHS` maps every key. `glyph.parts(Glyph.context({R, size, color,
1826
- cornerStyle, strokeColor, strokeWidth, icon, iconUrl}))` describes the shape centred on (0, 0) as
1827
- `rect`/`circle`/`path`/`image`/`text`/`group` parts; `drawGlyph(renderer, parts)` draws them through
1828
- a renderer. A glyph whose `ownsInterior` is true gets no centred icon or initials on top.
1829
-
1830
- ```javascript
1831
- const parts = glyphFor('cube').parts(Glyph.context({ R: 30, size: { w: 120, h: 55 }, color: '#AFFFAF', strokeColor: '#000', strokeWidth: 1.2 }));
1832
- parts.children.length; // 3: front, top and side faces
1833
- ```
1834
-
1835
- `GlyphTemplate` is a template drawn by a glyph: a subclass names the glyph (`glyph()`) and its
1836
- context, and the template draws its parts as SVG (`glyphToSvg`; `glyphElement(part)` gives the tag and
1837
- attributes, by the SVG renderer's rules) and attaches lines to the glyph's `outline(ctx)`. Every glyph
1838
- has an outline: its box by default, the real shape for round, skewed and pointed ones.
1839
-
1840
- **ArchiMate in the template editor.** `ArchimateElementTemplate(instance, model, {nodeOptions})` and
1841
- `ArchimateEdge(instance, view)` put an `ArchimateView` into a `DiagramEditor`. Neither keeps a copy:
1842
- position, size, stacking and per-view paint are read from and written to the `ElementInstance`, the
1843
- route, pinned sides and per-line style to the `RelationshipInstance`, so the editor's move, resize,
1844
- waypoint, side and style commands edit the view and undo through `view.history`. A template looks as
1845
- the graph engine draws the element (`ArchimateDiagram.buildNode`: layer colour, corner style, type
1846
- glyph badge, or the full notation shape with `nodeOptions.useDistinctShapes`); notes and images
1847
- (`ViewObjectInstance`) draw their own glyph. A line's dashes and end markers come from its
1848
- relationship's kind and cannot be set on the line; routing, attach, colour and width can, and unset
1849
- inherits. Its ends come from the instance (re-linking an end is a model edit, not a line edit).
1850
-
1851
- ```javascript
1852
- const view = model.currentView;
1853
- const templates = new Map([...view.elementInstances.values()].map(i => [i.id, new ArchimateElementTemplate(i, model)]));
1854
- const edges = new Map([...view.relationshipInstances.values()].map(i => [i.id, new ArchimateEdge(i, view)]));
1855
- new DiagramEditor({ diagramContainerId: 'canvas', templates, edges, history: view.history }).render();
1856
- ```
1857
-
1858
- `ArchimateCanvasEditor({view, model, nodeOptions, chooseKind, ...DiagramEditor options})` is that
1859
- editor as a class: it rebuilds its templates and lines from the view on every render (so an undone
1860
- view edit shows at once), uses the view's history, and does through the view and model what stands
1861
- for something else in ArchiMate, each as one undo step:
1862
- - `addElement(type, point)`, and dropping an `ArchimatePalette` tile (`ARCHIMATE_TYPE_MIME`), creates a
1863
- model element named after its type and shows it;
1864
- - connecting two elements (`connect(sourceId, targetId)`) creates a relationship. Its kind is asked
1865
- for through `chooseKind(kinds, from, to)` (a promise or value; null cancels) among
1866
- `allowedKinds(sourceId, targetId)`, or without a chooser is the first of `KIND_PREFERENCE` that is
1867
- allowed (the dependency kinds before the structural whole/part ones). Notes and images take none;
1868
- - deleting an element takes it and its lines off the view; deleting a line takes the relationship
1869
- off; the model keeps both.
1870
-
1871
- `ArchimatePalette({version, onPick, document})` renders the element types grouped by layer
1872
- (`ArchimatePalette.groups(version)`: the notation's layers, then ArchiMate 4's common behaviour types),
1873
- each tile only its notation glyph in its layer colour, four to a row (`ArchimatePalette.TILE`; the name shows on hover); drag a tile onto the canvas or click it (`onPick`).
1874
-
1875
- `ArchimateCanvasPart` (workspace bundle, type `archimate-canvas`; `examples/archimate.html`, and the testbed
1876
- `test/archimate-canvas-testbed.html`) is the
1877
- modeller on this canvas: toolbar (File with the view items,
1878
- version, distinct shapes, zoom, connect, Settings), palette, one tab per view (Properties…, Duplicate,
1879
- Delete) and the canvas; connecting opens a picker of the allowed kinds. The model tree is on the left, with a search box
1880
- (the tree's text filter: it hides what it leaves out there and dims it on the canvas), and the palette on the right,
1881
- as in Archi. File offers New view…, New canvas… and New canvas from template (`EditorPart#newItems()`), the open
1882
- canvas saved as a template, and Import Archi template…. Dragging an element from the tree onto the canvas shows it there
1883
- with its relationships to the elements already shown, one undo step (`editor.showElement(elementId, point)`,
1884
- `ShowElementCommand`: only lines to or from the new box; redo adds the same lines). The File menu exports
1885
- and imports CSV, `.archimate` and the Exchange Format; an import replaces the part's model
1886
- (`part.replaceModel`) and the tree, canvas and menus follow it. Spec: `elements`, `relationships`,
1887
- `views`, `name`, `archimateVersion`, `distinctShapes`, `viewpoint`, `viewpointMode`.
1888
-
1889
- **File formats.** `ModelFileFormat` is one format a model is exported to and imported from: a subclass gives
1890
- `label`, `accept`, `fileName`, `readsBytes` (import gets the file's bytes, not its text), `featureName`, and
1891
- `export(model)` (`{ data, mimeType }`) and `import(data)` (a new model). `modelFileMenuItems(formats, { model,
1892
- onImport, enabled })` makes the File menu's Export and Import items, one group per format: export downloads
1893
- the file, import asks for one and hands the new model to `onImport` (a failure is reported and the model kept).
1894
- `ARCHIMATE_FILE_FORMATS` are `ArchimateCsvFileFormat` (both tables in one file; import lays all the elements
1895
- out in one view), `ArchiFileFormat` and `ExchangeFileFormat`; both ArchiMate editors read them.
1896
-
1897
- ```js
1898
- import { ARCHIMATE_FILE_FORMATS } from 'renderengine';
1899
- const [csv] = ARCHIMATE_FILE_FORMATS;
1900
- const { data } = csv.export(model); // '=== elements.csv ===\n...'
1901
- const copy = csv.import(data); // a new ArchimateModel, with the view "Imported elements"
1902
- ```
1903
-
1904
- **Model tree layers.** The tree's Layers folder files an element under its layer as the diagram draws it
1905
- (`treeLayerOf(element)`: its own `layer`, an IT type's, or its notation type's), in the order `TREE_LAYERS`;
1906
- anything else (junction, grouping, location, an ArchiMate 4 common type) is under Other, as in Archi. A
1907
- tree leaf dragged anywhere carries its element id as `MODEL_ELEMENT_MIME`.
1908
-
1909
- An instance's `appearance` (`fillColor`, `fillOpacity`, `strokeColor`, `strokeWidth`, `textColor`, the text style
1910
- keys `fontFamily`, `fontSize`, `fontWeight`, `fontStyle`, `textDecoration`; on a
1911
- relationship instance the line style keys) is this occurrence's own paint; the graph engine draws it
1912
- too. A template whose properties are computed reports what it holds itself through
1913
- `ownProperty(key)`, which `SetPropertiesCommand` uses so undo restores "unset" rather than the
1914
- computed value.
1915
-
1916
- Each ArchiMate relationship type gives its notation as line style values:
1917
- `ArchimateNotation.relationshipType('composition').edgeStyle()` is
1918
- `{ lineStyle: 'solid', startMarker: 'diamond', endMarker: 'none' }`; arrowheads come back as marker keys
1919
- (`realization` ends in `'trianglehollow'`).
1920
-
1921
- `DIAGRAM_TYPES` (19 categories in the groups General, Process, UML, Data, Infrastructure,
1922
- Business, Ideas) decides which templates the palette offers. `editor.initPaletteForDiagramType(container, key)`
1923
- renders a grouped category picker above a grid of draggable tiles. `editor.setPaletteCategory(key)`
1924
- switches category, and `onTemplateTypeChange(key)` (a constructor option) reports the switch.
1925
- Unknown keys fall back to `'generic'` (Basic Shapes).
1926
-
1927
- Adding a shape and a category (runs under Node — no DOM needed until render):
1928
-
1929
- ```javascript
1930
- import { PolygonTemplate, DIAGRAM_TYPES, getDiagramTypesByGroup } from './src/index.js';
1931
-
1932
- class ChevronTemplate extends PolygonTemplate {
1933
- static defaults = { width: 120, height: 50 };
1934
- points() {
1935
- const n = this.height / 2;
1936
- return [[this.left, this.top], [this.right - n, this.top], [this.right, this.y],
1937
- [this.right - n, this.bottom], [this.left, this.bottom], [this.left + n, this.y]];
1938
- }
1939
- }
1940
-
1941
- DIAGRAM_TYPES['value-chain'] = {
1942
- name: 'Value Chain', group: 'Business', description: 'Primary activities in sequence',
1943
- templates: [{ id: 'activity', name: 'Activity', factory: (id, x, y) => new ChevronTemplate(id, 'Activity', { x, y }) }],
1944
- };
1945
-
1946
- console.log(DIAGRAM_TYPES['value-chain'].templates[0].factory('a1', 100, 50).points().length); // 6
1947
- console.log(getDiagramTypesByGroup().find(g => g.group === 'Business').types.map(t => t.key));
1948
- // [ 'organizational-chart', 'value-chain' ]
1949
- ```
1950
-
1951
- ### Diagram models and the editor part
1952
-
1953
- `ViewSetModel` is the base of a model edited as several views: `views()`, `viewCount`, `getView`,
1954
- `currentView`, and `createView`, `duplicateView`, `renameView`, `deleteView` (the last view can't be
1955
- deleted; `canDeleteView` says so) and `switchTo`, each notifying `onChange` listeners. A subclass
1956
- makes its own kind of view in `_newView(id, title)`; a view has an `id`, a `title` and
1957
- `copy(id, title)`. `DiagramModel` and `ArchimateModel` both extend it.
1958
-
1959
- `ViewMenu(model, {document, noun, editProperties})` turns those operations into menu items:
1960
- `newViewItem()` (asks for a name) and `items(viewId)` (Rename…, Duplicate, and Delete after a
1961
- confirmation when allowed). Given `editProperties(view)`, a properties dialog that includes the name,
1962
- **Properties…** takes Rename's place. Tab strips, File menus and the ArchiMate model tree all use it,
1963
- and both editors pass a properties dialog, so their tabs offer the same **Properties…**, **Duplicate**
1964
- and **Delete**.
1965
-
1966
- `ViewPropertiesDialog({model, view})` is the base of those dialogs: the view's name and documentation;
1967
- a subclass adds `sections()` (more FormDialog fields) and `applySections(values)`.
1968
- `SettingsViewPropertiesDialog` adds the view's level of the SettingsManager cascade (the ArchiMate
1969
- editor's); `DiagramViewPropertiesDialog({cascade, levelKey, onStyleChange})` adds a diagram's
1970
- line-style defaults, the `levelKey` level of an `EdgeStyleCascade`, applied as one undo step on the
1971
- view's history (the template editor's). Both build their inheritable fields with
1972
- `inheritableFields(properties, own, inherited)` (and read them back with `readInheritableFields`)
1973
- from `SettingsCascade.js`, as `EdgeStyleDialog` does: each field shows its own value or offers
1974
- "Inherit", labelled with the value and the level it would come from. `MODEL_SETTINGS_PROPERTIES`
1975
- carries the labels, types and choices those forms use.
1976
-
1977
- The SettingsManager cascade resolves each of those settings through a view's own settings, then its
1978
- model's, then the tool's, then the default (`cascade(view, modelId)`; `getModelOverrides(modelId)` is
1979
- what the model level itself sets, `getModelSettings(modelId)` the model's resolved values). Global
1980
- Settings sets the tool's value and the model's (which may inherit the tool's); a view's Properties sets
1981
- the diagram's (which may inherit the model's). "Invalid relationships" (`invalidRelationships`:
1982
- `highlight`, `tooltip`, `off`) is set this way.
1983
-
1984
- ```js
1985
- settingsManager.saveToolSettings({ invalidRelationships: 'tooltip' }); // every model, unless it says otherwise
1986
- settingsManager.saveViewSettings(view, { invalidRelationships: 'off' }); // this diagram only
1987
- settingsManager.getEffectiveSettings(view, 'default').invalidRelationships; // → 'off'
1988
- ```
1989
-
1990
- `DiagramModel` holds a set of diagrams (`DiagramView`s) edited together. It adds the palette
1991
- category they share (`templateType`) and the model-level line style (`edgeStyle`). Each `DiagramView` owns its `templates`
1992
- and `edges` maps, a diagram-level `edgeStyle` and its own `CommandHistory`. `view.copy()` is
1993
- independent: shapes and lines are copied, and the history starts empty.
1994
-
1995
- Passing a view's maps and history to `DiagramEditor` binds the editor to that view: edits land
1996
- in the view directly, and a rebuilt editor carries on with the same undo history:
1997
-
1998
- ```javascript
1999
- import { DiagramModel, DiagramEditor, RectangleTemplate } from './src/index.js';
2000
-
2001
- const model = new DiagramModel({ templateType: 'flowchart' });
2002
- const view = model.createView('Context');
2003
- const editor = new DiagramEditor({
2004
- enableKeyboardShortcuts: false, // no DOM needed for this example
2005
- templates: view.templates, edges: view.edges, history: view.history,
2006
- });
2007
- editor.addTemplate(new RectangleTemplate('a', 'Box'));
2008
- console.log(view.templates.has('a'), model.duplicateView(view.id).title); // true 'Context (copy)'
2009
- ```
2010
-
2011
- Call `editor.dispose()` before discarding an editor on a page that lives on. Its keyboard
2012
- shortcuts and pan listeners are registered on `document`. Shortcuts ignore keys typed into
2013
- form fields.
2014
-
2015
- **Subclassing.** The undo shortcuts and every command use `editor.history`, an accessor a
2016
- subclass can redirect. `ArchimateDiagramEditor` maps it to `currentView.history` (the model's one history), and hands the same history to the graph engine it draws with. Canvas edits
2017
- (move, resize, bend points, pinned sides, drawing a relationship) are recorded there too, written
2018
- back to the view through the hooks above, and undo across re-renders. A relationship drawn between
2019
- two elements is created in the model and on the view, and its kind is asked for once; undo removes
2020
- it from both. The line's right-click **Delete from View** takes one relationship instance off the
2021
- view and leaves the model relationship.
2022
-
2023
- **Views.** `DiagramEditorState` is the abstract base of every diagram view: its own `history` and
2024
- the stacking operations below. A subclass says what stacks through `layerItems()`.
2025
- `DiagramView` (the template editor's tabs) stacks its templates. `ArchimateView` stacks its element
2026
- instances, and records adding and removing them as undo steps:
2027
- `view.addToView(instance, { element })` shows an `ElementInstance` (creating the model element
2028
- first when `element` is given, and widening `elementIds` to it), and `view.deleteElement(instanceId)`
2029
- takes one off the view. Undo of either reverses all of it. `ArchimateModel.addElement(el)` and
2030
- `removeElement(id)` are the model's side; position stays on the instance, so `addElement` does not
2031
- keep an element's `x`/`y`.
2032
-
2033
- Relationships work the same way. `view.missingRelationshipInstances(elementId)` lists the pairings
2034
- the view could draw but doesn't (each instance of the element against each instance at the other
2035
- end of its relationships), `view.addRelationshipsToView(instances)` adds them as one undo step, and
2036
- `view.removeRelationship(instanceId)` takes one off. `ArchimateModel.addRelationship(rel)` and
2037
- `removeRelationship(id)` are the model's side.
2038
-
2039
- What a view draws per occurrence lives on the instance, so two occurrences of one element or
2040
- relationship can differ:
2041
-
2042
- | Instance | Field | Meaning |
2043
- |---|---|---|
2044
- | `ElementInstance` | `x`, `y`, `zIndex` | position and stacking |
2045
- | `ElementInstance` | `size` | `{width, height}` or `{radius}` set on this view; `{}` = the element's own |
2046
- | `RelationshipInstance` | `waypoints` | this line's bend points; `null` inherits the model relationship's, `[]` is straight |
2047
- | `RelationshipInstance` | `sourceSide`, `targetSide` | pinned attachment side; `null` = automatic |
2048
-
2049
- A note or image belongs to one view, not the model, so it is a `ViewObjectInstance` (a subclass of
2050
- `ElementInstance`): it carries its own `content` (`{id, name, shape: 'note' | 'image', label, iconUrl,
2051
- href, width, height}`), shows whatever the element filter says, and never appears in the model tree.
2052
- A view also takes them as `viewObjects: [{content, x, y}]`, which is what an Archi import produces.
2053
-
2054
- ```javascript
2055
- view.addToView(new ViewObjectInstance({ id: 'n1', name: 'Remember', label: 'Remember', shape: 'note' }, 40, 300));
2056
- model.getElement('n1'); // undefined: the note is the view's
2057
- ```
2058
-
2059
- A view's `edgeLayout` (`{relationshipId: {waypoints}}`, what an Archi import produces) is read once,
2060
- at construction: it routes each instance of that relationship that has no route of its own.
2061
-
2062
- ```javascript
2063
- const model = new ArchimateModel({ elements: [{ id: 'a', name: 'A' }], views: [{ id: 'v', elementIds: ['a'] }] });
2064
- const view = model.views()[0];
2065
- view.addToView(new ElementInstance('note-1', 40, 60), { element: { id: 'note-1', name: 'Remember', shape: 'note' } });
2066
- view.history.undo(); // the note leaves the view and the model; elementIds is ['a'] again
2067
- ``` A subclass whose drawing engine owns zoom, pan and canvas clicks passes
2068
- `enableCanvasInteractions: false`. One that draws into another SVG overrides `_canvasSvg()` so
2069
- `_clientToDiagram` and `_snapToGrid` keep working.
2070
-
2071
- **Stacking order.** `BringToFrontCommand`, `SendToBackCommand`, `RaiseCommand` and
2072
- `LowerCommand` (all `ReorderCommand`s) work on any target with `layerItems()`: the editor's
2073
- templates, or any view's `layerItems()` (`view.bringToFront(id)` and similar record one undo step
2074
- on `view.history`). Raise and lower swap with the item actually drawn
2075
- above or below, ties and gaps included. Each operation renumbers the stack to consecutive
2076
- integers, and undo restores every previous value.
2077
-
2078
- `EditorPart` (workspace bundle) is the base of the workbench parts that edit a `ViewSetModel`: a
2079
- subclass builds its model from the part's spec in `createModel(spec)`, and gets `part.model`,
2080
- `part.viewMenu` and `fileMenu()` (the view items, then `fileMenuItems()`: export and import in each of
2081
- its `fileFormats()`, none by default, then the image export), and its editor disposed with the part. An
2082
- import goes through `replaceModel(model)`, which a subclass follows in `modelReplaced(model)`.
2083
- `DiagramEditorPart`, `ArchimateEditorPart` and `ArchimateCanvasPart` extend it.
2084
-
2085
- `DiagramEditorPart` (workspace bundle) puts this together: `MenuBar` (File creates and deletes
2086
- diagrams; Settings has the grid and line-style levels), the palette, a `ViewTabStrip` (right-click
2087
- a tab for its **Properties…**: name, documentation and the diagram's line style; **Duplicate**;
2088
- **Delete**) and the canvas. `part.edgeStyleLevels(view)` gives a diagram's line-style levels and
2089
- `part.showViewProperties(view)` opens its properties. Spec options: `templateType`,
2090
- `tabOrientation` (`'horizontal'`/`'vertical'`), `diagramTitle`, and `toolStyleKey`, the
2091
- localStorage key for tool-level line style (`null` keeps it in memory only), saved through
2092
- `LocalJsonStore`. Its levels are Diagram (`view.edgeStyle`), Model (`model.edgeStyle`) and Tool.
2093
-
2094
- ```javascript
2095
- const { DiagramEditorPart } = window.RenderEngineWorkspace;
2096
- ws.registerPart(DiagramEditorPart);
2097
- ws.addParts([{ id: 'editor', type: 'diagram-editor', title: 'Diagram Editor', templateType: 'flowchart' }]);
2098
- ws.getPart('editor').model.currentView; // seed or inspect content before or after mounting
2099
- ```
2100
-
2101
- The workspace bundle includes its own copy of the engine, so a page can hold two copies of every
2102
- class. Library code imports its collaborators directly rather than looking them up on
2103
- `window.RenderEngine`, and `CommandHistory.execute` accepts any object with `execute()` and
2104
- `undo()` rather than checking `instanceof Command`. A command built from either bundle therefore
2105
- works in either bundle's history.
2106
-
2107
- ### Toolbars and dialogs
2108
-
2109
- `MenuBar` renders a row of toolbar items and has no controls of its own. Each editor contributes its
2110
- items through `menuBarItems()`: the template editor gives zoom, connect mode and a ⚙ Settings menu (grid,
2111
- grid size, line-style levels), and `ArchimateDiagramEditor` gives its File menu and a ⚙ Settings menu
2112
- (notation and model settings). Each editor's ⚙ Settings menu reads `settingsMenuItems()`, which a
2113
- subclass extends (ArchimateCanvasEditor puts version and distinct shapes in front of the grid and line
2114
- styles). Every editor's shape menu orders with `orderMenuItem(id)`, the one Order submenu. A host can add more; `DiagramEditorPart` prepends its File menu (new, duplicate, rename, delete
2115
- diagram). After an item acts, the bar re-renders. Items with live labels (the zoom %) also refresh on
2116
- `editor.onRendered`.
2117
-
2118
- | Item | Use |
2119
- |---|---|
2120
- | `ToolbarMenu(label, items)` | Button opening a `PopupMenu`; `items` may be a function, evaluated on open |
2121
- | `ToolbarButton(label, action, { title, active })` | Action, or a toggle when `active()` is given |
2122
- | `ToolbarSelect(label, choices, value, onChange)` | Labelled dropdown; `choices()` returns `[value, text, title?]` |
2123
- | `ToolbarCheckbox(label, checked, onChange)` | Labelled switch |
2124
- | `ToolbarText(text, style)` | Live text |
2125
- | `ToolbarSeparator()` | Divider |
2126
-
2127
- ```javascript
2128
- new MenuBar({ container, editor, items: [
2129
- new ToolbarMenu('File', [{ label: 'Export', action: exportIt }]),
2130
- ...editor.menuBarItems(),
2131
- ] }).render();
2132
- ```
2133
-
2134
- `FormDialog` is the one modal form: `fields` of type `text`, `textarea`, `number`, `color`, `select`
2135
- or `checkbox`. A field with `inherit: { text, short? }` may be left unset; it then reads as `undefined` (a select
2136
- offers `text` as its first choice; any other field, a checkbox too, gets a checkbox named `short`, default
2137
- "Inherit", that greys it out). `FormDialog.many(fieldLists)` makes one form for several items from each one's
2138
- fields: every field on "Keep" showing the value they share, read-only fields left out.
2139
- `onApply(values)` receives every field; `onClose(applied)` is told once when the dialog closes, whether it
2140
- was applied (false for Cancel, Escape or a click outside), so a caller waiting on a choice can settle.
2141
- `EdgeStyleDialog`, the grid-size and diagram-name prompts are all `FormDialog`s. A dialog fits the window
2142
- (at most 90% of its height): its fields scroll between the title and the buttons, which stay in view, and
2143
- dragging the title moves it. A `textarea` scrolls inside its own box (at most 40% of the window) and can be
2144
- dragged taller; a properties dialog shows `description`, `documentation`, `notes` and `tooltip` that way.
2145
- A `color` field (`ColorField`) holds its value as text, `#rrggbb` or `none` for no colour (transparent, which a
2146
- browser colour input cannot hold), beside the colour picker and a row of preset swatches: no colour, black,
2147
- white, red, green, blue, yellow, orange, purple, grey and light grey (`ColorField.PRESETS`). A field's
2148
- `presets` (`[{value, name}]`) replaces them, and `allowNone: false` leaves out "No colour".
2149
-
2150
- ```js
2151
- new FormDialog({ title: 'Box', fields: [
2152
- { key: 'fill', label: 'Fill colour', type: 'color', value: '#ffffb5' }, // presets, none included
2153
- { key: 'text', label: 'Text colour', type: 'color', value: '#000000', allowNone: false },
2154
- { key: 'brand', label: 'Brand colour', type: 'color', presets: [{ value: '#0b5394', name: 'Brand blue' }] },
2155
- ], onApply: (values) => console.log(values.fill) }).open(); // '#ffffb5', or 'none'
2156
-
2157
- const fields = (t) => [{ key: 'fill', label: 'Fill colour', type: 'color', value: t.fill }];
2158
- new FormDialog({ title: '2 boxes', fields: FormDialog.many([fields({ fill: '#ff0000' }), fields({ fill: '#ff0000' })]),
2159
- onApply: (values) => console.log(values.fill) }).open(); // undefined while Keep is ticked
2160
- ```
2161
-
2162
- **Grouping and locking shapes.** A view's `arrangement` (`Arrangement`, on every `DiagramEditorState`, so a
2163
- template diagram's `DiagramView` and an `ArchimateView` alike) says which items move as one (an
2164
- `ItemGroup`: logical, it draws nothing but a dashed outline while a member is selected) and which are fixed in
2165
- place (locked: not moved, resized, rotated, reordered, edited or deleted until unlocked; trying says so). A
2166
- group can be locked as a whole; groups do not nest (grouping a member brings its group). Changes are one undo
2167
- step each: `view.group(ids)`, `view.ungroup(id)`, `view.setLocked(ids, locked)`, or the commands
2168
- `GroupCommand`, `UngroupCommand`, `LockCommand`. In the template editor (and ArchiMate's canvas editor, its
2169
- subclass), Shift/Ctrl/⌘+click adds to the selection; dragging a selected shape moves the selection with its
2170
- groups, as one step, and nothing moves if any of it is locked. In ArchiMate's graph editor, dragging a box
2171
- moves its group with it, as one step. A shape's menu offers Group, Ungroup and Lock / Unlock
2172
- (`arrangeMenuItems(arrangement, ids, act)`, the one definition every editor uses; Ungroup only once unlocked),
2173
- as do Ctrl+G, Ctrl+Shift+G and Ctrl+L; a locked shape shows a lock and no resize or rotate handles.
2174
-
2175
- **A background.** A kind of group is an `ItemGroup` subclass, kept by its JSON's `kind`. A `BackgroundGroup` is
2176
- pinned behind everything else on its view: its members stack below every other item, so "Send to back" on
2177
- anything else stops just in front of it, and its own members reorder only among themselves.
2178
- `view.arrangeAsBackground(ids, { name })` (or the undoable `BackgroundCommand`) makes items one locked
2179
- background group, behind the rest. A canvas made from a template gets its template's boxes this way, so the
2180
- template stays at the back and holds still until it is unlocked (the shape's menu, **Unlock background**);
2181
- unlocked, it moves as one; ungrouped, its boxes are ordinary boxes. Archi files keep it: each member carries
2182
- the group (`renderengine.group`), its kind (`renderengine.groupKind`) and the lock.
2183
-
2184
- ```js
2185
- const groupId = view.group(['a', 'b']); // a and b move together
2186
- view.setLocked(['a'], true); // the group is fixed in place
2187
- view.arrangement.canMove(['b']); // → false
2188
- view.history.undo(); // free again
2189
- view.arrangeAsBackground(['t1', 't2']); // behind everything, locked, moved as one when unlocked
2190
- ```
2191
-
2192
- **Connecting and deleting in the template editor** are commands, one undo step each:
2193
- `editor.connect(sourceId, targetId)` runs `connectCommand(sourceId, targetId)` (a new line), and
2194
- `deleteTemplate(id)` / `deleteEdge(id)` run `deleteTemplateCommand(id)` (the shape and the lines joined
2195
- to it) / `deleteEdgeCommand(id)`. A subclass whose shapes and lines stand for something else
2196
- overrides those factories (ArchimateCanvasEditor does). A template class says whether the editor
2197
- offers it a rotate handle (`static rotatable`, true by default; ArchiMate's templates are not). `pickFile(accept)` resolves with the chosen `File` (or `null`).
2198
-
2199
- ## Where to go next
2200
-
2201
- - **`ARCHITECTURE.md`** — module layout, the chart-type catalog mechanics, how to add a new chart type.
2202
- - **`CHART_TYPES_ROADMAP.md`** — per-chart-type design rationale and what's deliberately deferred.
2203
- - **`examples/csv-chart-builder.html`** — every chart type live, including the actual "Live updates"
2204
- panel this doc's streaming section is drawn from; open it in a browser to see the pattern run.
2205
- - **`test/charts.html`** — a plainer interactive demo of the core chart set.
1
+ # RenderEngine — Reference
2
+
3
+ One document for "what can I call and what does it take" — every chart/diagram entry point,
4
+ every shared option, every utility, and (the thing none of the older docs cover) the pattern for
5
+ feeding a chart a live, growing-then-capped data source.
6
+
7
+ This supersedes `README_COMPLETE.md`, `FEATURES_ADDED.md`, `FRAMEWORK_ALIGNMENT.md`,
8
+ `COMPATIBILITY_MATRIX.md`, and `DELIVERY_SUMMARY.md` as the day-to-day API reference — those five
9
+ predate most of the current chart-type set (some still say "8 chart types") and are kept only as
10
+ historical record; see `DOCUMENTATION_INDEX.md`. For *why* things are built the way they are —
11
+ the chart-type catalog pattern, the 3D camera split, per-chart design rationale — see
12
+ `ARCHITECTURE.md` and `CHART_TYPES_ROADMAP.md`. This doc is the *what/how*, not the *why*.
13
+
14
+ ## Setup
15
+
16
+ ```javascript
17
+ import { RenderEngine } from 'renderengine'; // or dist/renderengine.umd.js in a browser
18
+
19
+ const engine = new RenderEngine(document.getElementById('chart'), { rendererType: 'canvas' }); // or 'svg'
20
+ const headless = new RenderEngine(null); // test mode — every chart call no-ops safely
21
+ ```
22
+
23
+ - **Canvas** — higher raw point-count ceiling, no native DOM hit-testing (`hitTest()` does the
24
+ work manually).
25
+ - **SVG** — native tooltips/hit-testing, crisp at any zoom, automatic wheel-zoom and
26
+ drag-empty-space-to-pan on every SVG-rendered chart (`resetView()` clears it back to identity;
27
+ a 3D-orbit chart's own drag-to-rotate/wheel-to-zoom takes over instead — see below).
28
+ - Both renderers sit behind one primitive API (`rect`/`circle`/`path`/`text`/`line`/…) — every
29
+ `Chart` subclass is renderer-agnostic, so any `xChart()` call below works unchanged under either.
30
+ - **Size.** The renderer takes the container's size once, when the engine is made. Call
31
+ `engine.fitToContainer()` before drawing in a container that may since have been resized or
32
+ was hidden then (it measured 0×0): it re-measures and resizes the renderer, returning whether
33
+ the size changed, and does nothing while the container is still 0×0. An SVG diagram that set
34
+ its own viewBox keeps it and scales into the new box; a canvas is cleared, so draw after.
35
+ Without it, a drawing made in a narrower or hidden pane stayed that size — clipped, or blank
36
+ until the page was reloaded.
37
+
38
+ ## The rendering model (read this before the streaming section)
39
+
40
+ Every chart call — `barChart`, `lineChart`, all of them — starts with `this.renderer.clear()`
41
+ (via the `Chart.render()` base method) and redraws the entire chart from the `data` array you pass
42
+ in. There is no incremental/diff update, no "append one point" method, no built-in windowing.
43
+ Calling the same method again on the same container **replaces** what was there. This is deliberate
44
+ (it's what keeps every one of the 28 chart types simple and renderer-agnostic) but it means
45
+ "streaming"/"bounded window" behavior is entirely a caller-side pattern: keep your own array as the
46
+ source of truth, mutate *that*, then re-call the chart method. See "Dynamically adding data points
47
+ in a bounded window" below for the concrete pattern, straight from a real working implementation
48
+ in this repo.
49
+
50
+ ## Chart types
51
+
52
+ `src/chartCatalog.js` exports `CHART_CATALOG` (also reachable as `RenderEngine.CHART_CATALOG`) —
53
+ the single source of truth for this table; build a type-picker from it rather than hand-copying
54
+ this list (see `ARCHITECTURE.md`). `engine.renderChartType(key, data, options)` dispatches by
55
+ catalog `key` for you.
56
+
57
+ | Key | Method | Data shape | Notable options |
58
+ |---|---|---|---|
59
+ | `bar` | `barChart(data, opts)` | `Array<{label, value}>` | `colors`, `showLegend`, `barColor` — one of the types with numeric y-axis ticks/gridlines, see note below |
60
+ | `groupedBar` | `groupedBarChart(data, opts)` | multi-series `Array<{name, values:Array<{label,value}>}>` | `colors`, `legend`, `groupPadding`, `barPadding` |
61
+ | `line` | `lineChart(data, opts)` | `Array<{label, value, error?, errorPlus?, errorMinus?}>` or multi-series `Array<{name, values:[{label,value,error?,...}]}>` | `colors`, `legend`, `zAxis` (isometric depth per series); per-point `error` / `errorPlus` / `errorMinus` draw vertical error bars |
62
+ | `pie` | `pieChart(data, opts)` | `Array<{label, value}>` | `innerRadiusRatio` (>0 = doughnut), `colors`, `legend` |
63
+ | `donut` | `donutChart(data, opts)` | `Array<{label, value}>` | same as `pie` with `innerRadiusRatio` defaulting to 0.5 |
64
+ | `area` | `areaChart(data, opts)` | same as `line` | `stacked`, `colors`, `legend` |
65
+ | `stackedArea` | `stackedAreaChart(data, opts)` | multi-series `Array<{name, values:Array<{label,value}>}>` | same as `area` with `stacked:true` forced |
66
+ | `scatter` | `scatterChart(data, opts)` | `Array<{label, x, y, errorX?, errorY?, errorXMinus?, errorXPlus?, errorYMinus?, errorYPlus?}>` | `colorCol` (color-group by field, or a compiled formula — see below), `referenceLine`; symmetric or asymmetric x/y error bars |
67
+ | `bubble` | `bubbleChart(data, opts)` | `Array<{label, x, y, z}>` | `z` drives size; `colorCol`; `zAxis` (isometric depth) |
68
+ | `heatmap` | `heatmap(data, opts)` | `Array<{x, y, value}>` | `colorScale` (low→high stops), `xLabels`/`yLabels` |
69
+ | `calendarHeatmap` | `calendarHeatmap(data, opts)` | `Array<{date: string\|Date, value: number}>` | `colorScale`, `emptyColor`; one row per calendar day |
70
+ | `gantt` | `ganttChart(data, opts)` | `Array<{label, start, end, progress?, category?, color?}>` | `start`/`end` accept Date/ISO/number; `progress` 0–100 |
71
+ | `streamGraph` | `streamGraph(data, opts)` | multi-series `Array<{name, values:Array<{label,value}>}>` | silhouette baseline; `colors`, `legend` |
72
+ | `radialBar` | `radialBarChart(data, opts)` | `Array<{label, value}>` | angular categories with radial bar length; `colors`, `legend` |
73
+ | `populationPyramid` | `populationPyramid(data, opts)` | `Array<{label, left, right}>` | back-to-back horizontal bars; `leftColor`, `rightColor` |
74
+ | `sankey` | `sankeyDiagram(data, opts)` | `{nodes?: Array<{id,label?,color?}>, links: Array<{source,target,value}>}` | proportional-width flow links; `nodeWidth`, `linkOpacity` |
75
+ | `chord` | `chordDiagram(data, opts)` | same as `sankey` | nodes on a circle with curved chords of proportional thickness |
76
+ | `candlestick` | `candlestick(data, opts)` | `Array<{label, open, high, low, close}>` | `upColor`, `downColor`, `markers` (tooltip annotations) |
77
+ | `stackedBar` | `stackedBarChart(data, opts)` | `Array<{label, values:number[]}>` | `colors` (no built-in legend — a known gap) |
78
+ | `pipe` | `pipeChart(data, opts)` | `Array<{label, value, metric?}>` or multi-series | `metric` tapers line width; `legend` |
79
+ | `radar` | `radarChart(data, opts)` | `Array<{axis, value}>` or multi-series | `colors`, `legend` |
80
+ | `funnel` | `funnelChart(data, opts)` | `Array<{label, value}>` (largest first) | `colors` |
81
+ | `waterfall` | `waterfallChart(data, opts)` | `Array<{label, value, type?:'delta'|'total'}>` | `upColor`, `downColor`, `totalColor` |
82
+ | `histogram` | `histogram(values, opts)` | `number[]` (raw sample, binned internally) | bin count derived, not caller-set |
83
+ | `boxPlot` | `boxPlot(data, opts)` | `Array<{label, values:number[]}>` (raw per-group sample) | quartiles/whiskers computed internally |
84
+ | `density` | `densityChart(data, opts)` | same as `boxPlot` | Gaussian KDE overlay per group; `legend` |
85
+ | `violin` | `violinChart(data, opts)` | same as `boxPlot` | mirrored KDE silhouette per group |
86
+ | `qq` | `qqPlot(values, opts)` | `number[]` | delegates to `scatterChart`; fitted reference line |
87
+ | `lag` | `lagPlot(values, opts)` | `number[]` | `lag` (default 1); delegates to `scatterChart` |
88
+ | `treemap` | `treemapChart(data, opts)` | `Array<{label, value?, children?}>` (hierarchical) | squarified layout, palette by top-level branch |
89
+ | `sunburst` | `sunburstChart(data, opts)` | same hierarchical shape as `treemap` | nested rings by depth |
90
+ | `outline` | `outlineChart(data, opts)` | same hierarchical shape as `treemap`/`sunburst` | `onNodeClick`; collapsible indented list |
91
+ | `gauge-dial` | `dialGauge(value, opts)` | `number`, `{value, target?}`, or `[{value, label?, color?}]` for a needle each (0–100 unless `min`/`max`) | the gauge options below; `cx`, `cy`, `r`, `thickness`, `startAngle`, `sweep`, `needleColors`; several needles get a legend, the first fills the arc |
92
+ | `gauge-bar` | `barGauge(value, opts)` | `number` or `{value, target?}` (0–100 unless `min`/`max`) | the gauge options below; `x`, `y`, `width`, `height`, `orientation: 'vertical'` |
93
+ | `gauge-arc` | `arcGauge(value, opts)` | `number` or `{value, target?}` | the gauge options below; `cx`, `cy`, `r`, `thickness`, `startAngle`, `sweep` (a half circle by default), `cap` |
94
+ | `gauge-thermometer` | `thermometerGauge(value, opts)` | `number` or `{value, target?}` | the gauge options below; `x`, `y`, `width`, `height` (the tube), `bulb` (its radius) |
95
+ | `gauge-ring` | `ringGauge(value, opts)` | `number` or `{value, target?}` | the gauge options below; a whole circle from the top, the value in the middle |
96
+ | `gauge-rings` | `multiRingGauge(values, opts)` | numbers or `{value, label?, color?, min?, max?}`, outer first | one ring per value on its own scale; `gap`, `colors`, `legend` |
97
+ | `kpi-tile` | `kpiTile(data, opts)` | `number` or `{value?, previous?, history?:number[]}` | the figure (grouped), its change (`change`: `'percent'` or `'value'`; `upIsGood`), a sparkline of the history; `x`, `y`, `width`, `height`, `bands`, `sparkColor`, `background` |
98
+ | `bullet` | `bulletGauge(data, opts)` | `number` or `{value, target?, ranges?:Array<{max,label?,color?}>}` | the gauge options below; `ranges` (its name for `bands`), `barColor`, `rangeColors`, `showLabels` |
99
+ | `bar3D` | `barChart3D(data, opts)` | `Array<{label, value, depth?}>` | `depthScale`; fixed-angle isometric, not a real camera |
100
+ | `bubble3D` | `bubbleChart3D(data, opts)` | `Array<{label, x, y, z, size?, category?}>` | real orbit camera — drag to rotate, wheel to zoom |
101
+ | `bar3DOrbit` | `barChart3DOrbit(data, opts)` | `Array<{x, z, value}>` (grid) | real orbit camera |
102
+ | `stackedBar3DOrbit` | `stackedBarChart3DOrbit(data, opts)` | `Array<{x, z, values:number[]}>` (grid) | real orbit camera |
103
+ | `wordCloud` | `wordCloud(data, opts)` | `Array<{text: string, value: number}>` | spiral placement + bounding-box collision; `minFontSize`, `maxFontSize`, `padding` |
104
+ | `parallelCoordinates` | `parallelCoordinates(data, opts)` | `Array<{label?, category?, [dim: number]}>` | one vertical axis per numeric dimension; category-driven colors; `dimensions` |
105
+ | `geoMap` | `geoMap(data, opts)` | `{features: Array<{id, name, value?, color?, geometry}>, bubbles?: Array<{lat, lon, value}>}` | equirectangular projection; `center`, `scale`, `colorScale`; see note below |
106
+
107
+ **Gauges.** Every gauge is a `GaugeChart` (`src/charting/charts/GaugeChart.js`) and takes the same options:
108
+
109
+ | Option | Meaning |
110
+ |---|---|
111
+ | `min`, `max` | The scale (0 and 100). A 0–100 scale reads as a percentage ("75.0%"), any other as its number |
112
+ | `format`, `units` | How numbers read: `(v) => string`, or `{decimals, prefix, suffix, grouping, compact}` ("$1,234.5", "12.3K"); `units` is a suffix ("120.0 ms") |
113
+ | `bands` | `[{to, color?, label?}]` in order, each from where the one before ended; drawn lighter behind the value, and the value takes its band's colour. `bandColors` colours the bands without their own |
114
+ | `color` | The value's colour (wins over its band's): one, a gradient array of colours or `{stop, color}`, or `(percent) => colour` |
115
+ | `target`, `targetColor` | A marker across the track at a value (or the data's `target`) |
116
+ | `ticks` | That many equal steps, marked outside the track and labelled with only the decimals a step needs |
117
+ | `segments`, `segmentGap` | The track as that many blocks, `segmentGap` pixels apart (2), lit up to the value (LED style); with bands each lit block takes its own band's colour, a level meter |
118
+ | `animate` | `true` (400 ms) or milliseconds: drawing the same type of gauge again on the same engine eases from the value it shows to the new one (the value, target and each of several values; ease-out). A newer value stops an older animation and starts from where it got to |
119
+ | `title` | |
120
+
121
+ ```js
122
+ engine.dialGauge(72, { min: 0, max: 120, units: 'km/h', ticks: 6, target: 100,
123
+ bands: [{ to: 80, color: '#6bcb77' }, { to: 100, color: '#ffd93d' }, { to: 120, color: '#ff6b6b' }] });
124
+ engine.barGauge({ value: 40, target: 55 }, { orientation: 'vertical', ticks: 4 });
125
+ engine.ringGauge({ value: 380 }, { max: 500, format: { decimals: 0, suffix: ' / 500' } });
126
+ engine.multiRingGauge([{ label: 'Move', value: 420, max: 600 }, { label: 'Stand', value: 9, max: 12 }]);
127
+ engine.kpiTile({ history: [182000, 191200, 214300] }, { title: 'Revenue', format: { prefix: '$', grouping: true } });
128
+ engine.dialGauge([{ label: 'Actual', value: 68 }, { label: 'Budget', value: 80 }], { animate: true });
129
+ engine.barGauge(84, { segments: 20, bands: [{ to: 70, color: '#2ed573' }, { to: 100, color: '#ff4757' }] });
130
+ ```
131
+
132
+ The pieces are in `src/charting/gauges/`: `GaugeScale` (range, fractions, ticks, formatting), `GaugeBands`
133
+ (the bands and which one a value is in), `GaugeValue` (value, target, previous, history, several values) and the
134
+ tracks a value moves along, `HorizontalTrack`, `VerticalTrack` (both `LinearTrack`s) and `ArcTrack` (any start
135
+ angle and sweep, a full ring included), `Sparkline` (a series fitted to a box) and `GaugeAnimation` (frames from `GaugeValue.between(from, to, t)` on an
136
+ injectable clock). A new gauge picks a track and
137
+ draws with `drawBackground`, `drawValue`, `drawTarget` and `drawTicks`; its `static LAYOUT` gives its default place
138
+ and size (`cx`, `cy`, `r`, ...), which the options of the same names override. `ArcGauge` → `RingGauge` is the
139
+ pattern: the ring only changes the layout and where the labels go.
140
+
141
+ **`geoMap` note:** the `test/charts.html` demo uses deliberately rough, hand-drawn continental
142
+ polygons so the page needs no external map file. Real use should pass proper GeoJSON-like
143
+ `Polygon`/`MultiPolygon` features; the engine itself has no bundled world geometry and makes no
144
+ external network requests.
145
+
146
+ **Not in `CHART_CATALOG`** (diagram renderers invoked directly, not "chart a tabular result"
147
+ types — see `ARCHITECTURE.md`):
148
+
149
+ - `graphDiagram(nodes, edges, opts)` — force-directed node/edge diagram. `nodes:[{id,label,group?,shape?,size?}]`,
150
+ `edges:[{source,target,label?,weight?}]`. Key options: `onNodeClick`, `onEdgeClick`, `editable` (drag-to-connect,
151
+ right-click rename/delete/add), `contextMenuItems`/`edgeContextMenuItems`/`canvasContextMenuItems`
152
+ (replace the built-in right-click menus; support `{label, items:[...]}` submenus), `colors`, `nodeRadius`.
153
+ Dewey-style hierarchical `id`s (`"1.2.3"`) auto-infer parent/child edges in place of `edges`.
154
+ **Sizing:** a node's `size` (or, with `sizeBy: 'degree'`, its connection count — no `size` field needed)
155
+ scales its radius between `minRadius`/`maxRadius` (`sizeScale: 'linear'|'sqrt'`); an edge's `weight`
156
+ scales its stroke width between `minEdgeWidth`/`maxEdgeWidth` (`edgeWidthScale`) the same way. Either
157
+ is off by default — nothing changes size until some node/edge actually carries a value (or `sizeBy` is set).
158
+ **Edge routing:** `edgeRouting: 'straight'|'curved'|'orthogonal'|'polyline'` sets the diagram-wide default
159
+ (`'straight'` unless set); any edge can override it with its own `routing`. `'curved'` is a bezier that
160
+ leaves/arrives perpendicular to each node; `'orthogonal'` is a right-angle Manhattan connector;
161
+ `'polyline'` is a straight line unless another node sits in the way, in which case it steps around it with
162
+ one waypoint. **Ports:** an edge's `sourceSide`/`targetSide` (`'top'|'right'|'bottom'|'left'`, default
163
+ `'auto'`) pin where on the node it attaches — that point stays fixed to the node (moves with it) instead of
164
+ always sliding around to face whatever it's connected to, matching how `fromCol`/`toCol` table links already
165
+ anchor to a fixed side. Table-to-table FK edges (`fromCol`/`toCol` set, both ends `shape:'table'`) keep
166
+ their own fixed-column bezier regardless of `routing`/side options. The force-directed *node* layout
167
+ (springs pulling connected nodes together, mutual repulsion apart) is what `graphDiagram` already runs by
168
+ default — `edgeRouting` only changes how a connector is *drawn* between wherever the two nodes end up.
169
+ **Edge selection tolerance:** `edgeHitTolerance` (default 8 SVG units) widens the hit area around edge
170
+ lines for `hitTest()`, so thin or angled connections are easier to select, hover, or double-click for
171
+ `onEdgeDoubleClick`. The renderer's own pixel-exact hit is tried first; if it misses, the closest edge
172
+ within `edgeHitTolerance` is returned.
173
+ **Manual routing:** with `editable: true`, right-click an edge and choose "Add Waypoint Here" to drop a
174
+ bend point where you clicked (`_insertEdgeWaypoint`/its pure half `_edgeWaypointsWithInsertion`), drag a
175
+ bend point to move it, double-click a bend point to remove it — this builds `edge.waypoints` (`[{x,y}, ...]`),
176
+ which beats every `routing` style outright and threads the line straight through each point in order. Set
177
+ `edge.waypoints` yourself to hand in a route programmatically, or read it back via
178
+ `onEdgeWaypointsChange(edge, waypoints)`. (Adding a waypoint used to be double-click-the-line itself — moved
179
+ to the context menu once double-click was repurposed for `onEdgeDoubleClick`, see below.) The same edge
180
+ context menu also offers **Invert Direction**, which swaps `source`/`target` in place and fires
181
+ `onEdgeInvert(edge, oldSource, oldTarget)`.
182
+ **Undo/redo:** with `editable: true`, every model edit (move, resize, rename, add/delete node,
183
+ add/delete edge, invert edge, waypoint edits) is recorded in a `CommandHistory` bound to the diagram.
184
+ Press `Ctrl+Z` / `Ctrl+Y` (or `Ctrl+Shift+Z` for redo) to step through the stack. `historySize` sets
185
+ the maximum number of retained edits (default `200`), and `onHistoryChange(canUndo, canRedo)` fires
186
+ whenever the stack changes so a UI can enable/disable its own undo/redo buttons.
187
+ A host that re-renders the diagram passes its own `history` (a `CommandHistory`): edits are recorded
188
+ there, so undo outlives each render, and the host binds the undo keys itself (the diagram's own
189
+ `Ctrl+Z` handler stands down; `historySize`/`onHistoryChange` then belong to the host's history).
190
+ Recorded commands act on whichever diagram is on screen when they are undone, finding nodes and
191
+ edges by `id`. Every apply, undo and redo included, reports the result, so a host's copy stays in
192
+ step:
193
+
194
+ | Edit | Hook |
195
+ |---|---|
196
+ | move a node | `onNodeMove(nodeId, {x, y})` |
197
+ | resize a node | `onNodeResize(node)` (its `width`/`height`, or `radius` for a round shape) |
198
+ | add, move or delete a bend point | `onEdgeWaypointsChange(edge, waypoints)` |
199
+ | drag an end to pin its side | `onEdgeSideChange(edge, 'source' \| 'target', side)` (`undefined` = automatic again) |
200
+ | draw an edge | `onEdgeAdd(edge)` on add and redo, `onEdgeRemove(edge)` on undo |
201
+
202
+ An `onEdgeAdd` handler may give the new edge an `id`; the command finds it by that id after a
203
+ re-render.
204
+
205
+ ```javascript
206
+ const history = new CommandHistory();
207
+ const layout = { a: { x: 80, y: 80 }, b: { x: 300, y: 200 } };
208
+ const draw = () => engine.graphDiagram(
209
+ Object.entries(layout).map(([id, p]) => ({ id, name: id.toUpperCase(), ...p })),
210
+ [{ id: 'ab', source: 'a', target: 'b' }],
211
+ { editable: true, history, onNodeMove: (id, p) => { layout[id] = p; } },
212
+ );
213
+ draw();
214
+ // ...the user drags A; the page redraws...
215
+ draw();
216
+ history.undo(); // A goes back on screen, and onNodeMove puts layout.a back too
217
+ ```
218
+ **Grid:** `gridSize` (number, default `0`) snaps dragged and newly-added nodes to the nearest multiple;
219
+ `showGrid` (boolean, default `false`) draws a light grid behind the diagram. Both are plain `graphDiagram`
220
+ options, so they work for any editable diagram including ArchiMate. `GraphDiagram.snapToGrid(value, gridSize)`
221
+ is the pure helper if you need the same rounding rule elsewhere.
222
+ **Double-click:** dispatches to whatever's under the cursor — `onNodeDoubleClick(node)`/
223
+ `onEdgeDoubleClick(edge)` fire for a node/edge's line (nothing built-in happens without one; a typical wiring
224
+ opens the same dialog the right-click menu's "Show/Edit Properties…" does), `onCanvasDoubleClick(point)`
225
+ fires for empty canvas (SVG/world coordinates — no single object there to edit, so this is the natural place
226
+ for a diagram/view-level settings dialog instead). Double-clicking an existing waypoint handle still removes
227
+ it regardless of these — a distinct, precise gesture on a small routing control point, not "the edge" in
228
+ general. Every context-menu action (built-in or caller-supplied, via `contextMenuItems`/
229
+ `edgeContextMenuItems`/`canvasContextMenuItems`) now also receives the right-click's SVG/world point as a
230
+ third argument, `action(subject, engine, point)`.
231
+ **Edge tooltips:** hovering an edge shows a tooltip with its non-structural ("extra") data, the same
232
+ "only what isn't already visible on the diagram itself" convention a node's own hover tooltip uses
233
+ (`_edgeTooltipText`) — a bare `{source,target}` edge with nothing else set shows no tooltip at all. An
234
+ ArchiMate relationship's `kind` (extra data from `_archimateBuildEdge`'s point of view) always shows this
235
+ way.
236
+ **Manual resize:** `resizable: true` (or a node's own `node.resizable`, which overrides the diagram
237
+ default either way) shows a small handle on hover; dragging it sets `node.radius` — a hard override that
238
+ beats `sizeBy`/domain sizing, the same way explicit `node.x`/`node.y` already beat computed layout — or,
239
+ for a `shape:'table'` node, `node.width` (height still comes from column count). The handle's corner
240
+ defaults to `'top-right'`; `resizeCorner` changes it diagram-wide, `resizeCornerByShape: {shape: corner}`
241
+ per shape, and a node's own `node.resizeCorner` per node — each level overriding the one before it. See
242
+ `onNodeResize(node)`.
243
+ **Icons:** a node's `icon` (a short glyph/emoji string) or `iconUrl` (an image/SVG file or `data:` URI)
244
+ replaces the plain initials text centred on its shape (`iconPosition: 'center'`, the default) — `iconUrl`
245
+ wins if both are set; scales with the node's own radius, so it grows/shrinks along with manual resize.
246
+ `iconPosition: 'corner'` (diagram-wide, or a node's own `node.iconPosition`) instead draws it as a small
247
+ badge at a corner — same node > `iconCornerByShape` > diagram-wide `iconCorner` precedence as the resize
248
+ handle's corner, independently configurable (`archimateDiagram` uses this for its type-icon badge). Neither
249
+ mode applies to `shape:'person'` (already draws its own glyph) or `shape:'table'`.
250
+ **Box sizing/label position:** a `shape:'rect'` node's own `width`/`height` (either or both) override the
251
+ usual square-of-radius box, same idea as a table node's `node.width`. `labelPosition: 'inside'` (diagram-wide
252
+ default `'below'`, or a node's own `node.labelPosition`) left-aligns the label inside the box instead of
253
+ captioning it underneath, word-wrapped to as many lines as fit the box rather than one overflowing line
254
+ (`_wrapLabelLines`, measured with the renderer's `measureText(text, font)` — a 2D canvas context in the SVG and
255
+ Canvas renderers, an average-character estimate where there is none), keeping clear of a corner icon in a top
256
+ corner — meant for exactly this wider-than-tall case (`archimateDiagram` sets both). A node's own text style:
257
+ `textColor`, `fontFamily`, `fontSize` (`"12px"` or `"9pt"`), `fontWeight`, `fontStyle`; `strokeDasharray` dashes
258
+ its outline (an ArchiMate Grouping is drawn dashed). `options.edgesOnTop` draws lines over boxes instead of under
259
+ them, as ArchiMate tools do (`archimateDiagram` sets it), so a line into an element drawn inside another stays
260
+ visible.
261
+ **Label alignment:** `textAlign` (`'left'`|`'center'`|`'right'`) and `verticalAlign` (`'top'`|`'middle'`|`'bottom'`)
262
+ control the label's placement. Per-node values override diagram-wide `options.textAlign`/`options.verticalAlign`.
263
+ Defaults preserve existing behaviour: `labelPosition:'inside'` defaults to top-left; `'below'` defaults to
264
+ horizontal centre. `verticalAlign` only affects inside labels.
265
+ **Notes / annotations:** a node with `shape:'note'` renders as a yellow sticky note with a folded corner,
266
+ suppresses the centred icon/initials, and wraps its label inside. `archimateDiagram` accepts an `annotations`
267
+ option (array of `{id?, label/name, x, y, width?, height?}`) — each annotation becomes a note node on the
268
+ diagram but is not validated as an ArchiMate element, so notes are saved with the view without affecting the
269
+ model. Any plain `graphDiagram` can also use `shape:'note'` directly.
270
+ **Importable images:** a node with `shape:'image'` renders its `iconUrl` as an SVG `<image>` filling the node's
271
+ `width`/`height` (`preserveAspectRatio` defaults to slice; set `imagePreserveAspectRatio` on the node or
272
+ diagram options to change, e.g. `'xMidYMid meet'`). Image nodes suppress the usual centred icon/initials and
273
+ caption label. An optional `href` property turns the image into a clickable hyperlink (opened on click).
274
+ `test/charts.html` lets users add images via a file picker, drag-and-drop, or paste, and the node Properties
275
+ dialog exposes the link field. `RenderEngine.importArchiXML()` maps Archi `DiagramModelImage` elements to
276
+ `shape:'image'` and preserves their `iconUrl`/`href`.
277
+ **Label resolution** (`_resolveNodeLabel`, used everywhere a label renders — the box/caption, tooltip, table
278
+ header, keyword search): `label` may be a plain string (used as-is), a function `(node) => string` (called
279
+ at render time, the same "may be a function" convention `contextMenuItems`' own `label` already uses), or a
280
+ string with `{field}` placeholders (each replaced with that field's own value off the node — plain
281
+ substitution, not a parser, e.g. `'{name} ({type})'`). An empty/unset `label` falls back to `node.name`.
282
+ **More arrowheads/line styles:** beyond `'arrow'|'circle'|'tee'|'none'`, `arrowhead` (and a second,
283
+ independent `arrowheadStart` for the source end — e.g. a diamond at the "whole" end while the target end
284
+ carries none) also accepts `'diamond'|'diamond-hollow'|'triangle-hollow'|'open-arrow'`. `lineStyle:
285
+ 'solid'|'dashed'|'dotted'` generalizes the older boolean `dashed` (still honored when `lineStyle` isn't set).
286
+ **Node/edge properties:** `showPropertiesDialog(subject, engine, kind, {onSave})` and
287
+ `showDiagramPropertiesDialog(engine, {onSave})` are **shipped** (`src/propertiesDialog.js`, exported from the
288
+ package root) — wire them to `onNodeDoubleClick`/`onEdgeDoubleClick`/`onCanvasDoubleClick` or a context-menu
289
+ item and a diagram becomes editable. They used to exist only as ~400 lines inside `test/charts.html`, which
290
+ meant a consumer had to copy a demo to get double-click editing, so nobody did. `onSave(subject, changedKeys)`
291
+ fires after the subject is updated and the diagram re-rendered — that's where a consumer persists. The
292
+ decisions are pure and exported separately for reuse or testing: `propertyRowsFor` (which fields, which
293
+ section, which control), `coerceFieldValue` (read a string back as the type the field held), `derivedRefreshPlan`
294
+ (may a derived colour/icon be refreshed silently), `LAYER_CHOICES`, `NODE_SECTIONS`.
295
+ **Typed user-defined properties:** nodes and edges may carry `properties: [{name, type, value}]` where `type`
296
+ is `'string'`|`'integer'`|`'boolean'`|`'date'`. The Properties dialog shows these in their own section, lets
297
+ the user add/remove/edit them, and coerces values by type. `RenderEngine.NODE_STRUCTURAL_KEYS` and
298
+ `EDGE_STRUCTURAL_KEYS` include `'properties'` so they don't appear as untyped "extra data" tooltip lines.
299
+ A host that adds its own bookkeeping fields to the nodes and edges it hands in lists them in the
300
+ `internalKeys` option (an array); they are then left out of tooltips, the keyword filter and the
301
+ Properties dialog's "Other" section like the built-in fields. `graphDiagram.structuralKeys('node' | 'table' |
302
+ 'edge')` returns the combined set. ArchiMate passes `ArchimateView.PROJECTION_KEYS` (`elementId`,
303
+ `relationshipId`, `diagramInstanceId`, `visible`, `zIndex`, `viewObject`).
304
+ Archi native `.archimate` import/export round-trips properties: import converts flat `<property key="..." value="..."/>`
305
+ elements to typed `'string'` entries; export stringifies typed values back to flat key/value pairs.
306
+
307
+ There is still no built-in "Show/Edit Properties" **menu item** — `contextMenuItems`/
308
+ `edgeContextMenuItems` are plain hooks a caller wires up themselves; `RenderEngine.NODE_STRUCTURAL_KEYS`/
309
+ `EDGE_STRUCTURAL_KEYS` (used internally to keep built-in fields out of the auto tooltip's "extra data"
310
+ lines) are public statics for exactly this — see the `showPropertiesDialog` example and its use in the
311
+ Editable + Value Sizing demo in `test/charts.html` for a small dialog that lists every field on a clicked
312
+ node/edge (built-in and caller-added alike) as an editable input, and `showDiagramPropertiesDialog` (wired
313
+ to `onCanvasDoubleClick`/a canvas context-menu item in the same demos) for a diagram/view-level settings
314
+ dialog — `edgeRouting`, plus (for an ArchiMate diagram) the live `iconChoiceIndex` setting via
315
+ `_archimateSetIconChoiceIndex`. In that Properties dialog's own layer/type cascade (`test/charts.html`),
316
+ changing an ArchiMate element's `layer` clears and refocuses its `type` field — every official type belongs
317
+ to exactly one layer, so the old type essentially never still applies once the category changes.
318
+ - `graphDiagram3D(nodes, edges, opts)` — same data shape and sizing options, real orbit camera instead of
319
+ 2D force layout (node radius/edge width are additionally scaled by the camera's own perspective factor).
320
+ - `archimateDiagram(elements, relationships, opts)` — ArchiMate-notation diagram (Archi/archimatetool.com-style):
321
+ a thin transform over `graphDiagram` that builds rect nodes/edges with per-layer pastel fill, the label
322
+ inside the box, a small type-icon corner badge, and the 11 official relationship line/arrowhead notations,
323
+ then calls `graphDiagram` with them — every `graphDiagram` capability (drag, resize, routing, right-click
324
+ menus, editable add/rename/delete, ...) still applies.
325
+
326
+ **Archi native `.archimate` import/export:** `RenderEngine.importArchiXML(xml, viewId?)` parses an Archi
327
+ native XML file and returns `{ elements, relationships, options }` ready for `archimateDiagram()`. It maps
328
+ ArchiMate elements/relationships, view `DiagramObject` children (bounds, font, line thickness), and the
329
+ visual objects `DiagramModelNote`, `DiagramModelGroup`, `DiagramModelImage`, `DiagramModelReference`,
330
+ plus `SketchModelSticky`/`Actor` and `CanvasModelBlock`/`Sticky`/`Image`. Archi stores a nested visual's bounds
331
+ relative to its parent (a group, a group in a group, or an element drawn inside another element); import
332
+ turns them into positions on the view, so an element at `x="10"` inside a group at `x="100"` lands at 110.
333
+ A group inside a group keeps its `parentId`. On export, a view writes the groups drawn on it (those
334
+ with bounds in its `folderLayout`; `view.placedFolders`) nested as they are, each element inside the
335
+ group its folder is drawn as (`view.placedFolderOf(elementId)`, the nearest drawn ancestor), bounds
336
+ relative to the group. A folder that only exists in the model tree is not written as a group.
337
+ `model.allFolders()` lists every folder after its parent, read from `parentId`.
338
+ `RenderEngine.exportArchiXML(model, views)`
339
+ serializes back to Archi native XML, including element and relationship names, documentation and
340
+ properties. `views` are `{id, name, children}`; `fromArchimateView(view)` builds `children` from an
341
+ `ArchimateView`: one visual per instance, referencing the model element and relationship ids, with
342
+ bounds, bend points, and view-only notes and images.
343
+ The XML is what Archi 5.8 itself reads and writes (checked by opening our files in Archi and reading
344
+ back what it saved): all nine top-level folders in Archi's order (Archi fails to open a model with a
345
+ view but without them), elements as ArchiMate 3.2 classes (Archi knows no other: an ArchiMate 4
346
+ `process` in the application layer is written as `ApplicationProcess`, see `archimate32Type`),
347
+ relationships as `<Kind>Relationship` (a directed association is `AssociationRelationship
348
+ directed="true"`), groups and notes as `archimate:Group`/`archimate:Note` (a note's text in a
349
+ `<content>` element), each line naming its relationship as `archimateRelationship`, and each box
350
+ listing its incoming lines in `targetConnections` (without them Archi leaves some lines undrawn).
351
+ Import reads Archi's names and the older ones earlier versions of this module wrote.
352
+
353
+ **What an import keeps.** Checked against a real Archi model (canvas, sketch and ArchiMate views, nested
354
+ folders, styles, a legend, hints, report properties): Archi renders our export of it pixel-for-pixel as it
355
+ renders the original, and re-saving our export in Archi gives back the original model.
356
+ - **Positions.** The renderer places a node by its centre; Archi keeps a box's top-left corner. Import
357
+ centres each box (`x + width/2`), export takes the corner back (whole pixels, so a centre comes back
358
+ within half a pixel).
359
+ - **One instance per box.** Each box Archi draws is an instance with its own size and style, so the same
360
+ element twice on a view is two instances; each line joins the two instances it joins. A view shows what
361
+ Archi drew on it and nothing else (no "all elements", no lines Archi did not draw).
362
+ - **All views.** Views in sub-folders of Views, and canvas (`canvas:CanvasModel`) and sketch views: canvas
363
+ blocks and stickies and sketch stickies are view-only boxes with their text, colours and picture; a canvas
364
+ box without a border colour has no border; a sticky's text is centred vertically, as Archi draws it.
365
+ - **Style.** Archi's attributes `fillColor`, `lineColor`/`borderColor`, `fontColor`, `font` (SWT FontData:
366
+ name, size in points, bold/italic), `lineWidth`, `textAlignment` (centred when absent) and `textPosition`
367
+ become the instance's `appearance`.
368
+ - **Plain lines.** A line with no relationship (a note to an element, a canvas connection) is a
369
+ `ViewConnectionInstance` on the view, its look from Archi's `type` bits (solid/dashed/dotted; filled, hollow
370
+ or line arrows at either end). The validator leaves it alone.
371
+ - **Everything else, written back.** What the model does not use is kept on the item (`instance.archi`,
372
+ `view.archi`, `model.archi`, a concept's `archi`) and written back unchanged: other attributes (`hintTitle`,
373
+ `imagePosition`, `accessType`, `viewpoint`, a line's `locked`, ...), other child elements (`hintContent`, `notes`,
374
+ features such as a legend's), the original style (each attribute written as it was until its meaning is
375
+ changed), Archi's ids for boxes and lines, the stacking (document) order, a box's place inside another box
376
+ (canvas stickies in their blocks), the model-tree sub-folders (`archiFolder`), picture paths in the
377
+ archive, the model's id, `documentation` (Archi's purpose) and properties, and view properties (keys may
378
+ repeat). A copied view's boxes get new ids. Text keeps its spaces, and a CR is written as `&#xD;`.
379
+ - **Locks and groups.** A box's lock is the view's arrangement: Archi's `locked="true"` on import, and on
380
+ export where Archi can lock (canvas boxes); elsewhere (ArchiMate elements, notes, groups, images) Archi has
381
+ no lock, so it is the feature `renderengine.locked`, which Archi keeps. A logical group is the feature
382
+ `renderengine.group` (the group's id) on each member.
383
+ - **Canvases and templates.** A canvas (`canvas:CanvasModel`) is a `CanvasView`, its own kind of
384
+ `ArchimateView` (`view.constructor.kind === 'canvas'`; `ArchimateModel.registerViewType(archiViewType, Class)`
385
+ maps an Archi view type to a view class). A canvas kept to copy from is a template (`view.template`);
386
+ Archi has no such flag, so it is the view feature `renderengine.template` (`ARCHI_TEMPLATE_FEATURE`),
387
+ which Archi keeps. A copied view keeps its boxes inside their blocks.
388
+ - **Types Archi lacks.** Archi has a class for each ArchiMate 3.2 element type only, and will not open a
389
+ file naming another. An ArchiMate 4 type is written as its 3.2 type; any other (an IT icon type such as
390
+ `load-balancer`, or a custom type) as the nearest 3.2 type by layer and aspect (`archiElementType`,
391
+ `NEAREST_ARCHI_TYPE`: a technology device is a Node, a data store an Artifact, and so on), with its own type
392
+ kept as the feature `renderengine.type` and an Archi specialization named after it ("Load Balancer"), so
393
+ Archi users see it too. Import gives the type back and drops that specialization. An element Archi had
394
+ already given a specialization keeps its own.
395
+ - **No colour.** A box with no fill (`fillColor: 'none'`) is Archi's `alpha="0"`, and one with no outline
396
+ (`strokeColor: 'none'`) is Archi's `lineAlpha` feature at 0, both ways.
397
+ Bend points are written as Archi writes them, offsets from the centres of the two connected boxes
398
+ (`<bendpoint startX startY endX endY/>`, centres taken in whole pixels as Archi does). On import, bend
399
+ point i of n sits at `(1 - w)(sourceCentre + start) + w(targetCentre + end)` with `w = (i + 1)/(n + 1)`;
400
+ an `x`/`y` bend point from an earlier export is still read as a point on the view. An element saved
401
+ at Archi's default size (`width="-1"`) is 120 x 55 (a group 400 x 140, a note 185 x 80).
402
+
403
+ ```js
404
+ // Archi wrote <bendpoint startX="40" startY="13" endX="-160" endY="-87"/> on a line from a box at
405
+ // (0,0,120,55) to one at (200,100,120,55):
406
+ toRenderEngineModel(parseArchiModel(xml)).relationships[0].waypoints; // → [{ x: 100, y: 40 }]
407
+ ```
408
+ `toArchimateModel(parseArchiModel(xml))` reads all of that back, keeping notes and images on their
409
+ view (not as model elements) and the model's `name`.
410
+
411
+ **Archi files with pictures** are zip archives: the XML as `model.xml` and each picture under
412
+ `images/`, which `imagePath` names. `parseArchiFile(input)` takes a file as text, bytes or an
413
+ `ArrayBuffer`, plain or archived, and returns what `parseArchiModel` does with each archived picture
414
+ turned into a `data:` URL. `toArchiFile(model, views)` takes what `toArchiXML` does and returns
415
+ `{ data, archive, mimeType }`: plain XML when no visual has a `data:` picture, otherwise an archive
416
+ (bytes) with the pictures under `images/`, once each. `src/archimateArchive.js` owns the bridge;
417
+ `ZipArchive` (`src/zip/`, with a raw DEFLATE decoder and CRC-32, no dependencies) reads stored and
418
+ deflated entries, which is what Archi writes, and writes stored ones.
419
+
420
+ ```js
421
+ const file = toArchiFile(model, views); // { data, archive: true, mimeType: 'application/zip' }
422
+ const back = toArchimateModel(parseArchiFile(file.data));
423
+ ```
424
+
425
+ **Open Group Exchange Format** (ArchiMate Model Exchange File Format 3.1), the standard route to and
426
+ from other ArchiMate tools (Archi, Sparx EA, BiZZdesign, ...). `parseExchangeModel(xml)` gives what
427
+ `parseArchiModel` gives, and `toExchangeXML(model, views)` takes what `toArchiXML` takes, so both share
428
+ the Archi mapping; `toArchiExport(model)` gives an `ArchimateModel` as `[modelData, views]` for any of the
429
+ writers. `src/archimateExchangeFormat.js` owns what differs:
430
+ - **Positions.** A node's x/y and a bendpoint are on the view; Archi's are relative to the parent box and
431
+ offsets from the line's ends. The format allows no negative position, so a view reaching left of or
432
+ above 0 is moved right and down as a whole (as Archi exports).
433
+ - **Concepts.** Properties name a property definition (one per key). An Or junction is Archi's junction of
434
+ type "or"; an access relationship's `accessType` (Read, Access, ReadWrite; Write when absent) and an
435
+ influence's `modifier` are Archi's `accessType` and `strength`; `isDirected` is a directed association.
436
+ A type Archi lacks is written as its nearest 3.2 type, with its own kept in the property
437
+ `renderengine.type` (`EXCHANGE_TYPE_PROPERTY`), which import reads back.
438
+ - **Folders** are organizations: an item per Archi top-level folder, holding the sub-folders. On import an
439
+ outer item named as one of Archi's folders is that folder; any other outer item is a sub-folder.
440
+ - **Views.** Only ArchiMate diagrams: the format has no canvases or sketches, which are not written. A
441
+ group is a Container, a note (or picture, or view reference) a Label. A viewpoint is the format's name
442
+ ("Business Process Cooperation", `EXCHANGE_VIEWPOINTS`) and Archi's id (`business_process_cooperation`).
443
+ - **Style.** Colours are r/g/b with a 0-100 alpha (0: no fill or no line), the font a name, size and
444
+ `style` ("bold italic"). Only what a box or line sets itself is written; the reading tool draws the
445
+ rest with its own defaults.
446
+ - **Identifiers** must be xs:IDs: an id that is one is kept, any other (`1`, `a b`) is made one
447
+ (`id-1`, `id-a_b`) and every reference follows it.
448
+
449
+ Checked with Archi 5.8: Archi validates the files against the schema and opens them; a file Archi
450
+ exported comes back through `parseExchangeModel`, `toArchimateModel`, `toArchiExport` and
451
+ `toExchangeXML` identical to Archi's own import of it. Archi's importer ignores `lineWidth`.
452
+
453
+ ```js
454
+ const model = toArchimateModel(parseExchangeModel(xml)); // a file from Archi, Sparx EA, BiZZdesign, ...
455
+ const out = toExchangeXML(...toArchiExport(model)); // and back
456
+ ```
457
+
458
+ `src/archimateArchiFormat.js` owns the pure
459
+ mapping and the tiny dependency-free XML parser (uses `DOMParser` in browsers; under Node, a fallback
460
+ that decodes entities and character references, takes either quote, and skips comments and CDATA).
461
+ **CSV import/export:** `RenderEngine.exportModelCSV({elements, relationships})` returns `{ elementsCSV,
462
+ relationshipsCSV }` for spreadsheet editing; `RenderEngine.importModelCSV(elementsCSV, relationshipsCSV)`
463
+ parses them back into element/relationship objects. Columns are `id,name,type,layer,documentation,properties`
464
+ for elements and `id,source,target,kind,label,documentation,properties` for relationships. The `properties`
465
+ column holds the typed property array as JSON. `src/archimateCSV.js` owns the pure CSV helpers and is also
466
+ exported directly (`exportElementsCSV`, `importRelationshipsCSV`, etc.).
467
+
468
+ **`src/archimate.js`** owns the actual notation data — `ArchimateLayer`/`ArchimateElementType`/
469
+ `ArchimateRelationshipType` instances (all also re-exported from the package root, and as
470
+ `RenderEngine.ArchimateNotation`/`.ArchimateLayer`/etc. statics for convenience), queried through the
471
+ `ArchimateNotation` registry rather than archimateDiagram's own flat string-keyed maps — deliberately OO
472
+ because a type's layer is then a real object reference (`type.layer`), not a name in one lookup table and
473
+ the same name in a second, separate one that could silently drift apart. `ArchimateNotation.typesInLayer
474
+ (layerName)` is the query that relationship supports directly — the dependency a "type" control that must
475
+ narrow to match a chosen "layer" needs (see the cascading `layer`/`type` fields in the Properties dialog in
476
+ `test/charts.html`, which read exactly this both ways: picking a layer narrows type's own suggestions to
477
+ that layer's types; picking/typing a recognised type snaps layer to match it).
478
+
479
+ Every element type and relationship type also carries a short `description` for authoring UIs:
480
+ `ArchimateNotation.describe(typeName, version)` returns an element's description, and
481
+ `ArchimateNotation.describeRelationship(kind)` returns a relationship's description (both undefined for
482
+ unrecognised names). The Properties dialog in `test/charts.html` shows these in a "?" tooltip next to the
483
+ `type` and `kind` fields. `LLM_ARCHIMATE_GUIDE.md` packages the same catalog plus prompting guidance for
484
+ LLMs that generate diagram data.
485
+
486
+ **IT / infrastructure extension types.** `archimateDiagram` also recognises a set of practical IT concepts
487
+ that ArchiMate does not name directly: `firewall`, `laptop`, `server`, `database`, `table`, `file`, `folder`,
488
+ `container`, `pod`, `load-balancer`, `cloud-region`, and more (see `src/itIcons.js`/`RenderEngine.ITIconNotation`).
489
+ These resolve to the `technology` layer with the correct `aspect` (`active-structure` for devices/services,
490
+ `passive-structure` for data/files) and their own icon, so they behave like first-class ArchiMate elements in
491
+ diagrams, validation, and the construction UI. They are treated as extended/custom types, not official
492
+ ArchiMate, and the validator uses their aspect/layer for relationship advice.
493
+
494
+ `elements:[{id,label,layer?,type?,aspect?,icon?,iconUrl?}]` — `layer` (any string, not just the 7 official
495
+ names) picks the fill colour (`ArchimateLayer#color`, extendable via `opts.layerColors`), or derives from
496
+ `type` when `layer` is omitted: first checked against the full ~55 official ArchiMate element names
497
+ (`ArchimateNotation.elementType(type)?.layer` — most, e.g. `'goal'`/`'node'`/`'resource'`, do NOT start
498
+ with their own layer's name), then the prefix of `type` before its first `-` for anything not in that
499
+ catalog (`type:'business-my-widget'` → layer `'business'`, for ANY `'<layer>-whatever'` string, official
500
+ name or not). **Icons:** an element's own `icon`/`iconUrl` always wins; otherwise a recognised `type`'s own
501
+ representative icon (`ArchimateElementType#icon` — a glyph echoing the real pictogram's idea, e.g. Goal's
502
+ 🎯, Location's 📍, Application Component's 🧩 — not a redraw of the actual vector artwork); otherwise a
503
+ generic per-layer one (`ArchimateLayer#defaultIcon`) — either can be overridden diagram-wide via
504
+ `opts.defaultIcons`. **Badges are the notation's own glyphs, not emoji.** In the real notation a corner
505
+ badge IS the element's standalone glyph drawn small — a Business Process is a box with a little arrow in
506
+ the corner, and that same arrow alone is the element without its box. So the badge is rendered from the
507
+ type's own `notationShape` (as line art, via the generic `node.iconShape` graphDiagram field), one vector
508
+ glyph per type used at two sizes. Every official type in both catalogs has one, so ArchiMate rendering
509
+ never falls back to an emoji; `opts.iconSet` (`'glyph'|'emoji'`, default `'glyph'`) switches the whole
510
+ diagram to the Unicode stand-in set instead, and THAT is where `iconChoiceIndex`/`iconChoices` apply — the
511
+ standard glyph has no alternatives to choose between, the emoji do. **Icon choice sets:** a type doesn't offer just the one icon —
512
+ `ArchimateElementType#iconChoices`/`ArchimateNotation.iconChoices(typeName)` return a deduplicated list with
513
+ the official ArchiMate-standard glyph always at index 0, followed by a few generic aspect-wide alternatives
514
+ (`active-structure`/`behavior`/`passive-structure`/`motivation`/`other` — e.g. behavior types alternate
515
+ between ▶️/🔄/⚡) and finally the type's own layer default; an unrecognised/absent type falls back to
516
+ `GENERIC_ICON_CHOICES` (every aspect's alternatives pooled together). Which choice is used as an element's
517
+ *default* (when it sets no `icon` of its own) is a diagram-wide setting, `opts.iconChoiceIndex` (default
518
+ `0`, the standard icon) — an out-of-range index falls back to choice 0 rather than throwing. This is what a
519
+ "choice of icons per type, with the ArchiMate-standard one as the default" UI (a `<select>` of
520
+ `iconChoices(type)`, e.g. the Properties dialog's own `icon` field in `test/charts.html`) is built on; an
521
+ element's own explicit `icon` still always wins over any diagram-wide choice. `iconChoiceIndex` isn't just a
522
+ one-shot build option — `engine._archimateSetIconChoiceIndex(newIndex)` changes it LIVE after the diagram's
523
+ already drawn, re-deriving the icon for every node that's still on its old auto-derived default (same "was
524
+ it ever customised" check the Properties dialog's own layer/type-change handler uses) and leaving any
525
+ individually-picked icon alone; `test/charts.html`'s `showDiagramPropertiesDialog` (opened via
526
+ `onCanvasDoubleClick` or a canvas context-menu item) is a small UI for this. It's kept on `engine.
527
+ _archimateOptions`, not on `_chartState` — `archimateDiagram`'s own settings (`iconChoiceIndex`/
528
+ `layerColors`/`defaultIcons`/`relationshipStyles`) aren't part of `graphDiagram`'s flat `options` (they're
529
+ destructured out before its own options are built), and `_chartState` itself gets overwritten wholesale on
530
+ every `graphDiagram`/`_rerenderGraph` call, so anything meant to survive a redraw lives on the engine
531
+ instance instead. **Box corner style**
532
+ (`ArchimateElementType#cornerStyle`): every Motivation-layer
533
+ element is chamfered (all 4 corners cut at 45°) regardless of anything else; `'behavior'` aspect
534
+ (Process/Function/Service/Event/Interaction-like) gets rounded corners — an explicit `element.aspect` wins,
535
+ else a recognised `type`'s own aspect (so `type:'business-process'` is rounded with zero extra config);
536
+ everything else is sharp (correct for active/passive-structure elements). An element's own `cornerStyle`
537
+ (`'sharp'|'rounded'|'chamfered'`) overrides all of that directly — and works on any plain `graphDiagram`
538
+ `shape:'rect'` node too, not just via `archimateDiagram`.
539
+
540
+ `relationships:[{source,target,kind?}]` — `kind` (`composition`/`aggregation`/`assignment`/`realization`/
541
+ `specialization`/`serving`/`access`/`influence`/`triggering`/`flow`/`association`) picks the notation
542
+ (`ArchimateRelationshipType`, extendable via `opts.relationshipStyles`); an explicit `lineStyle`/
543
+ `arrowhead`/`arrowheadStart` on one relationship still overrides its kind's default.
544
+
545
+ **Standard alignment (advisory warnings, not blocking).** `archimateDiagram` validates the model against
546
+ the ArchiMate metamodel and warns where it deviates. `opts.invalidRelationships` says how an invalid
547
+ relationship shows: `'highlight'` (the default: a red dashed line and a tooltip), `'tooltip'` (the warning
548
+ on hover only) or `'off'` (nothing checked); `opts.validate: false` also turns checking off. The ArchiMate
549
+ editor takes it from the "Invalid relationships" setting, which a diagram, its model or the tool sets
550
+ (see the settings cascade below). Warnings are delivered three ways:
551
+ - `opts.onValidationWarning(warning)` is called once per warning during render;
552
+ - `engine._archimateWarnings` holds the full list after render;
553
+ - invalid relationships carry a tooltip explaining the issue, and with `'highlight'` a red dashed stroke.
554
+ Relationships between ArchiMate types are checked against the specification's own relationship table
555
+ (The Open Group, ArchiMate 3.2 Specification, Appendix B; `ArchimateValidator.specificationKinds`), an
556
+ ArchiMate 4 type looked up as its 3.2 type. `src/archimateRelationshipTable.js` holds the table, generated
557
+ by `scripts/generate-archimate-relationships.mjs` from the machine-readable copy Archi ships. Custom and IT
558
+ types, which the table does not name, fall back to `ArchimateValidator.RULES`: data, each rule naming its
559
+ `kinds` and constraining `fromAspect(s)`, `toAspect(s)`, `aspects`, `sameAspect`, `sameLayer`, `fromTypes`
560
+ or `toTypes`. Also checked: v4 Common Domain elements (`process`/`function`/`service`/`interaction`/`event`)
561
+ needing an explicit `layer`, and retired v3.2 types used under v4.
562
+
563
+ ```js
564
+ ArchimateValidator.allowedKinds({ type: 'business-actor' }, { type: 'business-role' }, '4');
565
+ // → ['assignment', 'serving', 'triggering', 'flow', 'association', 'association-directed']
566
+ ``` `RenderEngine.ArchimateValidator` exposes the same checks for construction UIs:
567
+ `ArchimateValidator.validateModel(elements, relationships, version)`,
568
+ `ArchimateValidator.validateRelationship(source, target, kind, version)`, and
569
+ `ArchimateValidator.allowedKinds(source, target, version)` — the latter powers the filtered relationship-kind
570
+ dropdown in `test/charts.html`'s Add Edge and Edge Properties dialogs.
571
+
572
+ **Specializations.** `RenderEngine.ArchimateSpecializationRegistry` (src/archimateSpecializations.js)
573
+ stores user-defined specializations (`{id, name, baseTypes[], icon?, color?, description?}`). Tag an
574
+ element with `specialization: '<id-or-name>'` and `archimateDiagram` applies the specialization's icon
575
+ and colour unless the element already has its own. `registry.serialize()` / `registry.deserialize(array)`
576
+ make persistence trivial; `test/charts.html` persists the catalog in `localStorage` and exposes a
577
+ "Specializations…" manage dialog.
578
+
579
+ **Colours, glyphs and line weights are read out of the spec's own artwork, not eyeballed.** The
580
+ specification PDF embeds its "Appendix A: Summary of Language Notation" figures as LOSSLESS PNGs, so the
581
+ palette below is the literal pixel value of each fill: Business `#FFFFAF`, Application `#AFFFFF`,
582
+ Technology and Physical both `#AFFFAF` (the spec draws them the same green), Strategy `#F5DEAA`,
583
+ Motivation `#CCCCFF`, Implementation & Migration `#FFE0E0`, Location `#FFBF80`. A few types carry a colour
584
+ their own layer doesn't share — Plateau/Gap are green (`#E0FFE0`) among the pink Implementation elements,
585
+ Grouping is unfilled — so `ArchimateElementType#ownColor` overrides the layer fill for those (element
586
+ `color` > `opts.layerColors` > `ownColor` > layer). Elements are drawn with a thin BLACK outline
587
+ (`strokeColor: '#000000'`, `strokeWidth: 1.2` — both plain graphDiagram node fields, defaulting to the
588
+ usual white 2px elsewhere), which is what the real notation uses; the white stroke graphDiagram uses to
589
+ separate overlapping blobs is a large part of why an ArchiMate diagram drawn with plain defaults reads as
590
+ much brighter and softer than the spec. The relationship notations were checked glyph-by-glyph against the
591
+ same appendix — all 11 kinds, plus the directed-association variant (`association-directed`, a single-barb
592
+ `open-arrow-half`) and both junction connector forms the figure shows alongside them.
593
+
594
+ **ArchiMate version: `opts.archimateVersion` (`'3.2'|'4'`, default `'4'`, `ArchimateNotation.versions()`).**
595
+ ArchiMate 4 retires 19 of 3.2's element types (`src/archimate.js`'s own doc comment has the full reasoning):
596
+ fifteen layer-specific behavior types (`business-process`/`application-process`/`technology-process`, and
597
+ the same 3-layer split for `function`/`service`/`interaction`/`event`) collapse into five layer-INDEPENDENT
598
+ "Common Domain" types (`process`/`function`/`service`/`interaction`/`event` — `ArchimateElementType#layer`
599
+ is `null` for these five; the real layer must come from the element's own `layer` field, not the type name);
600
+ four more (`contract`, `representation`, `constraint`, `gap`) are retired outright, each replaced by an
601
+ existing type. Under the `'4'` default, every element is first run through `ArchimateNotation.
602
+ migrateElementToV4` — a direct mirror of a jArchi "Migrate Model Elements from ArchiMate 3.2 to ArchiMate 4"
603
+ script's own two mapping tables — a no-op for any other type (most of the ~55, e.g. `business-actor`,
604
+ `application-component`, `goal`, `node`, are identical in both versions and all 11 relationship notations
605
+ are unchanged), so 3.2-authored `elements` data still renders correctly, auto-upgraded and tagged with its
606
+ own migration provenance as plain extra data: `ArchiMate4_Domain` (the original layer, e.g. `'Business'`)
607
+ + `MigratedFrom` (the original type) for a behavior consolidation, or `LegacyType`/`Type` + `MigratedFrom`
608
+ for a retired type — both visible in the hover tooltip/Properties dialog like any other custom field, same
609
+ as `kind` already is for a relationship. An element that already carries its own `layer` keeps it; one that
610
+ doesn't gets it filled in from the migration's own knowledge of where it used to live (needed since a
611
+ Common Domain type's own layer is `null` — without this, a migrated element with no explicit `layer` would
612
+ silently fall through to 'other'). Under `'3.2'`, no migration happens at all — every `ArchimateNotation`
613
+ lookup `_archimateBuildNode`/`_archimateBuildEdge` make (`elementType`, `elementTypeNames`, `typesInLayer`,
614
+ `iconChoices`, `elementLayer` — all take an optional `version` parameter, default `'4'`) resolves against
615
+ the original 3.2 catalog instead. See the ArchiMate Diagram demo's own version `<select>` in
616
+ `test/charts.html`, which re-runs `archimateDiagram` over the SAME `elements`/`relationships` for either
617
+ choice (not a live bidirectional toggle — a v4-migrated type like `'process'` was never a v3.2 catalog entry
618
+ to begin with, so round-tripping back through the ALREADY-migrated data wouldn't resolve correctly; picking
619
+ a version is a "how should this diagram be built" choice, made fresh from the original input each time).
620
+
621
+ **Distinct notation shapes: `opts.useDistinctShapes` (boolean, default `false`).** The plain box + corner
622
+ icon isn't the only ArchiMate notation — the spec's own Appendix A ("Summary of Language Notation") gives
623
+ many types a real distinct outline instead (Process = a plain arrow, Interface = a lollipop, Node = a 3D
624
+ cube, ...). `ArchimateElementType#notationShape` (`src/archimate.js`) names that shape (a `graphDiagram`
625
+ `shape` value — see `_nodeShapeEl`) for every type that has one; `undefined` for a type the spec only ever
626
+ shows as a plain box (Business Object, Work Package, Deliverable, ...), or whose real glyph is still too
627
+ compound to render cleanly as a single node shape here (System Software's box+circle is the one gap left).
628
+ 32 shapes implemented, verified against rendered output (not just the spec image) — see
629
+ `test/archimate.test.js`'s own `notationShape` describe block for the full type→shape mapping:
630
+ `arrow` (Process), `chevron` (Function, points up with a notched bottom — distinct from arrow, which points
631
+ right), `half-circle` (Interaction), `flag` (Event/Value Stream/Implementation Event), `pill` (Service, a
632
+ fully-rounded stadium box), `double-circle` (Collaboration), `lollipop` (Interface), `cylinder` (Role/
633
+ Stakeholder — a HORIZONTAL drum, left/right elliptical caps, not top/bottom), `cube` (Node, a real 3-face
634
+ isometric box), `target` (Goal/Outcome/Course of Action — a bullseye with a dark centre dot), `hexagon`
635
+ (Material — flat top/bottom, pointed left/right), `pin` (Location), `cloud` (Meaning), `document` (Artifact),
636
+ `parallelogram` (Requirement), `constraint` (Requirement's parallelogram plus its own internal divider
637
+ line), `gear` (Equipment — two overlapping cogs), `ellipse` (Value), `battery` (Resource), `component`
638
+ (Application Component — a box with two connector tabs on the left edge), `factory` (Facility), `device`
639
+ (Device — a monitor on a stand), `steps` (Capability — an ascending staircase), `wheel` (Driver — a
640
+ steering wheel), `magnifier` (Assessment), `stack` (Plateau — three bars), `gap-circle` (Gap — a circle
641
+ crossed by two lines), `principle` (Principle — a rounded square with "!"), `double-arrow`
642
+ (Communication/Distribution Network), `double-arrow-dashed` (Path), `junction` (a small solid dot, not the
643
+ plain default circle — no centred initials text), plus the existing `person` (Business Actor). Same
644
+ "representative, not pixel-perfect" calibration as the corner-badge icons — cross-checked against the spec,
645
+ a few curves/angles simplified for a clean SVG path, not a redraw of the actual vector artwork. When a
646
+ distinct shape is in use: the label moves below it (there's no "inside the box" for most of these,
647
+ `labelPosition` defaults to `'below'` instead of `'inside'`), and no default corner icon OR centred-initials
648
+ fallback is shown (the shape itself already says what the element is — every notationShape case sets
649
+ `suppressInner`) — an element's own explicit `icon`/`iconUrl` still wins regardless. `useDistinctShapes` is
650
+ a diagram-wide DEFAULT (off, so nothing changes for an existing diagram that hasn't opted in); an individual
651
+ element's own `useDistinctShape` (true/false) overrides it either way. A type with no implemented
652
+ `notationShape` always renders as the plain box, whichever way the setting goes. Every distinct shape also
653
+ honours an explicit `width`/`height` override the same way the plain box does (`_nodeSize` supports this for
654
+ any shape now, not just `'rect'`/`'table'` — and the generic `'below'` label offset and the plain
655
+ `'person'`/`'diamond'`/default-circle shapes were both fixed to size off the box's own smaller dimension
656
+ instead of `max(w,h)/2`, so a wide-but-short box no longer bleeds its label or an oversized circle into the
657
+ row below it in a dense grid). See the ArchiMate Diagram demo's own checkbox in `test/charts.html`, and the
658
+ dedicated **ArchiMate Notation Reference** demo right after it — every type the current version's catalog
659
+ has, one row per layer, built entirely from `ArchimateNotation.layers()`/`.typesInLayer()` rather than a
660
+ hand-picked sample, with the same box/shape toggle.
661
+
662
+ **Adding elements/relationships interactively:** archimateDiagram only enriches the `elements`/
663
+ `relationships` it's first called with — a node added afterward (e.g. via `editable`'s own "Add Node", or a
664
+ caller's own canvas action) doesn't automatically get archimateDiagram's layer colour/corner style/icon.
665
+ Use `engine._archimateBuildNode(element, opts)` and `engine._archimateBuildEdge(relationship, opts)` to
666
+ enrich a new item exactly the same way archimateDiagram does internally — see the ArchiMate Diagram demo's
667
+ `canvasContextMenuItems`/`onEdgeAdd` in `test/charts.html`. The built-in drag-to-connect gesture now acts as
668
+ a **magic connector** for ArchiMate diagrams: it picks the first relationship `kind` that
669
+ `ArchimateValidator.allowedKinds(source, target, version)` considers valid and immediately applies the
670
+ matching line/arrowhead style, falling back to a plain `{source,target}` edge only when no validator is
671
+ available. The right-click edge menu also offers **Invert Direction** to swap source/target in place.
672
+
673
+ See that same demo for all 11 relationship kinds, business/application/technology/motivation layers, a
674
+ chamfered Motivation goal, and rounded process/service boxes.
675
+
676
+ **Properties dialog fields are typed, not one-size-fits-all text boxes** (`test/charts.html`'s
677
+ `showPropertiesDialogUnsafe`/`addRow`): `'text'` (default) | `'textarea'` (`description`) | `'color'` (a
678
+ native `<input type=color>` swatch/picker for `color`, in place of a raw hex string) | `'select'` (a closed
679
+ list — `layer`, and `icon` sourced from `ArchimateNotation.iconChoices(subject.type)`) | `'select-editable'`
680
+ (an `<input list=datalist>` that still accepts free text — `type`, since archimateDiagram deliberately
681
+ doesn't enforce a closed type catalog). The dialog also groups a node's fields into an **Object** section
682
+ (`name`/`description`/`type` — the ArchiMate concept itself, the same regardless of how any one diagram
683
+ draws it) and a **Diagram** section (`label`/`layer`/`color`/`icon`/`group`/`size` — this view's own
684
+ presentation of it), with any remaining extra data under "Other"; this is a form-level grouping only —
685
+ `graphDiagram` itself still renders one flat node object, there's no separate model/view split under the
686
+ hood. The existing layer↔type cascade (`ArchimateNotation.typesInLayer`/`.elementType`) now also refreshes
687
+ `icon`'s own `<select>` options to the newly-chosen type's `iconChoices` whenever `type` changes, keeping the
688
+ current icon selected if it's still offered.
689
+ - `dataModelDiagram(entities, relationships, opts)` — conceptual/logical/physical entity-relationship diagram,
690
+ the same "thin transform over `graphDiagram`" shape as `archimateDiagram`. **`src/datamodel.js`** owns the
691
+ notation data (`DataModelLevel`/`NotationStyle`, queried through the `DataModelNotation` registry — also
692
+ re-exported from the package root and as `RenderEngine.DataModelNotation`), its own separate OO file/
693
+ registry for the same reason `archimate.js` is: this domain's vocabulary shouldn't live inside RenderEngine
694
+ itself.
695
+ **Three levels, one model — the cross-mapping.** An entity is ONE object: `{id, name, description?, x?, y?,
696
+ color?, conceptual?, logical?, physical?}` — `name`/`description` are shared across every level (the
697
+ entity's own identity, unchanged by fidelity), while `entity.logical.attributes`/`entity.physical.
698
+ attributes` (`[{name, type?, isPK?, isFK?}]`, physical's own sub-object also taking `tableName`) hold that
699
+ level's own detail — the same object/visual split `archimateDiagram` draws between an element and its
700
+ diagram, just spanning "levels" instead of "diagrams". `opts.level` (`'conceptual'|'logical'|'physical'`,
701
+ default `'logical'`) picks which one's on screen; an entity with no attributes at the current level
702
+ (typical for `'conceptual'`) always renders as a plain box, regardless of notation. Re-calling
703
+ `dataModelDiagram` with a different `level` over the SAME `entities`/`relationships` re-renders that same
704
+ model at different fidelity — there's only ever one model, never three separately-maintained diagrams that
705
+ could drift apart (see the Data Model demo in `test/charts.html`, whose Level/Notation `<select>`s just call
706
+ it again).
707
+ **Three notations, one cardinality vocabulary.** `relationships:[{source,target,name?,sourceCardinality?,
708
+ targetCardinality?,identifying?}]` — cardinality is stored once as a UML-style multiplicity string
709
+ (`'0..1'|'1'|'0..*'|'*'|'1..*'`, `DataModelNotation.cardinalities()`), and `opts.notation`
710
+ (`'chen'|'crowsfoot'|'uml'`, default `'crowsfoot'`) picks how each end renders it: **Chen** turns the
711
+ relationship itself into its own `shape:'diamond'` node with two plain edges (not one direct edge), each
712
+ labelled with a plain `'1'`/`'N'` text cardinality; **Crow's Foot** renders cardinality as an arrowhead-style
713
+ glyph at each entity end (`crowsfoot-one`/`-one-optional`/`-many`/`-many-mandatory` — new SVGRenderer marker
714
+ defs, fork for "many", tick for "one", hollow circle for "optional", usable as a plain edge `arrowhead`/
715
+ `arrowheadStart` outside dataModelDiagram too); **UML** shows the multiplicity string itself as a text label
716
+ near each end instead, via the new generic `edge.sourceLabel`/`edge.targetLabel` fields (independent of the
717
+ existing midpoint `edge.label` — see graphDiagram's own doc comment). `relationship.identifying === false`
718
+ renders a dashed line (a non-identifying relationship, in real ERD terms) regardless of notation;
719
+ unset/`true` stays solid. None of the three notations are pixel-perfect renditions of the real, full
720
+ notations (same calibration as `archimateDiagram`'s own icons) — see `src/datamodel.js`'s own doc comment.
721
+ - `railroadDiagram(diagramString)` — renders an EBNF grammar string as a railroad diagram. No node/edge
722
+ data at all.
723
+
724
+ **Y-axis ticks.** Most cartesian chart types only draw a bare axis line — no numeric tick marks
725
+ or gridlines. `barChart` and `groupedBarChart` are wired up to `_drawYAxisTicks` (a shared private
726
+ helper built on `niceTicks`/`formatValue`), which draws left-edge tick marks, value labels, and
727
+ light horizontal gridlines, and returns the "nice" (rounded-up) domain max the chart's own marks
728
+ should scale against instead of the raw data max — see `barChart`'s own doc comment in
729
+ `src/index.js` for why that matters (so the tallest bar actually reaches the top tick). Extending
730
+ this to `line`/`area`/`stackedBar`/etc. is the same three-line change: call
731
+ `this._drawYAxisTicks(x, y, width, height, rawMax)` before drawing marks, and scale against its
732
+ return value instead of the raw max — not done yet for the rest, tracked as a follow-up.
733
+
734
+ ## Common options
735
+
736
+ Not every method takes every option below — the table above calls out what's notable per type —
737
+ but these recur across most 2D chart types:
738
+
739
+ ```javascript
740
+ {
741
+ title: 'Chart Title',
742
+ xAxisLabel: 'X Axis', yAxisLabel: 'Y Axis',
743
+ colors: ['#5470c6', '#91cc75'], // palette, indexed by series/category/segment
744
+ x: 70, y: 60, width: 500, height: 300, // chart box (defaults from container size)
745
+ showLegend: true, // or `legend:` depending on method — see table above
746
+ }
747
+ ```
748
+
749
+ Data-resolution options (used with `register()`/`refreshChart()`, see below):
750
+
751
+ ```javascript
752
+ { dataPath: 'metrics.sales', dataMapper: (v) => v * 1.1 }
753
+ ```
754
+
755
+ ### `colorCol` — plain field, or a compiled formula
756
+
757
+ `scatterChart`/`bubbleChart`/`bubbleChart3D`'s `colorCol` names which field to color-group points
758
+ by (falling back to a `category`/`group` field if present, and to one flat color if neither is
759
+ given). It also accepts a **compiled formula** over the point's own fields, instead of just a field
760
+ name — e.g. bucketing points by whether a value crosses a threshold, without pre-computing a
761
+ derived field before calling into RenderEngine:
762
+
763
+ ```javascript
764
+ engine.scatterChart(data, {
765
+ colorCol: { formula: 'value > threshold ? 1 : 0', params: ['value', 'threshold'] },
766
+ });
767
+ ```
768
+
769
+ `params` are field names read off each data point, in the order the formula uses them. The formula
770
+ is a small, dependency-free scalar-expression language (`src/kernelExpr.js`) — arithmetic,
771
+ comparisons, `&&`/`||`/`!`, `? :`, and `abs floor ceil round sign sqrt exp log sin cos tanh min max
772
+ pow clamp` — compiled once per `colorCol` object with `new Function` from an AST the parser built,
773
+ never from the formula text directly, so a bad or malicious-looking formula string is refused at
774
+ parse time rather than reaching `eval`. Every distinct value the formula returns gets its own
775
+ palette color, exactly the way a plain field's distinct values already do.
776
+
777
+ ## Data & utility methods
778
+
779
+ ```javascript
780
+ engine.setGlobalData(data) // set a global object referenceable by dot-path
781
+ engine.resolvePath(path, mapper?) // e.g. resolvePath('metrics.sales.0.value')
782
+ engine.register(id, type, options) // remember a {type, options} pair for a container id
783
+ engine.refreshChart(id) // re-invoke it — resolves options.data or dataPath+dataMapper fresh each time
784
+ engine.niceTicks(maxVal, count=4) // readable axis tick values
785
+ engine.formatValue(v) // 1234567 -> "1.23M"
786
+ engine.bindTooltip(element, text)
787
+ engine.bindRichTooltip(element, {title, rows:[{k,v}]})
788
+ engine.adjustColor(hex, amount) // lighten/darken, used internally for 3D shading
789
+ ```
790
+
791
+ `register`/`refreshChart` is the registration mechanism `addDataPoint`/`addGraphNode` (next section)
792
+ build on.
793
+
794
+ ## Interactivity
795
+
796
+ ```javascript
797
+ engine.hitTest(x, y) // shape under a point (manual on Canvas, native on SVG)
798
+ engine.enterState(shape, 'hover') // apply a named visual state
799
+ engine.leaveState(shape, 'hover')
800
+ engine.resetView() // reset SVG pan/zoom or 3D orbit camera to identity
801
+ ```
802
+
803
+ SVG-rendered charts get wheel-zoom and drag-empty-space-to-pan for free; 3D-orbit charts
804
+ (`bubble3D`, `bar3DOrbit`, `stackedBar3DOrbit`, `graphDiagram3D`) get drag-to-rotate/wheel-to-zoom
805
+ instead — the two are mutually exclusive per chart, never combined.
806
+
807
+ ## Getting a chart out — clipboard and files
808
+
809
+ ```javascript
810
+ import { copyImage, downloadPng, downloadSvg, toPngBlob } from 'renderengine';
811
+
812
+ await copyImage(container, { name: 'sales-by-region' }); // → {via:'clipboard'} | {via:'download', filename}
813
+ await downloadPng(container, { scale: 2 });
814
+ downloadSvg(container);
815
+ const blob = await toPngBlob(container, { scale: 2, background: '#ffffff' });
816
+ ```
817
+
818
+ Pass the container, the `<svg>` or the `<canvas>` — the drawable is found either way. Options are
819
+ `scale` (default 2, retina-sharp), `background` (default `'#ffffff'`, `null` for transparent) and
820
+ `name` (the download's filename stem).
821
+
822
+ `copyImage` **falls back to downloading the PNG** where writing images to the clipboard is
823
+ unavailable — Firefox, and any page not on a secure context — so the button always does something.
824
+ The returned `via` says which happened, so a caller can tell the user the truth rather than claiming
825
+ a copy that did not occur.
826
+
827
+ Three things this handles that a hand-rolled `XMLSerializer` + `canvas` will get wrong, each
828
+ invisible until someone pastes the result somewhere:
829
+
830
+ | | |
831
+ |---|---|
832
+ | **No intrinsic size** | An SVG sized by CSS (`width: 100%`) has no `width` attribute, and an `Image` loading that markup is 0×0 — a blank paste. Explicit pixel dimensions are set from the laid-out box. |
833
+ | **Transparent background** | Diagrams draw no background, so a PNG pasted into anything dark shows dark-on-dark labels. A background rect goes in as the first child, behind the drawing. |
834
+ | **Namespace loss** | Markup without `xmlns` will not load as an image at all; `xlink:href` (markers, patterns) needs its own, added only when something uses it. |
835
+
836
+ Rasterizing is capped at `MAX_IMAGE_PIXELS` (32M). Over that, the **scale** is reduced rather than
837
+ the diagram's own dimensions — halving the scale still shows the whole diagram, whereas cropping or
838
+ shrinking it loses content or legibility. `imageSize()` and `prepareSvgMarkup()` are exported
839
+ separately, and are pure, if you want the decisions without the DOM.
840
+
841
+ **The exported image does not inherit the page's theme.** Serialized markup loaded as an image sees
842
+ no author stylesheet, so the drawing falls back to the presentation attributes the engine itself
843
+ emits — dark text and axes — rather than whatever the workspace's dark theme was overriding them
844
+ with. That is the right result for something being pasted into a document on white paper, but it
845
+ does mean a copy taken from a dark-themed app comes out light. To export a genuinely dark image,
846
+ set `background` to the dark colour *and* render the chart with explicitly dark-friendly colours
847
+ rather than relying on CSS.
848
+
849
+ ## Dynamically adding data points in a bounded window
850
+
851
+ `addDataPoint`/`addGraphNode` are real `RenderEngine` methods — the engine owns the append/trim
852
+ mechanics, not the caller. Internally they delegate to a small class hierarchy in
853
+ `src/datawindow/` (see `ARCHITECTURE.md`'s "Data windows — streaming" section for the full design);
854
+ this section is just how to call it.
855
+
856
+ ### 1. Register, then push
857
+
858
+ ```javascript
859
+ engine.register('live-chart', 'line', { data: initialSeries, title: 'Live feed' });
860
+
861
+ let tick = 0;
862
+ setInterval(() => {
863
+ engine.addDataPoint('live-chart', () => ({ label: `T${++tick}`, value: randomValue() }), { maxPoints: 50 });
864
+ }, 1500);
865
+ ```
866
+
867
+ - `register(id, catalogKeyOrMethodName, options)` — same registration mechanism `refreshChart` has
868
+ always used; `options.data` is the chart's current dataset.
869
+ - `addDataPoint(id, pointOrFactory, { maxPoints })` — appends one point (dropping the oldest once
870
+ past `maxPoints`; `0` = unbounded) and re-renders via `refreshChart`. `pointOrFactory` can be a
871
+ literal point, or a factory — required for multi-series charts (`line`/`area`/`pipe`), where it's
872
+ called once per series so each series can get its own value, and useful for shapes like
873
+ `candlestick` where the next point depends on the last one:
874
+
875
+ ```javascript
876
+ // candlestick — factory receives the current data array, so the next bar
877
+ // can continue from the previous bar's close:
878
+ engine.addDataPoint('live-candles', (data) => {
879
+ const last = data[data.length - 1];
880
+ const open = last ? last.close : 100;
881
+ const close = +(open + (Math.random() - 0.5) * 6).toFixed(2);
882
+ return { label: `T${++tick}`, open, high: Math.max(open, close) + 1, low: Math.min(open, close) - 1, close };
883
+ }, { maxPoints: 100 });
884
+ ```
885
+
886
+ Every `CHART_CATALOG` entry declares a `streamShape` that decides how `addDataPoint` treats its
887
+ data — `addDataPoint` is a **no-op** when it's `'none'`:
888
+
889
+ | `streamShape` | Chart types | Behavior |
890
+ |---|---|---|
891
+ | `flat` | `bar`, `pie`, `scatter`, `bubble`, `candlestick`, `stackedBar`, `funnel`, `waterfall`, `histogram`, `qq`, `lag`, `treemap`, `sunburst`, `outline`, `bar3D`, `bubble3D` | one new record appended to the outer array |
892
+ | `series` | `line`, `area`, `pipe` | every series' own `values` grows by one point |
893
+ | `grouped-sample` | `boxPlot`, `density`, `violin` | exactly ONE randomly-chosen group's `values` grows (a new sample observation belongs to one group, unlike a shared time axis) |
894
+ | `none` | `heatmap`, `radar` (fixed matrix/axis-set), `bar3DOrbit`/`stackedBar3DOrbit` (fixed grid), `gauge-dial`/`gauge-bar`/`bullet` (a value to replace, not a window) | `addDataPoint` no-ops — for gauges, just call `dialGauge(newValue)`/`barGauge(newValue)`/`bulletGauge(newValue)` again directly |
895
+
896
+ ### 2. Graphs — `addGraphNode`
897
+
898
+ `graphDiagram`/`graphDiagram3D` aren't `CHART_CATALOG` entries (see ARCHITECTURE.md), so they get
899
+ their own method rather than going through `streamShape`. Register with `{nodes, edges}` instead of
900
+ `{data}` — `refreshChart` detects the graph shape via `options.nodes`:
901
+
902
+ ```javascript
903
+ engine.register('live-graph', 'graphDiagram', { nodes: initialNodes, edges: initialEdges });
904
+
905
+ setInterval(() => {
906
+ const parent = engine.registry['live-graph'].options.nodes[Math.floor(Math.random() * n)];
907
+ engine.addGraphNode('live-graph', { id: `n${++tick}`, label: `Node ${tick}` }, [{ source: parent.id, target: `n${tick}` }], { maxNodes: 30 });
908
+ }, 1500);
909
+ ```
910
+
911
+ `addGraphNode(id, node, newEdges, { maxNodes })` drops the oldest nodes once past `maxNodes`, and
912
+ any edge left dangling off a removed node along with them.
913
+
914
+ ### Chained form — `engine[id].addGraphNode(...)`
915
+
916
+ Once an id is `register()`ed, `engine[id]` (or `engine.someId` when the id happens to be a valid
917
+ identifier — most DOM ids, e.g. `chart-container`, contain a hyphen, so bracket notation is what
918
+ you'll actually type) returns a handle scoped to that container, dropping the repeated `id` first
919
+ argument:
920
+
921
+ ```javascript
922
+ engine.register('live-graph', 'graphDiagram', { nodes: initialNodes, edges: initialEdges });
923
+
924
+ setInterval(() => {
925
+ const parent = engine.registry['live-graph'].options.nodes[Math.floor(Math.random() * n)];
926
+ engine['live-graph'].addGraphNode({ id: `n${++tick}`, label: `Node ${tick}` }, [{ source: parent.id, target: `n${tick}` }], { maxNodes: 30 });
927
+ }, 1500);
928
+ ```
929
+
930
+ Both forms work interchangeably — `engine.addGraphNode(id, ...)` and `engine[id].addGraphNode(...)`
931
+ call the same method underneath. The handle also exposes `register`/`refreshChart`/`addDataPoint`
932
+ the same way. It only resolves for an id already in `engine.registry` (i.e. `register()` must still
933
+ be called the normal way first) — an unregistered id falls through to a real, `undefined` property
934
+ instead of a handle, same as before this existed.
935
+
936
+ ### 3. If you never `register()` a container
937
+
938
+ `addDataPoint`/`addGraphNode` require a prior `register()` call (they look up the chart's type and
939
+ current data from the registry). For a one-off container you don't want to register, own the array
940
+ yourself and re-call the chart method directly — `createDataWindow(streamShape, data, {max})` (also
941
+ exported, see `src/datawindow/index.js`) gives you the same append/trim classes standalone:
942
+
943
+ ```javascript
944
+ import { createDataWindow } from 'renderengine';
945
+
946
+ const window = createDataWindow('flat', initialData, { max: 50 });
947
+ setInterval(() => {
948
+ const data = window.push(() => ({ label: `T${++tick}`, value: randomValue() }));
949
+ engine.lineChart(data, { title: 'Live feed' });
950
+ }, 1500);
951
+ ```
952
+
953
+ ### Performance guidance
954
+
955
+ - Every tick is a full `clear()` + redraw, not a diff — cheap for tens–low-hundreds of points per
956
+ series, but don't drive it faster than the container actually needs to look "live." 1–2 second
957
+ intervals read as real-time to a human; sub-100ms intervals mostly burn CPU redrawing pixels
958
+ nobody perceives changing.
959
+ - If your update source fires faster than you want to redraw (e.g. a WebSocket pushing every
960
+ message), buffer incoming points and flush `addDataPoint` on a `requestAnimationFrame` or fixed
961
+ interval instead of calling it per message.
962
+ - **Canvas** renderer for high point counts or high-frequency updates (5,000+ point ceiling vs
963
+ SVG's ~1,000+); **SVG** if you need native per-point tooltips/hit-testing more than raw throughput.
964
+ - `graphDiagram`'s force layout (`_layoutForce3D`, `iterations` default 150) re-settles the whole
965
+ graph on every redraw — fine for occasional `addGraphNode` calls, but don't drive it on a
966
+ sub-second timer with a large node count; pin node positions (`arrange`-style layouts, see
967
+ `ARCHITECTURE.md`) if you need cheap, jitter-free incremental adds.
968
+
969
+ See `examples/csv-chart-builder.html`'s "Live updates" panels (section 4 for charts, section 8 for
970
+ graphs) for a full working implementation of all of the above, including the per-shape synthetic
971
+ value generators (`LIVE_APPENDERS`) that decide *what* value to invent each tick — legitimately
972
+ demo-specific, unlike the append/trim mechanics above.
973
+
974
+ ## ArchiMate Diagram Editor (OO component)
975
+
976
+ **`examples/archimate.html` is the canvas editor now** (`ArchimateCanvasPart`, type `archimate-canvas`, below under
977
+ "Diagram templates and palette categories"); this section's graph-engine editor is kept in
978
+ `examples/archimate-legacy.html` until the canvas editor has replaced it.
979
+
980
+ **In a workbench: `ArchimateEditorPart`.** The legacy modeller is a workbench part (type
981
+ `archimate-editor`, workspace bundle) built on `EditorPart`: it takes the model as part of its JSON
982
+ spec, fills its pane, and sits in a `<ws-workspace>` beside any other parts. `examples/archimate-legacy.html`
983
+ is this, and nothing else:
984
+
985
+ ```javascript
986
+ const { el, registerBuiltinParts, ArchimateEditorPart } = RenderEngineWorkspace;
987
+ const ws = document.body.appendChild(el({ tag: 'ws-workspace', attrs: { label: 'ArchiMate Modeller', theme: 'light' } }));
988
+ registerBuiltinParts(ws);
989
+ ws.registerPart(ArchimateEditorPart);
990
+ ws.addParts([{ id: 'modeller', type: 'archimate-editor', title: 'Model', elements: [...], relationships: [...], views: [...] }]);
991
+ ws.registerPerspective('Modelling', { type: 'part', partId: 'modeller' });
992
+ ws.activatePerspective('Modelling');
993
+ ws.getPart('modeller').model; // the ArchimateModel (an import replaces it)
994
+ ```
995
+
996
+ Spec options: `elements`, `relationships`, `views` and `name` (as `ArchimateModel` takes them),
997
+ `features` (toolbar controls; all on unless set `false`) and `analysis` (`true` adds the analysis panel).
998
+ The part builds the `ArchimateModel` and hands it to `ArchimateDiagramEditor` (its `model` option), with
999
+ `fullPage: false` and region ids unique to the part, and `ARCHIMATE_SERVICES` (the editor's classes and
1000
+ dialogs, imported directly) as its `workspace`.
1001
+
1002
+ **View settings, warnings and labels on the editor's canvas.** A host hands the editor the current view's
1003
+ resolved settings as `viewSettings: () => settingsManager.getEffectiveSettings(view, modelId)`
1004
+ (`editor.viewSettings()`, {} without one). The base editor draws the view's background, title band and
1005
+ watermark round the drawing from them (`applyViewDecorations` on the element holding the `<svg>`; a view
1006
+ without a background goes back to the page's own). `editor.edgeWarning(edge)` says what is wrong with a
1007
+ line, `{ message, highlight }` or null: the message is the line's tooltip, and with `highlight` the line is
1008
+ drawn red and dashed unless it has its own colour or dashes (class `edge-warning`). The ArchiMate canvas
1009
+ editor warns about a relationship the ArchiMate rules do not allow for its version (`ArchimateValidator`), as
1010
+ the view's `invalidRelationships` setting says: 'highlight' (the default), 'tooltip' or 'off'. A label
1011
+ expression (`${name} (${type})` or `{name}`, on the element's `label` or the instance's `labelOverride`) is
1012
+ resolved by the instance (`ElementInstance#getDisplayName`, through `LabelHelper.resolveNodeLabel`), so the
1013
+ tree, the dialogs and both editors show the same text.
1014
+
1015
+ **Properties: the shape, and what it stands for.** The base editor's shape dialog (`editTemplateProperties`:
1016
+ name, position, stacking, colours, text) and line dialog (label, sides, then only the style the line sets for
1017
+ itself, `edge.styleProperties`) each end with a subclass's own section: `conceptFields(template)` and
1018
+ `edgeConceptFields(edge)` give FormDialog fields, and `conceptCommand(template, values)` /
1019
+ `edgeConceptCommand(edge, values)` the command applying them; the whole edit is one undo step. The ArchiMate
1020
+ canvas editor's sections edit the model element (its own name, type, specialization among those registered for its
1021
+ type, icon among its type's choices, documentation, typed properties) and the
1022
+ relationship (kind, among those the rules allow and its own; documentation; typed properties), the same on every
1023
+ view, through `EditViewItemCommand`; an empty box or list is unset, and a new type drops a stored layer. An
1024
+ ArchiMate line's dashes and arrows come from its kind and a plain line's from its type, so neither is offered.
1025
+
1026
+ **Type pictures.** The model tree shows each element with its type's glyph in its layer colour, as the palette's tiles
1027
+ do, and a folder of one type in the Layers tree too (`typeGlyph(typeName, { size, version })` in
1028
+ `archimate/typeGlyph.js`, the one drawing both use; a 3.2 type under ArchiMate 4 is drawn as 3.2 draws it).
1029
+ `TreeBuilder`'s `iconFor(node)` gives a node a picture in place of its `icon` text.
1030
+
1031
+ **Nesting.** In the ArchiMate canvas editor, dropping a box wholly inside an element's box nests it there (Archi's
1032
+ nested elements): it moves with its parent, is raised above it, and an Archi file writes it inside it (whatever order
1033
+ they were added in). Dragged out again, it is taken out. The move and the nesting are one undo step. On a canvas, a
1034
+ block holds boxes the same way. Nesting one element in another then asks which relationship it stands for, among
1035
+ Archi's nesting kinds (`NESTING_KINDS`: composition, aggregation, assignment, realization, specialization) that the
1036
+ rules allow from parent to child, and none when the two already have one; **Add** puts it in the model, drawn as no
1037
+ line (the nesting shows it), as its own undo step; **No relationship** adds none. The shared editor does the
1038
+ finding: after a move, `DiagramEditor#nestingChanges(ids)` gives each moved shape's new parent (the innermost shape
1039
+ that wholly holds it and `canHold` it), and a subclass supplies `parentOf`, `canHold`, `nestCommand`
1040
+ (`NestInstanceCommand`) and `nested(changes)`; the template editor has no nesting. Model checks: **Check model…**
1041
+ warns of an element drawn inside another with no such relationship (`nested-without-relationship`), and a
1042
+ relationship a nesting shows is not reported as unused (`viewNestings(view)`, `nestingRelationships` in
1043
+ `diagrams/nesting.js`).
1044
+
1045
+ **Format painter.** The toolbar's **🖌** turns it on (Archi's Format Painter): with a shape selected it holds that
1046
+ shape's format, otherwise the first shape clicked gives it; every shape clicked after gets it (one undo step each),
1047
+ and nothing is selected or moved meanwhile. **Esc** or 🖌 again turns it off. A shape's menu has **🖌 Copy format**
1048
+ and, once one is held, **Paste format** (to the selection, when the shape is part of it). A format is what
1049
+ `Template#format()` reads from the shape's `static formatKeys` (paint and text style here; an ArchiMate shape's or
1050
+ group's whole appearance), as the shape holds it: a colour it takes from elsewhere (an ArchiMate layer colour) is
1051
+ left unset, so the shape painted gets its own default. `DiagramEditor#copyFormat(id)`, `pasteFormat(ids)` (each
1052
+ shape takes the keys it has; a locked one stops it) and `setFormatPainter(on)`.
1053
+
1054
+ **Properties of several shapes at once.** With more than one shape selected, a selected shape's menu offers
1055
+ **Properties of N shapes** (double-clicking one opens it too): one dialog with the colours and text style
1056
+ (`appearanceFields(template)`, the same fields as one shape's dialog) in which every field starts on **Keep**,
1057
+ greyed out, showing the value the shapes share (or empty, with "Differs between them"). Untick Keep and what you
1058
+ set goes to every shape; the rest stays as each one has it. The whole edit is one undo step; a locked shape in the
1059
+ selection stops it. `DiagramEditor#editSelectionProperties(ids?)` opens it and
1060
+ `selectionPropertiesCommand(templates, values)` is the edit (a value left `undefined` changes nothing); a
1061
+ subclass adds its own section with `manyConceptFields(templates)` and `manyConceptCommand(templates, values)`.
1062
+ The ArchiMate canvas editor's, when every selected shape is an element: their **type**, their **specialization**
1063
+ when they share a type, and **Set property** (a name and value, set where an element has that property and added
1064
+ where it doesn't), from `manyElementsFields` and `manyElementsEditCommands` in `archimate/elementProperties.js`.
1065
+ `PropertyListEditor` (`PropertyListEditor.field(properties, { key })`) is the typed-property list both
1066
+ ArchiMate dialogs use. Both editors fill the specialization catalog the same way:
1067
+ `ArchimateSpecializationRegistry.loadFrom(store)` reads the saved catalog (`SPECIALIZATIONS_STORE`, this browser's),
1068
+ or registers the defaults and saves them.
1069
+
1070
+ **Boxes, the tree and the menus on the canvas.** Every ArchiMate view adds boxes besides elements through
1071
+ `view.addBox(kindKey, at)` (one undo step), from the kinds its class lists in `BOX_KINDS`: Archi's Note and Image
1072
+ on an ArchiMate view (`DIAGRAM_BOX_KINDS`), Block, Sticky and Image on a canvas (`CANVAS_BOX_KINDS`);
1073
+ `canvasBoxMenuItems(view, at)` offers them on right-click in either editor. The canvas editor's element menu adds
1074
+ **Generate View For This** and **Add Missing Relationships (n)** (`missingRelationshipsOf(boxId)`, drawn as one
1075
+ step by `addMissingRelationships`), and ⚙ Settings → **Model settings…** when it has a `settingsManager`. The base
1076
+ editor reports selection changes (`editor.onSelectionChange(({ templateId, edgeId }) => …)`, which returns an
1077
+ unsubscribe); the canvas part keeps the model tree in step with it both ways and shows a canvas's hint, or the
1078
+ selected box's. `SettingsManager#onSettingsChange` returns an unsubscribe too.
1079
+
1080
+ **Specializations and viewpoints of your own.** ⚙ Settings → **Specializations…** and **Viewpoints…**, in both
1081
+ editors, list the user's catalog with Edit and Delete and "+ New …". A specialization has a name, an icon, a
1082
+ colour, a description and the element types it applies to (none: every type); it is saved in
1083
+ `SPECIALIZATIONS_STORE` and offered on elements of those types (`specializationCatalogDialog({ version, store,
1084
+ onChange })`). A viewpoint has a name (not a standard one's), a description and the element types it allows (at
1085
+ least one); it is saved in `CUSTOM_VIEWPOINTS_STORE` (`CustomViewpoints.load()`, `save(list)`,
1086
+ `dialog(options)`) and offered after the standard viewpoints, where it fades or hides the rest and limits the
1087
+ palette like any other. There is no "Custom" placeholder any more. `CatalogDialog` is the list editor both use
1088
+ (`{ title, noun, items, label, fields, validate, save, remove, onChange }`; deleting asks first), and FormDialog's
1089
+ `checklist` field (`choices` `[[value, text, group]]`, reads as the chosen values) is the element type list,
1090
+ grouped by layer as the palette groups them (`elementTypeChoices(version)`, from `elementTypeGroups`).
1091
+
1092
+ **An element's menu in the model tree** (both editors; `elementMenuItems(element, host)`): **Properties…** edits the
1093
+ element itself, the same on every view (`editElementProperties`: its name, type, specialization, icon,
1094
+ documentation and typed properties, one undo step on the open view); **Show in View (n)** lists the views it is on
1095
+ and opens the chosen one with it selected; **Add to "…"**, when the open view lacks it, shows it there in the middle
1096
+ of what is in sight (`visibleCentre()`) with its relationships to what is shown; **Generate View For This**. The
1097
+ element fields are one definition (`elementPropertyFields`, `elementEditCommand`), which the canvas editor's shape
1098
+ dialog also uses for its Element section.
1099
+
1100
+ **Pictures, analysis and filters on the canvas.** A picture file dropped on a view, or pasted, is an image box
1101
+ there (`editor.addImage(dataURL, point)`, one undo step, selected; the base editor's `enableImageDrop`). With
1102
+ `analysis: true` in its spec the canvas part shows the selected element's analysis (views it is in, its
1103
+ relationships) under the palette, for an element chosen on the canvas or in the tree. The tree's own filters (its
1104
+ "Filters…" dialog: text, types, specializations, properties) dim what they leave out on the canvas too:
1105
+ `modelTree.matchingElementIds()` (null with no filter on) and `modelTree.onFilterChange(fn)` feed
1106
+ `editor.setElementFilter`. The analysis names an element's layer as the tree files it (`treeLayerOf`).
1107
+
1108
+ **Groups on the canvas.** An Archi group (a model folder with bounds on the view, `view.placedFolders`) is a
1109
+ `ViewGroupTemplate`: Archi's box with a name tab (`TabbedBoxGlyph`), placed by its centre while its layout entry
1110
+ (`view.folderLayout[id]`: top-left x/y, width, height, zIndex, `appearance`) keeps the corner, so the editor's
1111
+ move, resize, restack and paint commands edit the view and undo there; renaming it renames the folder. Its paint
1112
+ is imported into `appearance` and written back as boxes' is (a change written, the rest as the file had it). A
1113
+ line to a group is drawn. Moving a container moves what it holds, however deep: the base editor's
1114
+ `moversFor` adds `containedIn(id)` (none in the base) for each mover; the canvas editor's groups hold the
1115
+ elements in their folder, the notes and pictures Archi drew in them (`instance.archi.parent`) and the groups
1116
+ inside them, and a canvas block holds the boxes Archi drew in it. Deleting a group takes it off the view
1117
+ (`RemoveGroupFromViewCommand`); what it held stays.
1118
+
1119
+ **Derived relationships on the canvas.** With `nodeOptions.showDerived` (⚙ Settings → Show derived, off by
1120
+ default; the canvas part's spec `showDerived`), the ArchiMate canvas editor draws each relationship
1121
+ `DerivedRelationships.compute(model, { maxHops: 2 })` derives between two elements on the view, as a
1122
+ `DerivedEdge`: dotted, labelled with its chain ("composition→assignment"), with "Derived: …" as its tooltip. It
1123
+ is drawn only: `edge.editable` is false, so the editor neither selects, routes nor deletes it (deleting a box
1124
+ leaves it alone). Any `Edge` can say so (`editable`, default true) and add to its tooltip (`tooltip`).
1125
+
1126
+ **Selecting several shapes.** In the base editor (so the template editor and the ArchiMate canvas alike): drag a
1127
+ box on empty canvas to select the shapes wholly inside it (Ctrl or Cmd adds them to the selection;
1128
+ `editor.templatesInside(rect)`, `editor.selectTemplates(ids, { extend })`); Shift- or Ctrl-click adds one;
1129
+ **Ctrl+A** selects every shape that is drawn (`selectableIds()`). Whatever is selected acts together, each as one
1130
+ undo step: dragging any of them moves them all (with their groups and what they hold); the arrow keys nudge
1131
+ them a grid step, one pixel without a grid, ten times as far with Shift (`nudgeSelection(dx, dy)`); **Delete**
1132
+ removes them once confirmed, and every line joined to any of them once (`deleteSelection()`,
1133
+ `deleteTemplatesCommand(ids)`; a subclass gives the per-shape part, `removeTemplateCommand(id)`, and the step's
1134
+ name, `deleteLabel(ids)`); grouping, locking and the Arrange menu already took the selection. A fixed shape among
1135
+ them stops a move or a delete.
1136
+
1137
+ **One undo history per model.** As in Archi, a model has one undo stack, `model.history` (a `ModelHistory`),
1138
+ that all its views share (`view.history` is it): every edit on any view, and the model's own changes, in the order
1139
+ made. Adding a view (New, Duplicate, Generate View, a new canvas, a model template), renaming one and deleting one
1140
+ are undo steps too (`AddViewCommand`, `RenameViewCommand`, `RemoveViewCommand`), as are a view's name and
1141
+ documentation from its Properties…. Each step remembers the view in front when it was made, and undoing or redoing
1142
+ it brings that view to the front, so the change is seen (Ctrl+Z on one tab can undo an edit made on another).
1143
+ Unsaved changes follow the one history (`isDirty()`). A model a part has just made or opened starts with nothing
1144
+ to undo.
1145
+
1146
+ ```js
1147
+ const model = new DiagramModel();
1148
+ const a = model.createView('A'); // one undo step
1149
+ model.renameView(a.id, 'First'); // another
1150
+ model.history.undo(); // 'A' again
1151
+ model.history.undo(); // the view is gone
1152
+ ```
1153
+
1154
+ **Find and replace.** Every `EditorPart` (the ArchiMate canvas part and the template editor) has an **Edit** menu
1155
+ after File: **Undo**, **Redo** and **Find and replace…**, over the whole model (`model.search()`, a
1156
+ `ModelSearch`, as `checker()` and `report()` are). The dialog (`FindReplaceDialog`) lists what matches as you
1157
+ type, with **Match case**, **Whole words**, **Regular expression** (`$1` in the replacement is its first group)
1158
+ and **Look in** names, documentation and property values; choosing a match shows it (its view, then the item
1159
+ selected), and **Replace all** replaces every match as one undo step on the model's history. What is
1160
+ searched: the model's name, documentation and properties and each view's title (never emptied), documentation and
1161
+ properties (`ModelSearch`); in the template editor also each shape's name and line's label
1162
+ (`DiagramModelSearch`); in an ArchiMate model also every element's and relationship's name, documentation and
1163
+ properties, and the notes on each view (`ArchimateModelSearch`). A property whose value is a number or true/false is
1164
+ left out. A notation's search adds its own texts with `viewItemTargets(view)` and `conceptTargets()`.
1165
+
1166
+ ```js
1167
+ const search = model.search();
1168
+ search.find('customer', { wholeWord: true, fields: ['name'] }); // [{ kind: 'element', id, owner, fieldLabel: 'Name', text, count, target }, ...]
1169
+ const cmd = search.replaceCommand('(\\w+) DB', 'DB of $1', { regex: true }); // a ReplaceTextCommand, or null
1170
+ model.currentView.history.execute(cmd); // one undo step
1171
+ ```
1172
+
1173
+ **Transform names, rename a property, fit to text.** The **Edit** menu has **Transform names…**: Title Case,
1174
+ Sentence case, split camelCase (OrderService → Order Service), camelCase, remove extra spaces, UPPER or lower case,
1175
+ on every name in the model or on those containing some text (`NAME_TRANSFORMS`, `transformName(key, name)` in
1176
+ `diagramModel/nameTransforms.js`; `model.search().transformCommand(key, text)`), one undo step, a view never left
1177
+ without a name. **Rename property…** renames a property everywhere it is (the model, its views, and an ArchiMate
1178
+ model's elements and relationships), one undo step; where something has both names, the old value fills the new one
1179
+ only if that is empty (`search.renamePropertyCommand(from, to)`, `search.propertyNames()`, over
1180
+ `search.propertyHolders()`, which a notation's search extends). Several elements' Properties also has **Remove
1181
+ property** (one any of them has). A shape's menu has **Fit to text**: the shape (or the selection it is part of) sized
1182
+ to its label, keeping its top-left corner, one undo step (`DiagramEditor.textSize(text, fontPixels, {padding, min})`
1183
+ estimates it; `editor.textFit(template)` gives the room a shape needs, more for an ArchiMate element's badge).
1184
+
1185
+ **HTML report.** File → **Export HTML report** (every `EditorPart` whose model has `report()`: the ArchiMate
1186
+ canvas part and the template editor) saves the whole model as one HTML file that opens in any browser, with nothing
1187
+ else to load: a tree on the left (Elements by layer, Relationships by kind, Views), a search box that keeps what
1188
+ matches by name, type or documentation, and a page per item. A view's page has its picture, and clicking an element
1189
+ on it opens that element's page; an element's page has its type, layer, documentation, properties, relationships
1190
+ (each with the element at the other end) and the views it is on; a relationship's links its source and target. The
1191
+ address says which page (`#element:id`), so Back and links work.
1192
+
1193
+ The parts, each usable alone:
1194
+
1195
+ ```js
1196
+ import { ArchimateModel } from './src/charting/diagrams/ArchimateModel.js';
1197
+ import { reportHtml, reportFileName } from './src/charting/report/reportHtml.js';
1198
+
1199
+ const model = new ArchimateModel({
1200
+ name: 'Shop',
1201
+ elements: [{ id: 'a', name: 'Customer', type: 'business-actor' }, { id: 'b', name: 'Order app', type: 'application-component' }],
1202
+ relationships: [{ id: 'r', kind: 'serving', source: 'b', target: 'a' }],
1203
+ views: [{ id: 'v', name: 'Main', elementIds: ['a', 'b'] }],
1204
+ });
1205
+ const data = model.report().data(); // plain JSON: { title, home, sections, items }
1206
+ data.items['element:a'].lists; // [{ title: 'Relationships', rows: [{ key: 'relationship:r' }] }, { title: 'Views', ... }]
1207
+ const html = reportHtml(data); // the file's text
1208
+ reportFileName(data.title); // 'shop-report.html'
1209
+ ```
1210
+
1211
+ `model.report({ pictures, title })` is a `ModelReport` (`src/charting/diagramModel/`): the model's page and a page
1212
+ per view, for any `ViewSetModel`; `ArchimateModel` returns an `ArchimateModelReport`, which adds the elements and
1213
+ relationships. A subclass adds items with `conceptItems()`, tree sections with `conceptSections()`, and a view's
1214
+ facts and lists with `viewFacts(view)` / `viewLists(view)`. Pictures come from the editor:
1215
+ `DiagramEditor#viewPicture()` is the current view as SVG cropped to what is drawn (no grid, selection or zoom), with
1216
+ a box per shape whose `reportRef(template)` names its page (`{kind: 'element', id}` in the ArchiMate editor);
1217
+ `part.viewPictures()` shows each view in turn to collect them. The page is written from `el()` specs by
1218
+ `toHtml(spec)` (`src/workspace/dom.js`: the same spec, as escaped HTML text), and its script is `reportViewer`.
1219
+
1220
+ **The File menu, grouped.** Every editor part's File menu (`EditorPart#fileMenuItems`) is: **New** ▸ (a view or
1221
+ diagram, a canvas, from a template, a model), Open…, Save, Save As… (with a ModelsPart also Open… in a new tab and
1222
+ Close model), **Import** ▸ (each format that imports, then the part's own: an Archi template, CSV with mapping),
1223
+ **Export** ▸ (each format, the part's own, then the HTML report and the pictures), the part's own groups (the canvas
1224
+ part's **Templates** ▸), and **This view** ▸ (its properties, duplicate, delete). A group of one item is that item
1225
+ (named as the group says it: "Export HTML report"). A part adds to it with `newItems()`, `ownImportItems()`,
1226
+ `ownExportItems()` and `ownFileItems()`; `EditorPart.menuGroup(label, items, { prefix })` and
1227
+ `EditorPart.tidyMenu(items)` make the groups; `modelFileMenuItems(formats, { only: 'export' | 'import' })` gives
1228
+ one direction's items.
1229
+
1230
+ **Open, save, and unsaved changes.** An editor part with file formats (`EditorPart`, the ArchiMate canvas part) has
1231
+ **Open…**, **Save** and **Save As…** at the top of File (**Ctrl+O**, **Ctrl+S**, **Ctrl+Shift+S**), kept by its
1232
+ `part.file` (`ModelFile`). Open asks for a file and takes the first of the part's formats whose extension and
1233
+ contents fit (`format.acceptsName(name)`, `format.recognizes(data)`: a `.xml` is Archi's or the Exchange Format by its
1234
+ namespace); Save writes back to that file where the browser can (the File System Access API: Chrome and Edge),
1235
+ otherwise it is Save As, which asks where (or, with no save dialog in the browser, downloads) in the open file's
1236
+ format or the part's `saveFormat()` (Archi's for the canvas part). The part knows whether the model changed since it
1237
+ was opened or saved: `model.markSaved()` / `model.isDirty()` / `model.onEdit(fn)` (`ViewSetModel`), from the model's
1238
+ one history (`CommandHistory#stateToken`, the same whenever it is back in a state, so undoing back is clean again),
1239
+ views added, renamed or deleted included. `part.fileLabel` ("shop.archimate •") shows in the part's toolbar and the page
1240
+ title, and leaving the page with changes unsaved asks first. An import is not a file: Save then asks where.
1241
+ `file.open()` is `file.choose()` (ask for a file and read it as a model, opening it nowhere yet) then
1242
+ `file.adopt(chosen)`.
1243
+
1244
+ **Several models open at once.** A `ModelsPart` (type `models`, one of `BUILTIN_PARTS`) holds one editor part per
1245
+ model, each a tab of a work area (`WorkArea`), so each keeps its own file, unsaved state and undo history, and copy
1246
+ and paste go between them. Its spec names the editor part type and what to open: `{ id, type: 'models', editorType:
1247
+ 'archimate-canvas', models: [editor part spec, ...], newModel: editor part spec }`. An editor part it hosts
1248
+ (`part.modelsHost`) has **New model**, **Open… in a new tab** (with file formats: the file is read first, and the
1249
+ tab is made only for a model) and **Close model** at the top of File, and File ▸ Import opens the imported model
1250
+ in a tab of its own (`part.onImported(model)`, `models.openModel(model)`; not hosted, it replaces the part's
1251
+ model). Its **Tools** menu (`part.hostMenus()`, `models.toolItems(part)`) works across the open models: **Compare
1252
+ with** ▸ and **Merge from** ▸ another one. A tab shows what the model is called (its file's name, else the model's name, as the toolbar does) and
1253
+ • with unsaved changes; closing one that has them asks first (its × too); the last one closed leaves a new, empty
1254
+ model; the page title follows the model in front. Keys an editor listens for on the page (undo, delete, Ctrl+S, ...)
1255
+ act only for the one on screen (`Part#isShown()`, `DiagramEditor#isShown()`). `examples/archimate.html` and the
1256
+ template editor's testbed host their editor this way:
1257
+
1258
+ **Comparing and merging models.** `model.compareWith(other)` (any `ViewSetModel`) is a `ModelComparison`: kind by
1259
+ kind (`model.comparedKinds()`, `ComparedKind` subclasses: `ViewsCompared` for every view-set model; an
1260
+ `ArchimateModel` adds `ElementsCompared` and `RelationshipsCompared`, and compares views with
1261
+ `ArchimateViewsCompared`), things matched by id are `'added'` (only the other has it), `'changed'` (with the
1262
+ `fields` that differ) or `'removed'` (only this one has it). A view is compared by what it draws, its boxes and
1263
+ lines known by their place in it, not by the ids reading a file makes, so a model read twice is alike.
1264
+ `comparison.mergeCommand(keys)` brings the chosen differences (default every added and changed one) from the other
1265
+ model, with what they need that this one lacks (a view's elements and relationships, a relationship's ends), as
1266
+ one undo step (`MergeModelCommand`); a view keeps its id, place and Archi folder, a template stays one.
1267
+ `ModelCompareDialog` shows a comparison (its **Merge…** goes on to merge), `ModelMergeDialog` chooses what to bring.
1268
+
1269
+ ```js
1270
+ const comparison = mine.compareWith(theirs);
1271
+ comparison.summary(); // [{kind, added, changed, removed}] per kind
1272
+ mine.history.execute(comparison.mergeCommand(['view:id-extra'])); // the view, and the elements it draws
1273
+ mine.history.undo();
1274
+ ```
1275
+
1276
+ ```js
1277
+ ws.addParts([{ id: 'models', type: 'models', editorType: 'archimate-canvas', newModel: { analysis: true },
1278
+ models: [{ id: 'modeller', title: 'Sample model', elements: [...], views: [...] }] }]);
1279
+ ws.registerPerspective('Modelling', { type: 'part', partId: 'models' });
1280
+ ```
1281
+
1282
+ **Align, distribute, match size, in every editor.** With two or more shapes selected, a shape's menu and the
1283
+ toolbar's **Arrange** menu (there always, greyed out with too few) offer **Align** (Left, Centre, Right, Top,
1284
+ Middle, Bottom), **Distribute** (Horizontally, Vertically; three or more) and **Match size** (Width, Height, Width
1285
+ and height). Align and match size follow the primary shape (the one selected last), as in Archi; distribute keeps
1286
+ the outermost two and makes the gaps between neighbours equal; match size keeps each shape's top-left corner. Each
1287
+ is one undo step; what a moved container holds moves with it; a locked shape stops it. `DiagramEditor` has
1288
+ `alignSelection(edge)`, `distributeSelection(direction)`, `matchSelectionSize(dimension)` and
1289
+ `alignMenuItems()`; where the shapes go is worked out from their boxes alone by `align`, `distribute` and
1290
+ `matchSize` (`src/charting/arrangement/alignment.js`), which return just the boxes that change:
1291
+
1292
+ ```js
1293
+ import { align, distribute, matchSize } from './src/charting/arrangement/alignment.js';
1294
+ const boxes = [{ id: 'a', left: 0, top: 0, width: 100, height: 50 }, { id: 'b', left: 150, top: 40, width: 60, height: 30 }, { id: 'c', left: 400, top: 10, width: 120, height: 80 }];
1295
+ align(boxes, 'top', boxes[1]); // [{ id: 'a', left: 0, top: 40 }, { id: 'c', left: 400, top: 40 }]
1296
+ distribute(boxes, 'horizontal'); // [{ id: 'b', left: 220, top: 40 }]
1297
+ matchSize(boxes, 'width', boxes[0]); // [{ id: 'b', width: 100, height: 30 }, { id: 'c', width: 100, height: 80 }]
1298
+ ```
1299
+
1300
+ **Import CSV with mapping.** File > **Import CSV with mapping…** (the ArchiMate canvas part) adds a spreadsheet's
1301
+ rows to the open model (it never replaces it): choose which column holds the name, the type (any case, a title, or
1302
+ CamelCase as Archi's elements.csv writes it; or one type for every row), an id, the documentation and which columns
1303
+ are properties, and optionally one whose cells name (or give the ids of) elements each row's element is related to,
1304
+ with the kind and direction. A row whose id, or name and type, is an element the model has updates its
1305
+ documentation and properties instead of making a second; rows naming the same element are one; a relationship the
1306
+ model has is not added twice, nor one the rules do not allow. The new elements can go on the open view, in a grid.
1307
+ One undo step; rows that could not be read (no name, an unknown type, an element to relate to that is not there)
1308
+ are listed after. The dialog suggests the columns from their names (Name, Type, ID, Documentation; the rest as
1309
+ properties), so Archi's elements.csv imports as it is. The CSV reader (`parseCsvTable`, `parseCsvRecords`,
1310
+ `guessDelimiter` in `src/csvTable.js`, which the CSV file format uses too) takes quoted cells with line breaks
1311
+ and commas, semicolons or tabs. `planCsvImport(table, mapping, model)` (`archimate/csvMapping.js`) plans it and
1312
+ `ImportRowsCommand` carries it out:
1313
+
1314
+ ```js
1315
+ const table = parseCsvTable(text);
1316
+ const plan = planCsvImport(table, { name: 'Name', type: 'Type', properties: ['Owner'], related: 'Uses', relatedKind: 'serving', relatedDirection: 'in' }, model);
1317
+ model.history.execute(new ImportRowsCommand(model, plan, { view: model.currentView })); // plan.problems: [{row, message}]
1318
+ ```
1319
+
1320
+ **draw.io export.** File > **Export draw.io (.drawio)** (the ArchiMate canvas part) saves every view as a page of one
1321
+ draw.io (diagrams.net) file, in draw.io's own ArchiMate 3 shapes: each box with its place, size, colours (its
1322
+ layer colour when it has none of its own) and text placement, a box nested in another as its child cell, notes,
1323
+ groups and canvas boxes; each line with its relationship's look (access by its access type) and its bendpoints. An
1324
+ element's or relationship's id, type, documentation and properties go on its cell's object, so draw.io keeps them.
1325
+ It is export only (`ModelFileFormat#imports` is false: no Import item, and Open… does not offer it).
1326
+ `toDrawioXML(model, { viewIds })` (`src/archimateDrawio.js`) writes it; `DRAWIO_ELEMENT_STYLES` and
1327
+ `DRAWIO_RELATIONSHIP_STYLES` are the type-to-shape tables.
1328
+
1329
+ **Roadmaps.** On the ArchiMate canvas, **View > Generate roadmap…** makes a new view of elements on a time line:
1330
+ each element with a start and an end date (by default its "Start Date" and "End Date" properties; ISO dates, a
1331
+ year and month, or day/month/year) is a bar from one to the other, under a header of years and months (short or
1332
+ full names), with month columns behind; bars that overlap in time go in lanes below each other. It takes the
1333
+ selected elements, or every element in the model with both dates; those without them are left out. The view comes
1334
+ to the front; one undo step. `model.generateRoadmap(elementIds, { startProperty, endProperty, name, monthNames })`
1335
+ returns `{ view, skipped }`; where everything goes is `roadmapPlan(items, options)` and dates are read by
1336
+ `parseDate(text)` (`src/charting/diagrams/roadmap.js`):
1337
+
1338
+ ```js
1339
+ const { view, skipped } = model.generateRoadmap(model.elements().map(e => e.id), { startProperty: 'From', endProperty: 'To' });
1340
+ ```
1341
+
1342
+ A note's own fill colour is drawn (it was always yellow); one without keeps the yellow sticky look.
1343
+
1344
+ **Add from text, in every editor.** Right-click the canvas → **Add from text…**: paste a list of names (from a
1345
+ document, a spreadsheet, a mail) and choose their kind, and a shape is made for each, placed in a grid from where
1346
+ you clicked, selected, one undo step. A name a line; a tab after it starts its documentation; blank lines are
1347
+ skipped and bullets and numbering ("- ", "• ", "1. ", "2) ") taken off. A first line `types=<kind>,<kind>,...`
1348
+ makes each tab-separated column that kind and each line a row of shapes (a kind by its key or its name, any case:
1349
+ `types=Business Actor,application-component`). The template editor makes its palette's shapes; the ArchiMate
1350
+ canvas makes new model elements with their boxes (the kinds its viewpoint allows; not on a canvas, whose boxes
1351
+ are not elements). `parseShapeText(text, {defaultType, resolveType})` and `placeShapes(parsed, at)`
1352
+ (`src/charting/textToShapes.js`) read and place the text; `DiagramEditor#addFromText(text, type, at)`,
1353
+ `shapeTypes()` and `createShapesCommand(items)`, which a subclass overrides to make its own kind of shape.
1354
+
1355
+ **Colour by, and legends, in every editor.** The **View** menu has **Colour by…** (a heat map): choose what to
1356
+ colour by, then a colour for each of its values (a soft palette by default, `DiagramEditor.COLOUR_PALETTE`; "No
1357
+ colour" leaves a value's shapes as they are), and whether to add a legend. Each shape takes its value's colour as
1358
+ its own fill, so the colours are kept like any other (an Archi file included), and one undo puts them back. The
1359
+ template editor colours by the kind of shape; the ArchiMate canvas by **Layer**, **Type**, **Specialization** or any
1360
+ **property** its elements have (notes and pictures have no value and stay as they are). A legend is a title and a
1361
+ box per value in its colour, in a row under the drawing (notes; on a canvas, stickies). **Add legend of the kinds
1362
+ shown** adds one for the kinds of shape (ArchiMate: the element types) in the colour of each's first shape. A
1363
+ locked shape keeps its colour. `colourKeys()`, `colourValue(template, key)`, `colourValues(key)`,
1364
+ `colourByCommand(key, colours, { legend })`, `kindLegendEntries()` and `legendCommand(entries, title)`, which a
1365
+ subclass overrides to say what its shapes are coloured by and what its legend boxes are:
1366
+
1367
+ ```js
1368
+ editor.colourByCommand('property:Lifecycle', { invest: '#98fb98', maintain: '#ffc300', retire: '#ffa07a' }, { legend: true });
1369
+ ```
1370
+
1371
+ **Layouts, in every editor.** The toolbar's **Arrange** menu has **Layout** (**Layout the view** with fewer than two
1372
+ shapes selected): **Grid**, **Circle**, **Tree, top down**, **Tree, left to right** (along the lines between the
1373
+ shapes: a parent centred over its children; a shape no line reaches is a root), **Force-directed** (lines pull,
1374
+ shapes push apart; the same input gives the same result) and **By kind, a row each** (the shape class in the
1375
+ template editor, the layer on the ArchiMate canvas). The shapes keep the top-left corner of the area they took;
1376
+ what a container holds goes with it; one undo step; a locked shape stops it. On the ArchiMate canvas, an element
1377
+ with parts on the view (the targets of its composition and aggregation relationships) has **Nest its parts inside**
1378
+ in its menu: each part nested in it, theirs in them however deep, in a grid below its label, the parent grown to
1379
+ hold them. `DiagramEditor#layoutShapes(kind, ids?)`, `layoutCategory(template)`, `partsOf(id)` and
1380
+ `nestPartsInside(id)`; where shapes go comes from `layout(kind, boxes, {edges, categoryOf, gap})` and
1381
+ `nestLayout(rootId, partsOf, boxOf)` (`src/charting/arrangement/layouts.js`), from the boxes alone:
1382
+
1383
+ ```js
1384
+ import { layout } from './src/charting/arrangement/layouts.js';
1385
+ const boxes = [{ id: 'a', left: 0, top: 0, width: 100, height: 50 }, { id: 'b', left: 0, top: 100, width: 100, height: 50 }, { id: 'c', left: 0, top: 200, width: 100, height: 50 }];
1386
+ layout('tree-down', boxes, { edges: [{ source: 'a', target: 'b' }, { source: 'a', target: 'c' }], gap: 40 });
1387
+ // [{ id: 'a', left: 70, top: 0 }, { id: 'b', left: 0, top: 110 }, { id: 'c', left: 140, top: 110 }]
1388
+ ```
1389
+
1390
+ **Copy and paste, in every editor.** **Ctrl+C** copies the selection, **Ctrl+X** cuts it (one undo step), **Ctrl+V**
1391
+ pastes (a little further along each time, selected), **Ctrl+D** duplicates without touching the clipboard; a shape's
1392
+ menu has Cut, Copy and Duplicate and the canvas menu **Paste here** (its top-left corner where you clicked). The
1393
+ clipboard is the page's (`DiagramClipboard`), so a copy pastes into another view, tab or model; an editor pastes only
1394
+ copies of its own kind (`static clipboardKind`: 'diagram' for the template editor, 'archimate' for ArchiMate views).
1395
+ An editor gives what a copy is (`copyPayload(ids)`) and how it is made again (`pasteCommand(payload, { dx, dy })`):
1396
+ the template editor copies each shape (its class and `serialize()`) and the lines between them; an ArchiMate view
1397
+ copies its items (`view.toJSON({ itemIds })`: boxes, the lines and relationships between them, the elements and
1398
+ relationships they show) and pastes them as new boxes for the **same** elements (`view.pasteCommand(data)`,
1399
+ `PlaceItemsCommand`), adding to the model what it lacks (a paste from another model) and to a view's element list
1400
+ what it does not list. Groups are not copied (they are folders).
1401
+
1402
+ **Mouse buttons, in every editor.** A left drag on empty canvas draws the selection box; the **right button drags
1403
+ the view** (a right-click that does not move still opens the menu), as do the middle button and Shift+left (Alt+left
1404
+ in the main ArchiMate editor). In the main ArchiMate editor (the graph engine) the box selects nodes too
1405
+ (`graph.selectNodes(ids, { extend })`, kept on the engine across redraws and marked with a glow), and dragging one of
1406
+ them moves them all as one undo step.
1407
+
1408
+ **Emphasis: highlight, viewpoint, filter.** A shape or line can be drawn as usual, dimmed (opacity 0.3),
1409
+ faded (0.2) or not at all (hidden), and only how it is drawn changes. Each reason is an `EmphasisRule`
1410
+ (`template(template, editor)` and `edge(edge, editor)` give `null`, `'dimmed'`, `'faded'` or `'hidden'`);
1411
+ `editor.emphasisRules()` lists the active ones, and the strongest level any of them gives wins
1412
+ (`templateEmphasis`, `edgeEmphasis`; a line to a hidden shape is hidden). A dimmed or faded item is drawn in
1413
+ a `<g opacity data-emphasis>` with its `data-template-id` or `data-edge-id`. The base editor's rule is the
1414
+ highlight: **View → Highlight…** (`editor.setHighlight(text)`, `HighlightRule`) dims every shape whose name
1415
+ does not contain the text, in any case, in the template editor and the ArchiMate canvas alike. The ArchiMate canvas
1416
+ editor adds `ViewpointRule`: with `nodeOptions.viewpoint` (⚙ Settings → Viewpoint), elements of types the
1417
+ viewpoint does not allow are faded, or hidden with `viewpointMode: 'hide'` (⚙ Settings → Outside the
1418
+ viewpoint), a line with one end outside is faded and one with both outside hidden; notes and canvas boxes are
1419
+ left alone. It also adds `ElementFilterRule` (`editor.setElementFilter(elementIds)`, null clears), which
1420
+ dims the model elements a filter did not keep. The palette then offers only the types the viewpoint allows
1421
+ (`new ArchimatePalette({ viewpoint })`).
1422
+
1423
+ ```js
1424
+ editor.setHighlight('order'); // the rest dimmed
1425
+ canvasEditor.nodeOptions = { ...canvasEditor.nodeOptions, viewpoint: 'Organization', viewpointMode: 'fade' };
1426
+ canvasEditor.templateEmphasis(appComponentShape); // 'faded'
1427
+ ```
1428
+
1429
+ **View menu, image export, model check, generated views.** These live in the `DiagramEditor` base, so the
1430
+ template editor (the testbed) and both ArchiMate editors have them. The **View** menu
1431
+ (`editor.viewMenuItems()`) has **Zoom to fit** (`editor.zoomToFit()`: everything drawn in sight, in the
1432
+ drawing's own units; `ViewFit` computes it, as a viewBox for the graph engine or a scale and pan for the
1433
+ template editor), **Reset zoom**, **Highlight…** (below), and, when the editor has a `model`, **Check model…**
1434
+ (`editor.showModelCheck()`). The checks are the model's own (`model.checker(options)`): a `ViewSetChecker` for
1435
+ any view set (empty views; an empty template is not one), and for an ArchiMate model an `ArchimateModelChecker`
1436
+ that adds relationships the specification does not allow, likely duplicates (elements of one type whose names
1437
+ have the same words, ignoring case, punctuation, word order and filler words such as "of" and "the":
1438
+ `nameKey(name)` and `duplicateGroups(elements)` in `diagrams/duplicates.js`), elements and relationships on no
1439
+ view, nested elements with no relationship, and names or documentation with unprintable characters (control
1440
+ characters, zero-width spaces; tabs and line breaks are fine in documentation). Each check is a method listed by
1441
+ `checks()`, so a subclass changes the set; choosing a finding shows it (`editor.revealFinding(finding)`: its view,
1442
+ its shape, or in the ArchiMate tree), and a finding can offer actions beside it (`editor.findingActions(finding)`).
1443
+ On the ArchiMate canvas, likely duplicates have **Merge…**: choose the element to keep (each choice says how many
1444
+ views and relationships use it) and the others are merged into it, one undo step (`model.mergeCommand(keepId,
1445
+ otherIds)`, a `MergeElementsCommand`). The kept element takes their relationships, their boxes on every view, the
1446
+ documentation and properties it lacks and a specialization if it has none; a relationship left joining it to
1447
+ itself, or the same as another, goes, its lines drawn as the other. The **File** menu (`EditorPart`'s, and the ArchiMate editor's) exports the current view
1448
+ as an image: PNG, SVG or a copy to the clipboard (`editor.exportImage('png' | 'svg' | 'copy')`, without the grid
1449
+ or selection; the ArchiMate editor's is off with `features.exportImage: false`). **Generate View For This**, on
1450
+ an element's menu in the diagram or the tree, makes a view of the element with everything related to it
1451
+ around it and every relationship between them (`model.generateView(elementIds, { name, related })`, Archi's
1452
+ "Generate View For").
1453
+
1454
+ ```js
1455
+ model.checker({ version: '3.2' }).run(); // [{ code, severity, kind, ids, message }, ...]
1456
+ model.generateView(['customer']); // a new current view: the customer and what it relates to
1457
+ ```
1458
+
1459
+ **Properties of an element, note or line.** Double-click it, or **Show/Edit Properties…** on its menu. The
1460
+ dialog's Object section is the concept (name, type, documentation as "description", specialization,
1461
+ properties), the same on every view; its Diagram section is how this view draws it: label, fill colour, line
1462
+ colour, text colour and icon, and a **Text** section from `TextStyle` (font, text size, bold, italic, alignment,
1463
+ position; the same fields the template editor shows). Bold and italic go to Archi in its font; Archi has no
1464
+ underline, so it is offered for template shapes only. Save
1465
+ keeps the changes where they live: the concept's in the model (a note's, image's or plain line's in the item
1466
+ itself), the rest on the view's instance, as one undo step on the view (`EditViewItemCommand`;
1467
+ `editor.editItemProperties(item, kind)` opens it, `viewItemChanges(kind, before, after)` sorts an edit). A
1468
+ line's colour is this view's. "No colour" (`none`) draws no fill or no outline; an element's icon then stays
1469
+ black.
1470
+
1471
+ **Views.** Right-click a view's tab, or its node in the model tree, for **Properties…**, **Duplicate**
1472
+ and **Delete** (the last view stays); the File menu starts with **New view…** and the current view's
1473
+ items. Double-clicking empty canvas, or the canvas menu's **View Properties…**, opens the same
1474
+ properties. `SettingsViewPropertiesDialog` edits the view's name (through `model.renameView`) and
1475
+ documentation, then the view's level of the settings cascade: routing, grid size and grid shown under
1476
+ Diagram, and background colour and image, a title band (text, position) and the watermark under
1477
+ Appearance. A cascading setting may inherit, and shows the value and the level it would come from. The diagram draws the view's resolved settings (background, title band, watermark).
1478
+ A filter (the tree's Filters…) is matched by model element, so it treats every view, a duplicate
1479
+ included, the same way, and never dims notes or images.
1480
+ A duplicate is independent: every element and relationship instance is copied with a new id, each
1481
+ relationship re-linked to the copied ends, with the element filter, viewpoint, documentation, view
1482
+ settings and folder layout; its undo history starts empty. View documentation round-trips through
1483
+ Archi export and import. The editor follows its model, so the same happens however a view is
1484
+ changed: `editor.model.duplicateView(id)` opens and shows the copy, deleting a view closes its tab.
1485
+
1486
+ **Canvases and templates.** As in Archi, the model tree's **Views** holds every view, canvases (`CanvasView`)
1487
+ and sketches included, in the sub-folders the Archi file kept it in (`view.archiFolder`; `viewFolderNodes(views,
1488
+ depth)` makes a level of them); templates are apart (`model.diagramViews()`, `model.canvases()`,
1489
+ `model.templates()`). The **Templates** folder holds the model's own templates (canvases
1490
+ marked `template`, saved in the `.archimate` file) and a **Library** of templates kept across models: a
1491
+ `CanvasTemplateLibrary` (a `TemplateLibrary`, the base any kind of view's library extends: entries, built-ins
1492
+ `DEFAULTS`, storage under `STORAGE_KEY`, change events), by default in this browser's storage (the editor's `templateLibrary` option takes
1493
+ another, or `null` for none). Right-click Views or one of its folders for **New canvas…** and **New canvas from template**; a
1494
+ canvas for **Save as model template…** and **Save to template library…**; a template for **New canvas from
1495
+ this**; Templates or Library for **Import Archi template…**, which reads Archi's `.archicanvas` and
1496
+ `.architemplate` files (a zip of a manifest, a thumbnail and the template's model, pictures included) into the
1497
+ library. The File menu offers the same. `CanvasTemplateMenu` builds these items, as `ViewMenu` does for views;
1498
+ its `askName`, `chooseFile` and `report` options replace the dialogs. A canvas made from a template gets new
1499
+ ids and the template's boxes and lines; the boxes become the canvas's background (a locked `BackgroundGroup`,
1500
+ behind everything added later; see "A background" under grouping and locking).
1501
+
1502
+ **Drawing on a canvas: Archi's palette, lines and hints.** On a canvas, right-click empty space for **Add
1503
+ Block**, **Add Sticky** and **Add Image…** (a picture file): the box is centred where you clicked, one undo step
1504
+ (`canvas.addBox(kind, at, { name, iconUrl })`; the kinds, with Archi's classes and looks, are `CANVAS_BOX_KINDS`,
1505
+ each a `CanvasBoxKind`; `canvasBoxMenuItems(view, at)` is the one menu definition both ArchiMate editors use).
1506
+ A line drawn between two boxes is a canvas connection (Archi's `CanvasModelConnection`), not a relationship:
1507
+ which a line is, is the view's rule (`view.linesAreRelationships(a, b)`: both ends model elements), so a line
1508
+ from a note on an ArchiMate view is a plain line too (`view.connect(a, b)` draws one, `view.removeRelationship(id)`
1509
+ takes it off; one undo step each). A **Hints** panel in the drawing's corner (`HintPanel`, folds to its header)
1510
+ shows the canvas's hint, and a box's own when it is clicked (`canvas.hint`, `canvas.hintFor(id)`; `ArchiHint`
1511
+ reads Archi's `hintTitle`/`hintContent` from any record and turns the HTML into plain text: it is never
1512
+ inserted as HTML). **Edit Hint…** on a box, and **Edit Canvas Hint…** on empty canvas, set a hint as a title and
1513
+ plain text, one undo step (`EditHintCommand`, on anything with an `archi` record; `ArchiHint.write` stores the
1514
+ text as escaped HTML paragraphs, as Archi keeps it; `hintMenuItems(view, itemId)` is the one menu definition). In
1515
+ the canvas editor, a canvas's side palette is Archi's (`CanvasPalette`, a kind of `ArchimatePalette`: Block,
1516
+ Sticky, Image, clicked or dragged in), and its plain lines are drawn and edited like any line (`ViewLineEdge`).
1517
+
1518
+ ```js
1519
+ const goal = canvas.addBox('block', { x: 300, y: 200 }, { name: 'Goals' });
1520
+ const idea = canvas.addBox('sticky', { x: 600, y: 200 }, { name: 'Idea' });
1521
+ canvas.connect(idea.id, goal.id); // a CanvasModelConnection
1522
+ canvas.hintFor(goal.id); // { title, content, text } or the canvas's
1523
+ ```
1524
+
1525
+ **Placing a template, and saving part of a canvas.** Right-click empty canvas for **Place template ▸** (the
1526
+ model's templates, then the library's): its boxes and lines go where you clicked, as their own locked
1527
+ background group, one undo step (`canvas.placeTemplate(source, { at, name })`, a `PlaceItemsCommand`), in front
1528
+ of any earlier background and behind everything else. Right-click a box for **Save as template ▸** (model or
1529
+ library): the box with its group (in the canvas editor, the selection with its groups), and the lines between
1530
+ them (`canvas.toJSON({ itemIds })`, `model.saveAsTemplate(viewId, title, { itemIds })`; `Arrangement#only(ids)`
1531
+ keeps their groups and locks). `CanvasTemplateMenu` builds these items (`placeTemplateItems(viewId, at)`,
1532
+ `saveAsTemplateItems(viewId, { itemIds })`) for both ArchiMate editors; the template editor's base offers the
1533
+ hooks (`shapeMenuExtras(id)`, `canvasMenuItems(at)` for empty canvas).
1534
+
1535
+ ```js
1536
+ canvas.placeTemplate(library.get('builtin-kanban').view, { at: { x: 400, y: 300 } }); // locked, behind
1537
+ model.saveAsTemplate(canvas.id, 'Lanes', { itemIds: [laneA, laneB] }); // a model template of two boxes
1538
+ ```
1539
+
1540
+ The library starts with built-in templates, so there is something to choose before anything is imported:
1541
+ SWOT analysis, PESTLE analysis, Stakeholder map (power and interest), Priority matrix (urgent and
1542
+ important), Retrospective, Kanban board and Architecture decision (`DEFAULT_CANVAS_TEMPLATES`). They are
1543
+ written for RenderEngine; each block carries a hint (Archi's `hintTitle`/`hintContent`), so a canvas made
1544
+ from one explains itself in Archi too. Built-ins come first, are marked `builtIn`, are not stored, and cannot
1545
+ be renamed or removed; `new CanvasTemplateLibrary({ defaults })` replaces them, and `defaults: []` gives none.
1546
+ `canvasTemplate({ id, name, description, blocks })` makes an entry from a list of blocks.
1547
+
1548
+ ```js
1549
+ const library = new CanvasTemplateLibrary(); // or { store: { load(fallback), save(value) } }
1550
+ model.newCanvasFromTemplate(library.get('builtin-swot').view, 'Our SWOT');
1551
+ const [bmc] = library.importArchiTemplate(bytes); // an .archicanvas file's bytes
1552
+ const canvas = model.newCanvasFromTemplate(bmc.view, 'Our business model'); // current view
1553
+ model.saveAsTemplate(canvas.id, 'Business model template'); // a model template; current view unchanged
1554
+ library.add({ view: canvas.toJSON(), name: 'Mine' }); // a library template
1555
+ ```
1556
+
1557
+ Archi's own canvas templates are not bundled (their content is licensed CC BY-SA); import them from an
1558
+ Archi install's `plugins/com.archimatetool.canvas_<version>/templates` folder.
1559
+
1560
+ **`ArchimateDiagramEditor`** is a reusable, self-contained class that encapsulates the complete lifecycle
1561
+ of an ArchiMate modeling UI: DOM structure creation, toolbar UI generation, model management, tree
1562
+ navigation, diagram rendering with styling, export/import, and control wiring. It is exported from both
1563
+ `RenderEngine` and `RenderEngineWorkspace`.
1564
+
1565
+ ### Self-contained design
1566
+
1567
+ The class owns **everything**: DOM structure, CSS styling, toolbar controls, event handling, and diagram
1568
+ rendering. The host HTML needs only:
1569
+ - One outer container div (e.g., `<div id="app"></div>`)
1570
+ - Model data (elements, relationships, views)
1571
+ - Feature configuration
1572
+
1573
+ ### Setup (with automatic toolbar)
1574
+
1575
+ ```javascript
1576
+ new RenderEngineWorkspace.ArchimateDiagramEditor({
1577
+ elements: [...],
1578
+ relationships: [...],
1579
+ views: [...],
1580
+ document,
1581
+ appContainerId: 'app', // Class generates toolbar, main, tree, diagram divs
1582
+ features: { // Toolbar is auto-generated based on these flags
1583
+ version: true,
1584
+ viewpoint: true,
1585
+ distinctShapes: true,
1586
+ showDerived: true,
1587
+ exportCSV: true,
1588
+ importCSV: true,
1589
+ exportArchimate: true,
1590
+ importArchimate: true,
1591
+ },
1592
+ workspace: window.RenderEngineWorkspace,
1593
+ }).render();
1594
+ ```
1595
+
1596
+ ### Setup (legacy: manual HTML + wireControls)
1597
+
1598
+ If you prefer to supply your own HTML with existing controls, omit `features` and use `wireControls()`:
1599
+
1600
+ ```javascript
1601
+ const editor = new RenderEngineWorkspace.ArchimateDiagramEditor({
1602
+ elements: [...],
1603
+ relationships: [...],
1604
+ views: [...],
1605
+ document,
1606
+ appContainerId: 'app', // or provide pre-made divs:
1607
+ toolbarContainerId: 'toolbar', // treeContainerId: 'tree',
1608
+ treeContainerId: 'tree', // diagramContainerId: 'diagram'
1609
+ diagramContainerId: 'diagram',
1610
+ workspace: window.RenderEngineWorkspace,
1611
+ });
1612
+
1613
+ // Wire controls for manually-created HTML
1614
+ editor.wireControls();
1615
+ editor.render();
1616
+ ```
1617
+
1618
+ ### Core methods
1619
+
1620
+ - **`render(options)`** — Render the current view's diagram with all options, styling, and position persistence. Called automatically on control changes if auto-toolbar is enabled.
1621
+ - **`savePositions()`** — Save element positions before a view switch.
1622
+ - **`exportCSV()`** — Export model to CSV format (elements + relationships). Returns a `Blob`.
1623
+ - **`importCSV(elementsCSV, relationshipsCSV)`** — Import the two CSV tables and replace the model: its elements and relationships, with one view of them all ("Imported elements"; a CSV holds no views). `importCSVText(text)` takes the one file `exportCSV()` writes.
1624
+ - **`replaceModel(model)`** — Edit another `ArchimateModel` in place of this one (every import ends here), closing the old views' tabs.
1625
+ - **`exportArchimate()`** — Export the model with every view as `.archimate` XML. Returns XML string (pictures stay `data:` URLs).
1626
+ - **`exportArchimateFile()`** — Export as Archi saves it: `toArchiFile`'s `{ data, archive, mimeType }`, a zip archive when a view holds pictures. The File menu's Export uses it.
1627
+ - **`importArchimate(file)`** — Import an Archi file, as XML text or as the file's bytes (plain or archived), and replace the entire model.
1628
+ - **`exportExchange()`** — Export the model as an Open Group Exchange Format file (XML text); its ArchiMate views are written, canvases and sketches are not. The File menu's "Export Exchange Format (.xml)" (feature `exportExchange`) downloads it.
1629
+ - **`importExchange(xml)`** — Import an Exchange Format file and replace the entire model (File menu, feature `importExchange`).
1630
+ - **`fileFormats`** (constructor option, default `ARCHIMATE_FILE_FORMATS`) — The file formats the File menu exports and imports; each is offered when its `features` flags are on (`export<featureName>`, `import<featureName>`).
1631
+ - **`wireControls(options)`** — Wire up control event listeners for manually-created HTML controls (version, viewpoint, distinct shapes, show derived, manage dialogs, filter). Auto-renders on change. Optional callbacks for custom handlers.
1632
+ - **`setupImageDragDrop()`** — Enable drag-drop and paste image support on diagram.
1633
+ - **`setDependencies(deps)`** — Inject dialog functions and custom callbacks (dependency injection).
1634
+ - **`saveSpecializations()`** — Persist specialization registry to localStorage.
1635
+ - **`getOption(controlId, defaultValue)`** — Read a control value from the DOM.
1636
+
1637
+ ### Properties (read-only)
1638
+
1639
+ - **`model`** — The `ArchimateModel` instance.
1640
+ - **`tree`** — The `ModelTree` navigator.
1641
+ - **`currentView`** — The active `ArchimateView`.
1642
+ - **`currentEngine`** — The active `RenderEngine` instance for the view.
1643
+ - **`diagrams`** — The `DiagramTabs` tab manager. `diagrams.containerFor(viewId)` is the element a view is
1644
+ drawn in. Nothing it builds carries an id (it uses the classes `diagram-area`, `diagram-tab-bar`, and
1645
+ `diagram-view` with `data-view-id`), and each `SVGRenderer` prefixes its line-end marker ids
1646
+ (`renderer.markerId('arrow')`, `renderer.markerUrl('arrow')` → `url(#re0-arrow)`), so several editors can
1647
+ share a page.
1648
+
1649
+ ### Styling & View Properties
1650
+
1651
+ The `render()` method applies:
1652
+ - **Background color** and **background image** to the diagram container.
1653
+ - **Title** (top or bottom, or hidden).
1654
+ - **Watermark** with three repeat modes: `'none'` (centered), `'repeat'` (diagonal), `'tile'` (canvas background).
1655
+
1656
+ All are properties of the current view's model object and persisted with the view.
1657
+
1658
+ All styling (page layout, containers, toolbar controls, tree nodes) is **generated by the class via CSS**—
1659
+ no stylesheets needed in host HTML.
1660
+
1661
+ ### Feature flags (auto-toolbar mode)
1662
+
1663
+ When a `features` config is provided, the class builds its toolbar from `menuBarItems()`, rendered by
1664
+ the shared `MenuBar` (see "Toolbars and dialogs" below):
1665
+
1666
+ - **`search`** — Regex-based element filter with magnifying glass icon (🔍) above the tree. Real-time filtering as user types. Accepts regex patterns (case-insensitive). Clear with backspace to restore full diagram. Also searches documentation with `{ searchDocs: true }` option.
1667
+ - **⚙ Settings** — a menu, always present, as in the template editor (`settingsMenuItems()`). It holds
1668
+ the notation settings below, each shown with its current value, then **Model settings…** (the model
1669
+ settings dialog, `showModelSettings()`: routing, grid, watermark).
1670
+ - **`version`** — Settings ▸ Version ▸ ArchiMate 4 / 3.2
1671
+ - **`viewpoint`** — Settings ▸ Viewpoint (every built-in and custom viewpoint) and ▸ Viewpoint mode (fade/hide)
1672
+ - **`distinctShapes`** — Settings ▸ Distinct shapes: on/off
1673
+ - **`showDerived`** — Settings ▸ Show derived: on/off
1674
+ - **`exportCSV`**, **`importCSV`**, **`exportArchimate`**, **`importArchimate`** — entries in the **File** menu,
1675
+ after the view items (New view…, Properties…, Duplicate, Delete)
1676
+
1677
+ An element's right-click menu orders it with the same **⬍ Order** submenu as the template editor
1678
+ (Bring to Front, Send to Back, Raise, Lower; `DiagramEditor.ORDER_OPERATIONS`), each one undo step on
1679
+ the view.
1680
+
1681
+ Set any to `false` to leave that setting out of the menu. Values live on the editor: `getOption(key)` / `setOption(key, value)`,
1682
+ keys `archimate-version`, `archimate-viewpoint`, `archimate-viewpoint-mode`, `archimate-distinct-shapes`,
1683
+ `archimate-show-derived`. Hosts that supply their own controls with those element ids (legacy
1684
+ `wireControls()` mode) are read from the DOM when the editor holds no value.
1685
+
1686
+ **Search feature examples:**
1687
+ ```javascript
1688
+ // Basic search by name
1689
+ setFilter('business') // Shows only business-layer elements
1690
+
1691
+ // Regex patterns
1692
+ setFilter('actor|process') // Shows actors and processes (alternation)
1693
+ setFilter('.*service') // Shows elements ending with 'service'
1694
+ setFilter('^Customer') // Shows elements starting with 'Customer'
1695
+
1696
+ // Include documentation in search
1697
+ setFilter('critical', { searchDocs: true })
1698
+ setFilter('urgent', { searchDocs: true })
1699
+
1700
+ // Clear filter
1701
+ setFilter('') // Restores full diagram
1702
+ ```
1703
+
1704
+ **Display behavior:**
1705
+ - **Diagram view:** Highlights matching elements with gold glow (others at 30% opacity)
1706
+ - **Tree panel:** Hides non-matching nodes (shows only matches)
1707
+ - **Combined effect:** Focus on matches while preserving diagram context
1708
+
1709
+ **Visual styles:**
1710
+ - **Diagram matches:** Gold glow + 110% brightness
1711
+ - **Diagram non-matches:** 30% opacity (visible but de-emphasized)
1712
+ - **Tree non-matches:** Hidden completely (CSS display: none)
1713
+
1714
+ ### Example: Minimal setup (recommended)
1715
+
1716
+ HTML: just an app container.
1717
+ ```html
1718
+ <!DOCTYPE html>
1719
+ <html>
1720
+ <head><meta charset="UTF-8"><title>ArchiMate</title></head>
1721
+ <body>
1722
+ <div id="app"></div>
1723
+ <script src="renderengine-workspace.umd.js"></script>
1724
+ <script>
1725
+ new window.RenderEngineWorkspace.ArchimateDiagramEditor({
1726
+ elements: [...],
1727
+ relationships: [...],
1728
+ views: [...],
1729
+ document,
1730
+ appContainerId: 'app',
1731
+ features: {
1732
+ version: true, viewpoint: true, distinctShapes: true,
1733
+ showDerived: true, exportCSV: true, importCSV: true,
1734
+ exportArchimate: true, importArchimate: true,
1735
+ },
1736
+ workspace: window.RenderEngineWorkspace,
1737
+ }).render();
1738
+ </script>
1739
+ </body>
1740
+ </html>
1741
+ ```
1742
+
1743
+ For a page of its own, `fullPage` (default `true`) fills the viewport; `false` fills the app container
1744
+ instead, as inside a workbench part. `model` hands in an `ArchimateModel` rather than building one from
1745
+ `elements`/`relationships`/`views`. See `examples/archimate-legacy.html` for the workbench host, and
1746
+ `test/charts.html` for integration with the workspace shell and dialog handlers.
1747
+
1748
+ ## Diagram templates and palette categories
1749
+
1750
+ `DiagramEditor` shapes are `Template` subclasses (`src/charting/templates/`). Geometry is
1751
+ centre-based — `(x, y)` is the centre of a `width × height` box — and every template accepts
1752
+ `{ x, y, width, height, rotation, zIndex, fillColor, fillOpacity, strokeColor, strokeWidth, textColor }`
1753
+ and a text style: `fontFamily`, `fontSize` (a bare number is points), `fontWeight: 'bold'`, `fontStyle: 'italic'`,
1754
+ `textDecoration: 'underline'`, `textAlign` ('left' | 'center' | 'right') and `textPosition` ('top' | 'middle' |
1755
+ 'bottom') (`TextStyle`; unset, the name is drawn at 12px at the shape's own label point). Unset sizes and
1756
+ colours come from the class's `static defaults`. A shape's properties dialog (double-click it) edits its name,
1757
+ place, colours and text style (Font, Text size, Bold, Italic, Underline, Alignment, Position) as one undo step;
1758
+ `serialize()` keeps the style. `TextStyle` is the one definition every editor uses: `fields(style)` and
1759
+ `fromValues(values)` for a FormDialog, `fromNode`/`toNode` for a graph-renderer node (whose position is
1760
+ `verticalAlign`), `svgAttributes(style)` and `block(style, box, lines)` for drawing (the graph renderer, the
1761
+ ArchiMate canvas shapes and the template shapes all place text with it).
1762
+
1763
+ ```js
1764
+ new RectangleTemplate('r1', 'Billing', { fontFamily: 'Georgia', fontSize: '14pt', fontWeight: 'bold', textAlign: 'left', textPosition: 'top' });
1765
+ ```
1766
+
1767
+ | Family | Classes |
1768
+ |---|---|
1769
+ | Boxes | `RectangleTemplate`, `RoundedRectangleTemplate`, `StadiumTemplate`, `PredefinedProcessTemplate`, `ClassTemplate`, `ComponentTemplate`, `SwimlaneTemplate`, `PackageTemplate`, `BoxTemplate` (3D) |
1770
+ | Round | `CircleTemplate`, `EllipseTemplate`, `FinalStateTemplate`, `CloudTemplate` |
1771
+ | Polygons | `DiamondTemplate`, `TriangleTemplate`, `PentagonTemplate`, `HexagonTemplate`, `OctagonTemplate`, `StarTemplate`, `ParallelogramTemplate`, `TrapezoidTemplate`, `ManualInputTemplate` |
1772
+ | Other | `CylinderTemplate`, `DocumentTemplate`, `NoteTemplate`, `DataStoreTemplate`, `ActorTemplate`, `LifelineTemplate` |
1773
+
1774
+ A template draws its outline in `shapeElements(doc)`, or in `points()` when it extends
1775
+ `PolygonTemplate`. The base class adds the group, rotation, label and editor wiring, and
1776
+ `renderPreview(size)` reuses the same outline for the palette tile.
1777
+
1778
+ **Edge endpoints.** `outline()` returns the visible outline as a polygon, with curves sampled
1779
+ (the default is the box). `boundaryPoint(dx, dy)` is where a ray from the centre in that
1780
+ on-screen direction meets the outline, rotation included. For concave outlines it uses the
1781
+ outermost crossing. A shape whose outline isn't its box overrides `outline()` to match what it
1782
+ draws. `EdgeGeometry.computeEdgePath` uses `info.boundaryPoint` whenever a node supplies one,
1783
+ for `'auto'` anchors and for pinned sides (`sourceSide`/`targetSide`). Nodes without one keep
1784
+ the `shape`-based model. `EdgeGeometry.endDirections(d)` returns the direction in which a path
1785
+ arrives at each end.
1786
+
1787
+ **Selecting and bending edges.** Selection is editor state (`selectedEdgeId` /
1788
+ `selectedTemplateId`). Selecting a shape deselects the edge, and clicking empty canvas clears both
1789
+ (`clearSelection()`). Only the selected edge shows handles: end dots, solid waypoints, and a
1790
+ hollow handle mid-segment. Click or drag a hollow handle, double-click the line, or right-click →
1791
+ *Add waypoint here* to add a bend. It is inserted into the nearest segment, so the route keeps
1792
+ its order. Drag a waypoint to move it. Double-click it or right-click → *Delete waypoint* to
1793
+ remove it, or use *Straighten* to remove all of them. Points snap to the grid while it is shown
1794
+ and are converted through pan/zoom. The programmatic forms are `addWaypoint(edgeId, {x, y})`,
1795
+ `removeWaypoint(edgeId, index)` and `clearWaypoints(edgeId)`. Each is one undo step.
1796
+
1797
+ **Reconnecting.** Drag either end dot of the selected edge onto another shape to move that
1798
+ end. Waypoints and style are kept, and that end's side pin resets to automatic. Dropping it on
1799
+ empty canvas, or on the shape at the other end, cancels. This is one undo step
1800
+ (`ReconnectEdgeCommand`).
1801
+
1802
+ **Line style levels.** A line's style resolves through `EdgeStyleCascade`: the line itself,
1803
+ then each level in `editor.edgeStyleLevels` (most specific first, `[{ key, label, style }]`),
1804
+ then the schema default. A value a level doesn't set is inherited. `EDGE_STYLE_PROPERTIES` is the
1805
+ single list of cascading properties:
1806
+
1807
+ | Key | Values (default first) |
1808
+ |---|---|
1809
+ | `routing` | `straight`, `orthogonal`, `curved`, `polyline` (bends around other shapes) |
1810
+ | `attach` | `floating` (outline, aimed at the other end), `sides` (middle of the facing side) |
1811
+ | `lineStyle` | `solid`, `dashed`, `dotted` (same dash patterns as `GraphDiagram`, via `LINE_STYLE_DASH`) |
1812
+ | `strokeColor`, `strokeWidth` | `#666666`, `2` |
1813
+ | `startMarker`, `endMarker` | `none`, or an `EdgeMarker` key |
1814
+
1815
+ `LINE_ONLY_PROPERTIES` lists `label`, `sourceSide` and `targetSide`; these never cascade. A
1816
+ pinned side beats `attach`. `editor.editLineStyle(levelKey)` opens the defaults dialog for one
1817
+ level, and the edge's *Properties* dialog edits the line. In both, every cascading field offers
1818
+ *Inherit*, labelled with the value and the level it would come from. Changes go through
1819
+ `SetStyleCommand` (one undo step; `undefined` = inherit). The editor never stores the levels
1820
+ itself, so the host decides what a level is and where it persists:
1821
+
1822
+ ```javascript
1823
+ new DiagramEditor({
1824
+ diagramContainerId: 'canvas',
1825
+ edgeStyleLevels: [
1826
+ { key: 'view', label: 'Diagram', style: view.edgeStyle }, // per diagram tab
1827
+ { key: 'model', label: 'Model', style: model.edgeStyle }, // shared by the model's diagrams
1828
+ { key: 'tool', label: 'Tool', style: toolDefaults }, // e.g. persisted in localStorage
1829
+ ],
1830
+ });
1831
+ ```
1832
+
1833
+ `MenuBar`'s Settings menu adds one *Line style: … defaults* entry per level.
1834
+
1835
+ **Edge markers.** `edge.startMarker` / `edge.endMarker` hold an `EdgeMarker` key:
1836
+ `arrow`, `openarrow`, `openarrowhalf`, `trianglehollow`, `diamond`, `diamondhollow`, `circle` or
1837
+ `crowsfoot` (`EDGE_MARKERS`). `getEdgeMarker(value)` ignores case and punctuation, so
1838
+ `"Crow's Foot"` resolves too, and so do ArchiMate's own arrowhead names (`diamond-hollow`,
1839
+ `open-arrow-half`, …). A marker's `shapes(tip, dir, size, color, background)` returns element
1840
+ descriptors whose tip is on the boundary and whose body extends back along the arriving
1841
+ segment, so nothing is hidden under the node; hollow markers are filled with `background`
1842
+ (default white) so the line does not show through them.
1843
+
1844
+ **Glyphs.** Every node shape the graph engine draws (`rect`, `circle`, `diamond`, `note`,
1845
+ `image`, `icon`, `person` and the ArchiMate notation shapes `pill`, `cube`, `chevron`, …) is a
1846
+ `Glyph` subclass in `src/charting/glyphs/`, one per file. `glyphFor(key)` returns it (an unknown key
1847
+ gives the circle) and `GLYPHS` maps every key. `glyph.parts(Glyph.context({R, size, color,
1848
+ cornerStyle, strokeColor, strokeWidth, icon, iconUrl}))` describes the shape centred on (0, 0) as
1849
+ `rect`/`circle`/`path`/`image`/`text`/`group` parts; `drawGlyph(renderer, parts)` draws them through
1850
+ a renderer. A glyph whose `ownsInterior` is true gets no centred icon or initials on top.
1851
+
1852
+ ```javascript
1853
+ const parts = glyphFor('cube').parts(Glyph.context({ R: 30, size: { w: 120, h: 55 }, color: '#AFFFAF', strokeColor: '#000', strokeWidth: 1.2 }));
1854
+ parts.children.length; // 3: front, top and side faces
1855
+ ```
1856
+
1857
+ `GlyphTemplate` is a template drawn by a glyph: a subclass names the glyph (`glyph()`) and its
1858
+ context, and the template draws its parts as SVG (`glyphToSvg`; `glyphElement(part)` gives the tag and
1859
+ attributes, by the SVG renderer's rules) and attaches lines to the glyph's `outline(ctx)`. Every glyph
1860
+ has an outline: its box by default, the real shape for round, skewed and pointed ones.
1861
+
1862
+ **ArchiMate in the template editor.** `ArchimateElementTemplate(instance, model, {nodeOptions})` and
1863
+ `ArchimateEdge(instance, view)` put an `ArchimateView` into a `DiagramEditor`. Neither keeps a copy:
1864
+ position, size, stacking and per-view paint are read from and written to the `ElementInstance`, the
1865
+ route, pinned sides and per-line style to the `RelationshipInstance`, so the editor's move, resize,
1866
+ waypoint, side and style commands edit the view and undo through `view.history`. A template looks as
1867
+ the graph engine draws the element (`ArchimateDiagram.buildNode`: layer colour, corner style, type
1868
+ glyph badge, or the full notation shape with `nodeOptions.useDistinctShapes`); notes and images
1869
+ (`ViewObjectInstance`) draw their own glyph. A line's dashes and end markers come from its
1870
+ relationship's kind and cannot be set on the line; routing, attach, colour and width can, and unset
1871
+ inherits. Its ends come from the instance (re-linking an end is a model edit, not a line edit).
1872
+
1873
+ ```javascript
1874
+ const view = model.currentView;
1875
+ const templates = new Map([...view.elementInstances.values()].map(i => [i.id, new ArchimateElementTemplate(i, model)]));
1876
+ const edges = new Map([...view.relationshipInstances.values()].map(i => [i.id, new ArchimateEdge(i, view)]));
1877
+ new DiagramEditor({ diagramContainerId: 'canvas', templates, edges, history: view.history }).render();
1878
+ ```
1879
+
1880
+ `ArchimateCanvasEditor({view, model, nodeOptions, chooseKind, ...DiagramEditor options})` is that
1881
+ editor as a class: it rebuilds its templates and lines from the view on every render (so an undone
1882
+ view edit shows at once), uses the view's history, and does through the view and model what stands
1883
+ for something else in ArchiMate, each as one undo step:
1884
+ - `addElement(type, point)`, and dropping an `ArchimatePalette` tile (`ARCHIMATE_TYPE_MIME`), creates a
1885
+ model element named after its type and shows it;
1886
+ - connecting two elements (`connect(sourceId, targetId)`) creates a relationship. Its kind is asked
1887
+ for through `chooseKind(kinds, from, to)` (a promise or value; null cancels) among
1888
+ `allowedKinds(sourceId, targetId)`, or without a chooser is the first of `KIND_PREFERENCE` that is
1889
+ allowed (the dependency kinds before the structural whole/part ones). Notes and images take none;
1890
+ - deleting an element takes it and its lines off the view; deleting a line takes the relationship
1891
+ off; the model keeps both.
1892
+
1893
+ `ArchimatePalette({version, onPick, document})` renders the element types grouped by layer
1894
+ (`ArchimatePalette.groups(version)`: the notation's layers, then ArchiMate 4's common behaviour types),
1895
+ each tile only its notation glyph in its layer colour, four to a row (`ArchimatePalette.TILE`; the name shows on hover); drag a tile onto the canvas or click it (`onPick`).
1896
+
1897
+ `ArchimateCanvasPart` (workspace bundle, type `archimate-canvas`; `examples/archimate.html`, and the testbed
1898
+ `test/archimate-canvas-testbed.html`) is the
1899
+ modeller on this canvas: toolbar (File with the view items,
1900
+ version, distinct shapes, zoom, connect, Settings), palette, one tab per view (Properties…, Duplicate,
1901
+ Delete) and the canvas; connecting opens a picker of the allowed kinds. The model tree is on the left, with a search box
1902
+ (the tree's text filter: it hides what it leaves out there and dims it on the canvas), and the palette on the right,
1903
+ as in Archi. File offers New view…, New canvas… and New canvas from template (`EditorPart#newItems()`), the open
1904
+ canvas saved as a template, and Import Archi template…. Dragging an element from the tree onto the canvas shows it there
1905
+ with its relationships to the elements already shown, one undo step (`editor.showElement(elementId, point)`,
1906
+ `ShowElementCommand`: only lines to or from the new box; redo adds the same lines). The File menu exports
1907
+ and imports CSV, `.archimate` and the Exchange Format; an import replaces the part's model
1908
+ (`part.replaceModel`) and the tree, canvas and menus follow it. Spec: `elements`, `relationships`,
1909
+ `views`, `name`, `archimateVersion`, `distinctShapes`, `viewpoint`, `viewpointMode`.
1910
+
1911
+ **File formats.** `ModelFileFormat` is one format a model is exported to and imported from: a subclass gives
1912
+ `label`, `accept`, `fileName`, `readsBytes` (import gets the file's bytes, not its text), `featureName`, and
1913
+ `export(model)` (`{ data, mimeType }`) and `import(data)` (a new model). `modelFileMenuItems(formats, { model,
1914
+ onImport, enabled })` makes the File menu's Export and Import items, one group per format: export downloads
1915
+ the file, import asks for one and hands the new model to `onImport` (a failure is reported and the model kept).
1916
+ `ARCHIMATE_FILE_FORMATS` are `ArchimateCsvFileFormat` (both tables in one file; import lays all the elements
1917
+ out in one view), `ArchiFileFormat` and `ExchangeFileFormat`; both ArchiMate editors read them.
1918
+
1919
+ ```js
1920
+ import { ARCHIMATE_FILE_FORMATS } from 'renderengine';
1921
+ const [csv] = ARCHIMATE_FILE_FORMATS;
1922
+ const { data } = csv.export(model); // '=== elements.csv ===\n...'
1923
+ const copy = csv.import(data); // a new ArchimateModel, with the view "Imported elements"
1924
+ ```
1925
+
1926
+ **Model tree layers.** The tree's Layers folder files an element under its layer as the diagram draws it
1927
+ (`treeLayerOf(element)`: its own `layer`, an IT type's, or its notation type's), in the order `TREE_LAYERS`;
1928
+ anything else (junction, grouping, location, an ArchiMate 4 common type) is under Other, as in Archi. A
1929
+ tree leaf dragged anywhere carries its element id as `MODEL_ELEMENT_MIME`.
1930
+
1931
+ An instance's `appearance` (`fillColor`, `fillOpacity`, `strokeColor`, `strokeWidth`, `textColor`, the text style
1932
+ keys `fontFamily`, `fontSize`, `fontWeight`, `fontStyle`, `textDecoration`; on a
1933
+ relationship instance the line style keys) is this occurrence's own paint; the graph engine draws it
1934
+ too. A template whose properties are computed reports what it holds itself through
1935
+ `ownProperty(key)`, which `SetPropertiesCommand` uses so undo restores "unset" rather than the
1936
+ computed value.
1937
+
1938
+ Each ArchiMate relationship type gives its notation as line style values:
1939
+ `ArchimateNotation.relationshipType('composition').edgeStyle()` is
1940
+ `{ lineStyle: 'solid', startMarker: 'diamond', endMarker: 'none' }`; arrowheads come back as marker keys
1941
+ (`realization` ends in `'trianglehollow'`).
1942
+
1943
+ `DIAGRAM_TYPES` (19 categories in the groups General, Process, UML, Data, Infrastructure,
1944
+ Business, Ideas) decides which templates the palette offers. `editor.initPaletteForDiagramType(container, key)`
1945
+ renders a grouped category picker above a grid of draggable tiles. `editor.setPaletteCategory(key)`
1946
+ switches category, and `onTemplateTypeChange(key)` (a constructor option) reports the switch.
1947
+ Unknown keys fall back to `'generic'` (Basic Shapes).
1948
+
1949
+ Adding a shape and a category (runs under Node — no DOM needed until render):
1950
+
1951
+ ```javascript
1952
+ import { PolygonTemplate, DIAGRAM_TYPES, getDiagramTypesByGroup } from './src/index.js';
1953
+
1954
+ class ChevronTemplate extends PolygonTemplate {
1955
+ static defaults = { width: 120, height: 50 };
1956
+ points() {
1957
+ const n = this.height / 2;
1958
+ return [[this.left, this.top], [this.right - n, this.top], [this.right, this.y],
1959
+ [this.right - n, this.bottom], [this.left, this.bottom], [this.left + n, this.y]];
1960
+ }
1961
+ }
1962
+
1963
+ DIAGRAM_TYPES['value-chain'] = {
1964
+ name: 'Value Chain', group: 'Business', description: 'Primary activities in sequence',
1965
+ templates: [{ id: 'activity', name: 'Activity', factory: (id, x, y) => new ChevronTemplate(id, 'Activity', { x, y }) }],
1966
+ };
1967
+
1968
+ console.log(DIAGRAM_TYPES['value-chain'].templates[0].factory('a1', 100, 50).points().length); // 6
1969
+ console.log(getDiagramTypesByGroup().find(g => g.group === 'Business').types.map(t => t.key));
1970
+ // [ 'organizational-chart', 'value-chain' ]
1971
+ ```
1972
+
1973
+ ### Diagram models and the editor part
1974
+
1975
+ `ViewSetModel` is the base of a model edited as several views: `views()`, `viewCount`, `getView`,
1976
+ `currentView`, and `createView`, `duplicateView`, `renameView`, `deleteView` (the last view can't be
1977
+ deleted; `canDeleteView` says so) and `switchTo`, each notifying `onChange` listeners. A subclass
1978
+ makes its own kind of view in `_newView(id, title)`; a view has an `id`, a `title` and
1979
+ `copy(id, title)`. `DiagramModel` and `ArchimateModel` both extend it.
1980
+
1981
+ `ViewMenu(model, {document, noun, editProperties})` turns those operations into menu items:
1982
+ `newViewItem()` (asks for a name) and `items(viewId)` (Rename…, Duplicate, and Delete after a
1983
+ confirmation when allowed). Given `editProperties(view)`, a properties dialog that includes the name,
1984
+ **Properties…** takes Rename's place. Tab strips, File menus and the ArchiMate model tree all use it,
1985
+ and both editors pass a properties dialog, so their tabs offer the same **Properties…**, **Duplicate**
1986
+ and **Delete**.
1987
+
1988
+ `ViewPropertiesDialog({model, view})` is the base of those dialogs: the view's name and documentation;
1989
+ a subclass adds `sections()` (more FormDialog fields) and `applySections(values)`.
1990
+ `SettingsViewPropertiesDialog` adds the view's level of the SettingsManager cascade (the ArchiMate
1991
+ editor's); `DiagramViewPropertiesDialog({cascade, levelKey, onStyleChange})` adds a diagram's
1992
+ line-style defaults, the `levelKey` level of an `EdgeStyleCascade`, applied as one undo step on the
1993
+ view's history (the template editor's). Both build their inheritable fields with
1994
+ `inheritableFields(properties, own, inherited)` (and read them back with `readInheritableFields`)
1995
+ from `SettingsCascade.js`, as `EdgeStyleDialog` does: each field shows its own value or offers
1996
+ "Inherit", labelled with the value and the level it would come from. `MODEL_SETTINGS_PROPERTIES`
1997
+ carries the labels, types and choices those forms use.
1998
+
1999
+ The SettingsManager cascade resolves each of those settings through a view's own settings, then its
2000
+ model's, then the tool's, then the default (`cascade(view, modelId)`; `getModelOverrides(modelId)` is
2001
+ what the model level itself sets, `getModelSettings(modelId)` the model's resolved values). Global
2002
+ Settings sets the tool's value and the model's (which may inherit the tool's); a view's Properties sets
2003
+ the diagram's (which may inherit the model's). "Invalid relationships" (`invalidRelationships`:
2004
+ `highlight`, `tooltip`, `off`) is set this way.
2005
+
2006
+ ```js
2007
+ settingsManager.saveToolSettings({ invalidRelationships: 'tooltip' }); // every model, unless it says otherwise
2008
+ settingsManager.saveViewSettings(view, { invalidRelationships: 'off' }); // this diagram only
2009
+ settingsManager.getEffectiveSettings(view, 'default').invalidRelationships; // → 'off'
2010
+ ```
2011
+
2012
+ `DiagramModel` holds a set of diagrams (`DiagramView`s) edited together. It adds the palette
2013
+ category they share (`templateType`) and the model-level line style (`edgeStyle`). Each `DiagramView` owns its `templates`
2014
+ and `edges` maps, a diagram-level `edgeStyle` and its own `CommandHistory`. `view.copy()` is
2015
+ independent: shapes and lines are copied, and the history starts empty.
2016
+
2017
+ Passing a view's maps and history to `DiagramEditor` binds the editor to that view: edits land
2018
+ in the view directly, and a rebuilt editor carries on with the same undo history:
2019
+
2020
+ ```javascript
2021
+ import { DiagramModel, DiagramEditor, RectangleTemplate } from './src/index.js';
2022
+
2023
+ const model = new DiagramModel({ templateType: 'flowchart' });
2024
+ const view = model.createView('Context');
2025
+ const editor = new DiagramEditor({
2026
+ enableKeyboardShortcuts: false, // no DOM needed for this example
2027
+ templates: view.templates, edges: view.edges, history: view.history,
2028
+ });
2029
+ editor.addTemplate(new RectangleTemplate('a', 'Box'));
2030
+ console.log(view.templates.has('a'), model.duplicateView(view.id).title); // true 'Context (copy)'
2031
+ ```
2032
+
2033
+ Call `editor.dispose()` before discarding an editor on a page that lives on. Its keyboard
2034
+ shortcuts and pan listeners are registered on `document`. Shortcuts ignore keys typed into
2035
+ form fields.
2036
+
2037
+ **Subclassing.** The undo shortcuts and every command use `editor.history`, an accessor a
2038
+ subclass can redirect. `ArchimateDiagramEditor` maps it to `currentView.history` (the model's one history), and hands the same history to the graph engine it draws with. Canvas edits
2039
+ (move, resize, bend points, pinned sides, drawing a relationship) are recorded there too, written
2040
+ back to the view through the hooks above, and undo across re-renders. A relationship drawn between
2041
+ two elements is created in the model and on the view, and its kind is asked for once; undo removes
2042
+ it from both. The line's right-click **Delete from View** takes one relationship instance off the
2043
+ view and leaves the model relationship.
2044
+
2045
+ **Views.** `DiagramEditorState` is the abstract base of every diagram view: its own `history` and
2046
+ the stacking operations below. A subclass says what stacks through `layerItems()`.
2047
+ `DiagramView` (the template editor's tabs) stacks its templates. `ArchimateView` stacks its element
2048
+ instances, and records adding and removing them as undo steps:
2049
+ `view.addToView(instance, { element })` shows an `ElementInstance` (creating the model element
2050
+ first when `element` is given, and widening `elementIds` to it), and `view.deleteElement(instanceId)`
2051
+ takes one off the view. Undo of either reverses all of it. `ArchimateModel.addElement(el)` and
2052
+ `removeElement(id)` are the model's side; position stays on the instance, so `addElement` does not
2053
+ keep an element's `x`/`y`.
2054
+
2055
+ Relationships work the same way. `view.missingRelationshipInstances(elementId)` lists the pairings
2056
+ the view could draw but doesn't (each instance of the element against each instance at the other
2057
+ end of its relationships), `view.addRelationshipsToView(instances)` adds them as one undo step, and
2058
+ `view.removeRelationship(instanceId)` takes one off. `ArchimateModel.addRelationship(rel)` and
2059
+ `removeRelationship(id)` are the model's side.
2060
+
2061
+ What a view draws per occurrence lives on the instance, so two occurrences of one element or
2062
+ relationship can differ:
2063
+
2064
+ | Instance | Field | Meaning |
2065
+ |---|---|---|
2066
+ | `ElementInstance` | `x`, `y`, `zIndex` | position and stacking |
2067
+ | `ElementInstance` | `size` | `{width, height}` or `{radius}` set on this view; `{}` = the element's own |
2068
+ | `RelationshipInstance` | `waypoints` | this line's bend points; `null` inherits the model relationship's, `[]` is straight |
2069
+ | `RelationshipInstance` | `sourceSide`, `targetSide` | pinned attachment side; `null` = automatic |
2070
+
2071
+ A note or image belongs to one view, not the model, so it is a `ViewObjectInstance` (a subclass of
2072
+ `ElementInstance`): it carries its own `content` (`{id, name, shape: 'note' | 'image', label, iconUrl,
2073
+ href, width, height}`), shows whatever the element filter says, and never appears in the model tree.
2074
+ A view also takes them as `viewObjects: [{content, x, y}]`, which is what an Archi import produces.
2075
+
2076
+ ```javascript
2077
+ view.addToView(new ViewObjectInstance({ id: 'n1', name: 'Remember', label: 'Remember', shape: 'note' }, 40, 300));
2078
+ model.getElement('n1'); // undefined: the note is the view's
2079
+ ```
2080
+
2081
+ A view's `edgeLayout` (`{relationshipId: {waypoints}}`, what an Archi import produces) is read once,
2082
+ at construction: it routes each instance of that relationship that has no route of its own.
2083
+
2084
+ ```javascript
2085
+ const model = new ArchimateModel({ elements: [{ id: 'a', name: 'A' }], views: [{ id: 'v', elementIds: ['a'] }] });
2086
+ const view = model.views()[0];
2087
+ view.addToView(new ElementInstance('note-1', 40, 60), { element: { id: 'note-1', name: 'Remember', shape: 'note' } });
2088
+ view.history.undo(); // the note leaves the view and the model; elementIds is ['a'] again
2089
+ ``` A subclass whose drawing engine owns zoom, pan and canvas clicks passes
2090
+ `enableCanvasInteractions: false`. One that draws into another SVG overrides `_canvasSvg()` so
2091
+ `_clientToDiagram` and `_snapToGrid` keep working.
2092
+
2093
+ **Stacking order.** `BringToFrontCommand`, `SendToBackCommand`, `RaiseCommand` and
2094
+ `LowerCommand` (all `ReorderCommand`s) work on any target with `layerItems()`: the editor's
2095
+ templates, or any view's `layerItems()` (`view.bringToFront(id)` and similar record one undo step
2096
+ on `view.history`). Raise and lower swap with the item actually drawn
2097
+ above or below, ties and gaps included. Each operation renumbers the stack to consecutive
2098
+ integers, and undo restores every previous value.
2099
+
2100
+ `EditorPart` (workspace bundle) is the base of the workbench parts that edit a `ViewSetModel`: a
2101
+ subclass builds its model from the part's spec in `createModel(spec)`, and gets `part.model`,
2102
+ `part.viewMenu` and `fileMenu()` (the view items, then `fileMenuItems()`: export and import in each of
2103
+ its `fileFormats()`, none by default, then the image export), and its editor disposed with the part. An
2104
+ import goes through `replaceModel(model)`, which a subclass follows in `modelReplaced(model)`.
2105
+ `DiagramEditorPart`, `ArchimateEditorPart` and `ArchimateCanvasPart` extend it.
2106
+
2107
+ `DiagramEditorPart` (workspace bundle) puts this together: `MenuBar` (File creates and deletes
2108
+ diagrams; Settings has the grid and line-style levels), the palette, a `ViewTabStrip` (right-click
2109
+ a tab for its **Properties…**: name, documentation and the diagram's line style; **Duplicate**;
2110
+ **Delete**) and the canvas. `part.edgeStyleLevels(view)` gives a diagram's line-style levels and
2111
+ `part.showViewProperties(view)` opens its properties. Spec options: `templateType`,
2112
+ `tabOrientation` (`'horizontal'`/`'vertical'`), `diagramTitle`, and `toolStyleKey`, the
2113
+ localStorage key for tool-level line style (`null` keeps it in memory only), saved through
2114
+ `LocalJsonStore`. Its levels are Diagram (`view.edgeStyle`), Model (`model.edgeStyle`) and Tool.
2115
+
2116
+ ```javascript
2117
+ const { DiagramEditorPart } = window.RenderEngineWorkspace;
2118
+ ws.registerPart(DiagramEditorPart);
2119
+ ws.addParts([{ id: 'editor', type: 'diagram-editor', title: 'Diagram Editor', templateType: 'flowchart' }]);
2120
+ ws.getPart('editor').model.currentView; // seed or inspect content before or after mounting
2121
+ ```
2122
+
2123
+ The workspace bundle includes its own copy of the engine, so a page can hold two copies of every
2124
+ class. Library code imports its collaborators directly rather than looking them up on
2125
+ `window.RenderEngine`, and `CommandHistory.execute` accepts any object with `execute()` and
2126
+ `undo()` rather than checking `instanceof Command`. A command built from either bundle therefore
2127
+ works in either bundle's history.
2128
+
2129
+ ### Toolbars and dialogs
2130
+
2131
+ `MenuBar` renders a row of toolbar items and has no controls of its own. Each editor contributes its
2132
+ items through `menuBarItems()`: the template editor gives zoom, connect mode and a ⚙ Settings menu (grid,
2133
+ grid size, line-style levels), and `ArchimateDiagramEditor` gives its File menu and a ⚙ Settings menu
2134
+ (notation and model settings). Each editor's ⚙ Settings menu reads `settingsMenuItems()`, which a
2135
+ subclass extends (ArchimateCanvasEditor puts version and distinct shapes in front of the grid and line
2136
+ styles). Every editor's shape menu orders with `orderMenuItem(id)`, the one Order submenu. A host can add more; `DiagramEditorPart` prepends its File menu (new, duplicate, rename, delete
2137
+ diagram). After an item acts, the bar re-renders. Items with live labels (the zoom %) also refresh on
2138
+ `editor.onRendered`.
2139
+
2140
+ | Item | Use |
2141
+ |---|---|
2142
+ | `ToolbarMenu(label, items)` | Button opening a `PopupMenu`; `items` may be a function, evaluated on open |
2143
+ | `ToolbarButton(label, action, { title, active })` | Action, or a toggle when `active()` is given |
2144
+ | `ToolbarSelect(label, choices, value, onChange)` | Labelled dropdown; `choices()` returns `[value, text, title?]` |
2145
+ | `ToolbarCheckbox(label, checked, onChange)` | Labelled switch |
2146
+ | `ToolbarText(text, style)` | Live text |
2147
+ | `ToolbarSeparator()` | Divider |
2148
+
2149
+ ```javascript
2150
+ new MenuBar({ container, editor, items: [
2151
+ new ToolbarMenu('File', [{ label: 'Export', action: exportIt }]),
2152
+ ...editor.menuBarItems(),
2153
+ ] }).render();
2154
+ ```
2155
+
2156
+ `FormDialog` is the one modal form: `fields` of type `text`, `textarea`, `number`, `color`, `select`
2157
+ or `checkbox`. A field with `inherit: { text, short? }` may be left unset; it then reads as `undefined` (a select
2158
+ offers `text` as its first choice; any other field, a checkbox too, gets a checkbox named `short`, default
2159
+ "Inherit", that greys it out). `FormDialog.many(fieldLists)` makes one form for several items from each one's
2160
+ fields: every field on "Keep" showing the value they share, read-only fields left out.
2161
+ `onApply(values)` receives every field; `onClose(applied)` is told once when the dialog closes, whether it
2162
+ was applied (false for Cancel, Escape or a click outside), so a caller waiting on a choice can settle.
2163
+ `EdgeStyleDialog`, the grid-size and diagram-name prompts are all `FormDialog`s. A dialog fits the window
2164
+ (at most 90% of its height): its fields scroll between the title and the buttons, which stay in view, and
2165
+ dragging the title moves it. A `textarea` scrolls inside its own box (at most 40% of the window) and can be
2166
+ dragged taller; a properties dialog shows `description`, `documentation`, `notes` and `tooltip` that way.
2167
+ A `color` field (`ColorField`) holds its value as text, `#rrggbb` or `none` for no colour (transparent, which a
2168
+ browser colour input cannot hold), beside the colour picker and a row of preset swatches: no colour, black,
2169
+ white, red, green, blue, yellow, orange, purple, grey and light grey (`ColorField.PRESETS`). A field's
2170
+ `presets` (`[{value, name}]`) replaces them, and `allowNone: false` leaves out "No colour".
2171
+
2172
+ ```js
2173
+ new FormDialog({ title: 'Box', fields: [
2174
+ { key: 'fill', label: 'Fill colour', type: 'color', value: '#ffffb5' }, // presets, none included
2175
+ { key: 'text', label: 'Text colour', type: 'color', value: '#000000', allowNone: false },
2176
+ { key: 'brand', label: 'Brand colour', type: 'color', presets: [{ value: '#0b5394', name: 'Brand blue' }] },
2177
+ ], onApply: (values) => console.log(values.fill) }).open(); // '#ffffb5', or 'none'
2178
+
2179
+ const fields = (t) => [{ key: 'fill', label: 'Fill colour', type: 'color', value: t.fill }];
2180
+ new FormDialog({ title: '2 boxes', fields: FormDialog.many([fields({ fill: '#ff0000' }), fields({ fill: '#ff0000' })]),
2181
+ onApply: (values) => console.log(values.fill) }).open(); // undefined while Keep is ticked
2182
+ ```
2183
+
2184
+ **Grouping and locking shapes.** A view's `arrangement` (`Arrangement`, on every `DiagramEditorState`, so a
2185
+ template diagram's `DiagramView` and an `ArchimateView` alike) says which items move as one (an
2186
+ `ItemGroup`: logical, it draws nothing but a dashed outline while a member is selected) and which are fixed in
2187
+ place (locked: not moved, resized, rotated, reordered, edited or deleted until unlocked; trying says so). A
2188
+ group can be locked as a whole; groups do not nest (grouping a member brings its group). Changes are one undo
2189
+ step each: `view.group(ids)`, `view.ungroup(id)`, `view.setLocked(ids, locked)`, or the commands
2190
+ `GroupCommand`, `UngroupCommand`, `LockCommand`. In the template editor (and ArchiMate's canvas editor, its
2191
+ subclass), Shift/Ctrl/⌘+click adds to the selection; dragging a selected shape moves the selection with its
2192
+ groups, as one step, and nothing moves if any of it is locked. In ArchiMate's graph editor, dragging a box
2193
+ moves its group with it, as one step. A shape's menu offers Group, Ungroup and Lock / Unlock
2194
+ (`arrangeMenuItems(arrangement, ids, act)`, the one definition every editor uses; Ungroup only once unlocked),
2195
+ as do Ctrl+G, Ctrl+Shift+G and Ctrl+L; a locked shape shows a lock and no resize or rotate handles.
2196
+
2197
+ **A background.** A kind of group is an `ItemGroup` subclass, kept by its JSON's `kind`. A `BackgroundGroup` is
2198
+ pinned behind everything else on its view: its members stack below every other item, so "Send to back" on
2199
+ anything else stops just in front of it, and its own members reorder only among themselves.
2200
+ `view.arrangeAsBackground(ids, { name })` (or the undoable `BackgroundCommand`) makes items one locked
2201
+ background group, behind the rest. A canvas made from a template gets its template's boxes this way, so the
2202
+ template stays at the back and holds still until it is unlocked (the shape's menu, **Unlock background**);
2203
+ unlocked, it moves as one; ungrouped, its boxes are ordinary boxes. Archi files keep it: each member carries
2204
+ the group (`renderengine.group`), its kind (`renderengine.groupKind`) and the lock.
2205
+
2206
+ ```js
2207
+ const groupId = view.group(['a', 'b']); // a and b move together
2208
+ view.setLocked(['a'], true); // the group is fixed in place
2209
+ view.arrangement.canMove(['b']); // → false
2210
+ view.history.undo(); // free again
2211
+ view.arrangeAsBackground(['t1', 't2']); // behind everything, locked, moved as one when unlocked
2212
+ ```
2213
+
2214
+ **Connecting and deleting in the template editor** are commands, one undo step each:
2215
+ `editor.connect(sourceId, targetId)` runs `connectCommand(sourceId, targetId)` (a new line), and
2216
+ `deleteTemplate(id)` / `deleteEdge(id)` run `deleteTemplateCommand(id)` (the shape and the lines joined
2217
+ to it) / `deleteEdgeCommand(id)`. A subclass whose shapes and lines stand for something else
2218
+ overrides those factories (ArchimateCanvasEditor does). A template class says whether the editor
2219
+ offers it a rotate handle (`static rotatable`, true by default; ArchiMate's templates are not). `pickFile(accept)` resolves with the chosen `File` (or `null`).
2220
+
2221
+ ## Where to go next
2222
+
2223
+ - **`ARCHITECTURE.md`** — module layout, the chart-type catalog mechanics, how to add a new chart type.
2224
+ - **`CHART_TYPES_ROADMAP.md`** — per-chart-type design rationale and what's deliberately deferred.
2225
+ - **`examples/csv-chart-builder.html`** — every chart type live, including the actual "Live updates"
2226
+ panel this doc's streaming section is drawn from; open it in a browser to see the pattern run.
2227
+ - **`test/charts.html`** — a plainer interactive demo of the core chart set.