@peter.prib/renderengine 0.5.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/REFERENCE.md +297 -17
- package/dist/renderengine-workspace.umd.js +5369 -2952
- package/dist/renderengine.umd.js +4466 -2320
- package/examples/archimate.html +8 -28
- package/examples/data-model.html +45 -556
- package/package.json +70 -70
- package/src/archimateArchiFormat.js +11 -3
- package/src/archimateExchangeFormat.js +17 -9
- package/src/chartCatalog.js +10 -0
- package/src/charting/ArchimateDiagramEditor.js +1 -1
- package/src/charting/Axis.js +20 -0
- package/src/charting/ChartChooser.js +229 -0
- package/src/charting/DiagramEditor.js +35 -3
- package/src/charting/Dialog.js +195 -0
- package/src/charting/DuplicatesDialog.js +108 -0
- package/src/charting/FormDialog.js +28 -97
- package/src/charting/InteractionManager.js +3 -0
- package/src/charting/Legend.js +45 -2
- package/src/charting/MergeCompareDialog.js +81 -0
- package/src/charting/PopupMenu.js +12 -3
- package/src/charting/SettingsDialog.js +6 -2
- package/src/charting/SettingsManager.js +14 -1
- package/src/charting/archimate/ArchimateCanvasEditor.js +190 -15
- package/src/charting/archimate/ArchimatePalette.js +1 -1
- package/src/charting/charts/AreaChart.js +5 -2
- package/src/charting/charts/BarChart.js +5 -3
- package/src/charting/charts/BarChart3DOrbit.js +5 -7
- package/src/charting/charts/BubbleChart.js +5 -7
- package/src/charting/charts/BubbleChart3D.js +5 -7
- package/src/charting/charts/CandlestickChart.js +3 -1
- package/src/charting/charts/DensityChart.js +4 -6
- package/src/charting/charts/GroupedBarChart.js +2 -2
- package/src/charting/charts/LineChart.js +5 -2
- package/src/charting/charts/ParallelCoordinatesChart.js +2 -2
- package/src/charting/charts/PipeChart.js +4 -6
- package/src/charting/charts/RadarChart.js +5 -6
- package/src/charting/charts/ScatterChart.js +5 -7
- package/src/charting/charts/StackedBarChart.js +4 -2
- package/src/charting/charts/StackedBarChart3DOrbit.js +5 -7
- package/src/charting/charts/StreamGraphChart.js +5 -2
- package/src/charting/charts/TableChart.js +98 -0
- package/src/charting/charts/WaterfallChart.js +2 -1
- package/src/charting/content/ContentSource.js +129 -0
- package/src/charting/content/ContentSources.js +77 -0
- package/src/charting/datamodel/DataModel.js +116 -0
- package/src/charting/diagrams/ArchimateModel.js +38 -3
- package/src/charting/diagrams/ArchimateModelChecker.js +13 -4
- package/src/charting/diagrams/MergeElementsCommand.js +124 -101
- package/src/charting/diagrams/SvgZoom.js +20 -1
- package/src/charting/diagrams/duplicates.js +95 -13
- package/src/charting/duplicateSearchForm.js +58 -0
- package/src/charting/modelTree/ModelTree.js +239 -202
- package/src/charting/modelTree/ModelTreePanel.js +955 -919
- package/src/charting/modelTree/TreeBuilder.js +93 -45
- package/src/charting/modelTree/TreeViewMode.js +9 -6
- package/src/charting/modelTree/viewFolders.js +15 -6
- package/src/icons/IconSet.js +86 -0
- package/src/icons/index.js +59 -0
- package/src/index.js +86 -10
- package/src/workspace/dom.js +56 -42
- package/src/workspace/index.js +1 -1
- package/src/workspace/nav/NavEntry.js +42 -0
- package/src/workspace/nav/NavGroup.js +43 -0
- package/src/workspace/nav/NavItem.js +53 -0
- package/src/workspace/nav/NavMenu.js +143 -0
- package/src/workspace/nav/NavSection.js +17 -0
- package/src/workspace/nav/index.js +5 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +8 -3
- package/src/workspace/parts/DataModelPart.js +91 -0
- package/src/workspace/parts/EditorPart.js +8 -0
- package/src/workspace/parts/ModelHosted.js +1 -0
- package/src/workspace/parts/ModelsPart.js +765 -622
- package/src/workspace/styles.js +28 -1
package/README.md
CHANGED
|
@@ -29,6 +29,10 @@ open the pages directly:
|
|
|
29
29
|
- `examples/diagram-editor.html`: the diagram editor
|
|
30
30
|
- `examples/data-model.html`: the data modeller
|
|
31
31
|
|
|
32
|
+
The models they open come from [modelengine](https://gitlab.com/peter.prib/modelengine), kept apart from the tools: with
|
|
33
|
+
it checked out beside renderengine, the pages open on its models (File ▸ Open from ▸ modelengine lists them by domain);
|
|
34
|
+
without it they start empty.
|
|
35
|
+
|
|
32
36
|
All work in current Chrome, Edge, Firefox and Safari. Save writes back to the file you opened in Chrome and Edge;
|
|
33
37
|
other browsers download the file instead.
|
|
34
38
|
|
package/REFERENCE.md
CHANGED
|
@@ -88,6 +88,7 @@ catalog `key` for you.
|
|
|
88
88
|
| `treemap` | `treemapChart(data, opts)` | `Array<{label, value?, children?}>` (hierarchical) | squarified layout, palette by top-level branch |
|
|
89
89
|
| `sunburst` | `sunburstChart(data, opts)` | same hierarchical shape as `treemap` | nested rings by depth |
|
|
90
90
|
| `outline` | `outlineChart(data, opts)` | same hierarchical shape as `treemap`/`sunburst` | `onNodeClick`; collapsible indented list |
|
|
91
|
+
| `table` | `tableChart(data, opts)` | `Array<object>` — a row per object, a column per key | `columns` (names or `{key, label?, align?}`), `maxRows`, `nullText` (`'—'`); values exact, numeric columns right-aligned; drawn with the renderer's primitives, so SVG or canvas |
|
|
91
92
|
| `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
93
|
| `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
94
|
| `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` |
|
|
@@ -812,6 +813,22 @@ types — see `ARCHITECTURE.md`):
|
|
|
812
813
|
is not there. Model ▸ **Check model…** lists the findings in the `ModelCheckDialog` the diagram editors' check uses;
|
|
813
814
|
choosing one shows it (the entity's level, the service domain seen through, the mapping view).
|
|
814
815
|
|
|
816
|
+
**Likely duplicate entities.** **Find likely duplicates…** (Model ▸, Tools ▸ and the model's tab; the part's
|
|
817
|
+
`modelMenuItems()`) asks for a level (the one in front first) and how to compare, with the form the ArchiMate
|
|
818
|
+
modeller uses (`duplicateSearchFields`): names, definitions or attributes (their names together), exactly or by their
|
|
819
|
+
words, and the noise to leave out. The groups found are listed as the ArchiMate modeller lists them
|
|
820
|
+
(`DuplicatesDialog`, below), and merging shows the entities side by side first (`model.entityComparison(level, ids)`:
|
|
821
|
+
name, definition, each attribute, relationships, what each is mapped to; `MergeCompareDialog`): choose the entity
|
|
822
|
+
kept and, when their definitions differ, which (or all joined); entities alike merge without asking. The kept one takes the
|
|
823
|
+
attributes it lacks (by name, any case), their relationships (one left joining it to itself, or doubling one there,
|
|
824
|
+
goes) and their mapping pairs; the others go, perspectives and contexts forgetting them, one undo step
|
|
825
|
+
(`model.duplicateEntities(level, options)`, `model.mergeEntitiesCommand(level, keepId, otherIds, { description })`).
|
|
826
|
+
|
|
827
|
+
```js
|
|
828
|
+
model.duplicateEntities('logical', { by: 'attributes' }); // [{ code: 'possible-duplicate', level, ids, message }]
|
|
829
|
+
part.runModelEdit(model.mergeEntitiesCommand('logical', 'customer', ['client']));
|
|
830
|
+
```
|
|
831
|
+
|
|
815
832
|
**Checked as DMBOK2 reviews a model.** The check also finds, at the logical and physical levels: an entity with
|
|
816
833
|
attributes and no primary key (`no-primary-key`); a foreign key whose `references` (an entity, or `entity.attribute`,
|
|
817
834
|
by id or name) is not there (`reference-missing`) or has no relationship with its entity either way
|
|
@@ -949,8 +966,9 @@ types — see `ARCHITECTURE.md`):
|
|
|
949
966
|
service domains.
|
|
950
967
|
`model.serviceDomainsCommand(specs, source)` brings them in as one undo step: a new one is added; one already
|
|
951
968
|
there (by id, else by name) keeps its control record, qualifiers and the rest, taking its area and domain from
|
|
952
|
-
the landscape, and its asset and pattern where it has none.
|
|
953
|
-
|
|
969
|
+
the landscape, and its asset and pattern where it has none. modelengine's banking domain has a small excerpt in
|
|
970
|
+
that shape to try it with (`test/fixtures/bian-landscape-sample.xml` here; not BIAN's landscape, which is BIAN's to
|
|
971
|
+
publish).
|
|
954
972
|
|
|
955
973
|
```javascript
|
|
956
974
|
const specs = new BianLandscapeReader().read(BianLandscapeReader.parse(xml));
|
|
@@ -1045,7 +1063,8 @@ types — see `ARCHITECTURE.md`):
|
|
|
1045
1063
|
and Redo; Tools ▸ Compare with and Merge from (the other open models and the bases; a merge is one undo step); Model ▸ New / Edit / Delete perspective and context (those dialogs), Perspective and Context selects (when
|
|
1046
1064
|
it has some; a perspective brings its context, and the context's scope shows beside them), Level (Conceptual,
|
|
1047
1065
|
Logical, Physical, and Mapping), Notation, and Export ▸ PNG / SVG. A box or note dragged stays on the level it
|
|
1048
|
-
was dragged on (the mapping view's in its own layout).
|
|
1066
|
+
was dragged on (the mapping view's in its own layout). modelengine's enterprise ontology, which
|
|
1067
|
+
`examples/data-model.html` (`npm run datamodel`) opens when modelengine is checked out beside renderengine, uses every
|
|
1049
1068
|
feature above. A data model can also be registered like a chart:
|
|
1050
1069
|
`engine.register(id, 'dataModel', { entities, relationships, level })` then `engine.refreshChart(id)` (each
|
|
1051
1070
|
diagram catalog entry names its data options, `args`).
|
|
@@ -1062,6 +1081,50 @@ this to `line`/`area`/`stackedBar`/etc. is the same three-line change: call
|
|
|
1062
1081
|
`this._drawYAxisTicks(x, y, width, height, rawMax)` before drawing marks, and scale against its
|
|
1063
1082
|
return value instead of the raw max — not done yet for the rest, tracked as a follow-up.
|
|
1064
1083
|
|
|
1084
|
+
### Choosing a chart for result rows — `ChartChooser`
|
|
1085
|
+
|
|
1086
|
+
`new ChartChooser().choose(rows, columns?, { complete? })` says which chart types fit a set of result rows (a query
|
|
1087
|
+
answer, a CSV), best first, each with the rows already shaped as that type's `dataShape` — so
|
|
1088
|
+
drawing the best one is `engine.renderChartType(best.key, best.data)`. A `table` always fits and is
|
|
1089
|
+
always offered, last.
|
|
1090
|
+
|
|
1091
|
+
```javascript
|
|
1092
|
+
import { RenderEngine, ChartChooser } from '@peter.prib/renderengine';
|
|
1093
|
+
|
|
1094
|
+
const rows = [{ Region: 'West', Revenue: 200 }, { Region: 'East', Revenue: 405 }];
|
|
1095
|
+
const choices = new ChartChooser().choose(rows, [{ name: 'Region' }, { name: 'Revenue', measure: 'additive' }]);
|
|
1096
|
+
// → bar, pie, donut, table — each { key, label, data, reason }
|
|
1097
|
+
const [best] = choices;
|
|
1098
|
+
new RenderEngine(container).renderChartType(best.key, best.data, { title: best.reason });
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
`columns` are optional hints, `{ name, kind?, measure? }`: `kind` is `'categorical'`, `'numeric'`
|
|
1102
|
+
or `'time'` (without a hint, a column whose values are all numbers is numeric, else categorical);
|
|
1103
|
+
`measure` is the data model's measure kind (`'additive'`, `'semi-additive'`, `'non-additive'`).
|
|
1104
|
+
`complete: false` says the rows are only part of the answer (a top N, a first page).
|
|
1105
|
+
|
|
1106
|
+
| Rows look like | Offered, best first |
|
|
1107
|
+
|---|---|
|
|
1108
|
+
| one number, one row | `kpi-tile` |
|
|
1109
|
+
| one category, one number | `bar`; also `pie`, `donut` when the measure is additive, the rows are complete, no value is negative and there are 2–8 categories |
|
|
1110
|
+
| one time column, one number | `line`, `area`, `bar` — in time order |
|
|
1111
|
+
| one category, several numbers | `groupedBar`, `line` (a series per number); with time, `line` first |
|
|
1112
|
+
| two categories, one number | `groupedBar`, `heatmap`; with a time column, `line` (a line per category) first |
|
|
1113
|
+
| anything else | `table` only |
|
|
1114
|
+
|
|
1115
|
+
Three rules keep a choice from misleading: only an **additive** measure is offered as shares of a
|
|
1116
|
+
whole (a slice of an average or a balance means nothing); only **complete** rows are (three
|
|
1117
|
+
customers' slices of a top 3 would claim to be the whole); and a **missing value is never drawn as
|
|
1118
|
+
0** — a result with a hole in a series is not offered as series charts (the heatmap, which draws
|
|
1119
|
+
only the cells present, still is), and a bar chart simply has no bar for a null.
|
|
1120
|
+
|
|
1121
|
+
A table has no axes, so it takes none of a chart's axis margins. `TableChart.heightFor(rowCount,
|
|
1122
|
+
{ title?, maxRows? })` is the height it will draw, for sizing its container first.
|
|
1123
|
+
|
|
1124
|
+
Every suggestion is checked against the registry the chooser is given (`chartRegistry` by default):
|
|
1125
|
+
a type is offered only if the catalog has it and every axis it marks `required` is one the result's
|
|
1126
|
+
shape provides, so the catalog stays the single source of truth for what each chart needs.
|
|
1127
|
+
|
|
1065
1128
|
## Common options
|
|
1066
1129
|
|
|
1067
1130
|
Not every method takes every option below — the table above calls out what's notable per type —
|
|
@@ -1077,6 +1140,13 @@ but these recur across most 2D chart types:
|
|
|
1077
1140
|
}
|
|
1078
1141
|
```
|
|
1079
1142
|
|
|
1143
|
+
- **Legend placement** (`Legend.beside`, every chart with a legend): to the right of the plot when there is
|
|
1144
|
+
room in the drawing; otherwise, since the plot usually fills it, inside the plot's top-right corner on a
|
|
1145
|
+
light backing. A chart of points (scatter, bubble) marks its entries with circles, the others with squares.
|
|
1146
|
+
- **Category labels** (line, area, bar, stacked bar, candlestick, waterfall, streamgraph): as many as fit.
|
|
1147
|
+
When they would overprint, every n-th is drawn, n from the longest label and the plot width
|
|
1148
|
+
(`Axis.categoryLabelStep(labels, width, { fontSize })`).
|
|
1149
|
+
|
|
1080
1150
|
Data-resolution options (used with `register()`/`refreshChart()`, see below):
|
|
1081
1151
|
|
|
1082
1152
|
```javascript
|
|
@@ -1112,6 +1182,8 @@ engine.setGlobalData(data) // set a global object referenceable by
|
|
|
1112
1182
|
engine.resolvePath(path, mapper?) // e.g. resolvePath('metrics.sales.0.value')
|
|
1113
1183
|
engine.register(id, type, options) // remember a {type, options} pair for a container id
|
|
1114
1184
|
engine.refreshChart(id) // re-invoke it — resolves options.data or dataPath+dataMapper fresh each time
|
|
1185
|
+
engine.refreshAll() // every registered chart
|
|
1186
|
+
engine.setData(data) // setGlobalData(data), then refreshAll(): a live page's poll
|
|
1115
1187
|
engine.niceTicks(maxVal, count=4) // readable axis tick values
|
|
1116
1188
|
engine.formatValue(v) // 1234567 -> "1.23M"
|
|
1117
1189
|
engine.bindTooltip(element, text)
|
|
@@ -1120,7 +1192,24 @@ engine.adjustColor(hex, amount) // lighten/darken, used internally for 3
|
|
|
1120
1192
|
```
|
|
1121
1193
|
|
|
1122
1194
|
`register`/`refreshChart` is the registration mechanism `addDataPoint`/`addGraphNode` (next section)
|
|
1123
|
-
build on.
|
|
1195
|
+
build on. **One engine keeps a page of charts:** each registered id is drawn in the element with that id, by
|
|
1196
|
+
the engine itself when it is its own container, otherwise by an engine of its own for that element, made with
|
|
1197
|
+
this engine's options and kept while the element is the same, so a zoom survives the next refresh. A chart
|
|
1198
|
+
whose `dataPath` resolves to nothing yet is not drawn.
|
|
1199
|
+
|
|
1200
|
+
```javascript
|
|
1201
|
+
const charts = new RenderEngine(null, { rendererType: 'svg', zoom: { wheelModifier: 'ctrl' } });
|
|
1202
|
+
charts.register('cpu-gauge', 'gauge-dial', { dataPath: 'cpuPercent' });
|
|
1203
|
+
charts.register('perf-chart', 'line', {
|
|
1204
|
+
dataPath: 'history',
|
|
1205
|
+
dataMapper: (h) => [{ name: 'CPU', values: h.map(x => ({ label: x.t, value: x.cpu })) }],
|
|
1206
|
+
});
|
|
1207
|
+
setInterval(async () => charts.setData(await (await fetch('/api/metrics')).json()), 2000);
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
**Zoom.** An SVG chart zooms with the mouse wheel. The engine option `zoom: { wheelModifier: 'ctrl' }`
|
|
1211
|
+
(or `'alt'`, `'shift'`; `ctrl` also takes ⌘) zooms only with that key held, so a plain wheel scrolls a page of
|
|
1212
|
+
charts; `zoom: false` turns it off.
|
|
1124
1213
|
|
|
1125
1214
|
## Interactivity
|
|
1126
1215
|
|
|
@@ -1359,6 +1448,24 @@ do, and a folder of one type in the Layers tree too (`typeGlyph(typeName, { size
|
|
|
1359
1448
|
`archimate/typeGlyph.js`, the one drawing both use; a 3.2 type under ArchiMate 4 is drawn as 3.2 draws it).
|
|
1360
1449
|
`TreeBuilder`'s `iconFor(node)` gives a node a picture in place of its `icon` text.
|
|
1361
1450
|
|
|
1451
|
+
**Tree rows and long folders.** A tree row is one compact line: a label too long for the panel ends in an ellipsis,
|
|
1452
|
+
with the whole of it as the row's tooltip. A folder shows its first `leafLimit` leaves (8 in the model tree,
|
|
1453
|
+
`ModelTree.LEAF_LIMIT`, set per tree by `new ModelTree(model, { leafLimit })` or `setOptions({ leafLimit })`), then
|
|
1454
|
+
a "▸ N more" row that shows the rest; the folders inside it always show. A folder asked for more keeps showing all
|
|
1455
|
+
when the tree is redrawn (`TreeBuilder`'s `shownAll`, a Set of node ids the caller keeps, as it keeps `openState`).
|
|
1456
|
+
`TreeBuilder`'s `filter(node)` says which leaves to show: a folder shows when a leaf under it does, and opens while
|
|
1457
|
+
filtering (without changing what `openState` remembers); one with nothing kept inside is left out, whatever its own
|
|
1458
|
+
name.
|
|
1459
|
+
|
|
1460
|
+
```js
|
|
1461
|
+
const tree = new TreeBuilder(document, {
|
|
1462
|
+
container, rootNodes, getNodeChildren: (node) => node.kids ?? [],
|
|
1463
|
+
leafLimit: 8, shownAll: new Set(),
|
|
1464
|
+
filter: (node) => node.label.toLowerCase().includes('order'), // null: every node
|
|
1465
|
+
});
|
|
1466
|
+
tree.render();
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1362
1469
|
**Nesting.** In the ArchiMate canvas editor, dropping a box wholly inside an element's box nests it there (Archi's
|
|
1363
1470
|
nested elements); so does dropping an element with its centre on another element's box when a relationship a
|
|
1364
1471
|
nesting stands for is allowed between them (`nestsOnDrop`), and the parent then grows to show it whole, with its
|
|
@@ -1441,9 +1548,15 @@ there (`editor.addImage(dataURL, point)`, one undo step, selected; the base edit
|
|
|
1441
1548
|
relationships) under the palette, for an element chosen on the canvas or in the tree. The tree's own filters (its
|
|
1442
1549
|
"Filters…" dialog: text, types, specializations, properties) dim what they leave out on the canvas too:
|
|
1443
1550
|
`modelTree.matchingElementIds()` (null with no filter on) and `modelTree.onFilterChange(fn)` feed
|
|
1444
|
-
`editor.setElementFilter`.
|
|
1551
|
+
`editor.setElementFilter`. The search text filters from 3 characters on (`ModelTree.MIN_FILTER_CHARS`; per tree,
|
|
1552
|
+
`minFilterChars` or `setOptions({ minFilterChars })`); shorter, nothing is filtered (`modelTree.filterText` is the
|
|
1553
|
+
text in effect, `getFilterText()` the text typed). While it filters, the box is marked (`data-filtering`, a blue
|
|
1554
|
+
border) and a line under it says what was found ("Filtering: 3 elements, 1 view found"); with fewer characters it
|
|
1555
|
+
says how many more to type. It finds views, templates and folders by name as well as
|
|
1556
|
+
elements, and leaves out the folders with nothing found in them (`modelTree.textMatches(name, documentation)`; type,
|
|
1557
|
+
specialization and property filters are the elements' only). Under the search box, **In:** chooses what the search covers, the whole model or only
|
|
1445
1558
|
the diagram in front (`modelTree.setSearchScope('model' | 'view')`, `ModelTree.SEARCH_SCOPES`), and
|
|
1446
|
-
**Descriptions** looks for the text in elements' and
|
|
1559
|
+
**Descriptions** looks for the text in elements', relationships' and views' documentation too
|
|
1447
1560
|
(`modelTree.setSearchInDescriptions(true)`). The Filters… dialog opens showing the filters on now
|
|
1448
1561
|
(`getTypeFilter()`, `getSpecializationFilter()`, `getPropertyFilter()`). Its properties start unticked (no property
|
|
1449
1562
|
filter; `OptInCheckboxTreeSelector`: a value is ticked to keep what has it), and a property with more than 10
|
|
@@ -1470,7 +1583,11 @@ leaves it alone). Any `Edge` can say so (`editable`, default true) and add to it
|
|
|
1470
1583
|
|
|
1471
1584
|
**Selecting several shapes.** In the base editor (so the template editor and the ArchiMate canvas alike): drag a
|
|
1472
1585
|
box on empty canvas to select the shapes wholly inside it (Ctrl or Cmd adds them to the selection;
|
|
1473
|
-
`editor.templatesInside(rect)`, `editor.selectTemplates(ids, { extend })`); Shift- or Ctrl-click adds one
|
|
1586
|
+
`editor.templatesInside(rect)`, `editor.selectTemplates(ids, { extend })`); Shift- or Ctrl-click adds one, and so
|
|
1587
|
+
does a right-click with Shift or Ctrl, before the menu opens for the whole selection; a plain right-click on a shape
|
|
1588
|
+
outside the selection selects it alone (`editor.selectForMenu(id, { extend })`). The last shape chosen has the
|
|
1589
|
+
handles; every other selected shape is marked by a dashed, tinted outline set off its border
|
|
1590
|
+
(`DiagramEditor.SELECTION_OUTLINE`: `pad`, `stroke`, `strokeWidth`, `dash`, `fill`);
|
|
1474
1591
|
**Ctrl+A** selects every shape that is drawn (`selectableIds()`). Whatever is selected acts together, each as one
|
|
1475
1592
|
undo step: dragging any of them moves them all (with their groups and what they hold); the arrow keys nudge
|
|
1476
1593
|
them a grid step, one pixel without a grid, ten times as far with Shift (`nudgeSelection(dx, dy)`); **Delete**
|
|
@@ -1634,6 +1751,28 @@ whose schema changed) gives them afresh with `models.setBases(specs)`; what is o
|
|
|
1634
1751
|
(`models.viewBase(i)`) opens a base to look at: a tab of its own copy, named "… (view)", that can be laid out and
|
|
1635
1752
|
moved about but is never kept (`part.keepOff`).
|
|
1636
1753
|
|
|
1754
|
+
**Content: models kept apart from the tools.** Models, data models and user stories by domain live apart from the
|
|
1755
|
+
modellers (modelengine, or any collection laid out like it): a `manifest.json` listing items by domain, each a file in
|
|
1756
|
+
a format a modeller already reads. A `ContentSource` reads one, fetched from its address (`url`: a folder holding
|
|
1757
|
+
`manifest.json`, or the manifest itself) or given whole (`bundle`: `{ manifest, files }`, what modelengine's
|
|
1758
|
+
`dist/modelengine.js` puts on `globalThis.ModelEngineContent` for a page opened off disk; `ContentSource.fromPage()`).
|
|
1759
|
+
Nothing in the tools lists what a collection has: `source.items({ formats })` offers the items a modeller's file
|
|
1760
|
+
formats read, each matched by its file's whole ending (`ContentSource.formatFor(item, formats)`: the one format
|
|
1761
|
+
accepting all after the name's first dot, so `.requirements.json` is not any `.json`), and `source.open(item,
|
|
1762
|
+
formats)` opens one through that format. A `ModelsPart` offers its sources (`ContentSources`: the page's bundle, its
|
|
1763
|
+
spec's `content: [{ label?, url }]`, the host's `contentSources` service, and those added with **Add a source…**,
|
|
1764
|
+
kept in this browser) under File ▸ **Open from** ▸ *source* ▸ *domain* ▸ *item* (`models.contentItems()`); a source
|
|
1765
|
+
not read says why there. An item opens in a tab of its own (`models.openContent(source, item)`) remembering where it
|
|
1766
|
+
came from (`part.contentOrigin`), so its tab menu and Tools offer **Update from *source*…**: the item read again and
|
|
1767
|
+
its changes reviewed as a merge, one undo step, keeping what was changed here (`models.updateFromContent(part)`).
|
|
1768
|
+
`openContent: 'first'` (or an item's id) in the spec opens that item on a first visit, in the empty model's place.
|
|
1769
|
+
|
|
1770
|
+
```js
|
|
1771
|
+
const source = new ContentSource({ url: 'https://example.org/modelengine/' });
|
|
1772
|
+
const [item] = await source.items({ formats: [new DataModelFileFormat()] }); // what the data modeller reads
|
|
1773
|
+
const { model } = await source.open(item, [new DataModelFileFormat()]);
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1637
1776
|
**Kept in this browser, and backed up.** With `keep: true` in its spec, a `ModelsPart` keeps every model it holds
|
|
1638
1777
|
in this browser, so work outlives the page without saving a file: a moment after each edit (`ModelsPart.KEEP_DELAY`)
|
|
1639
1778
|
and at once when imported, each in its editor's own file format (`part.keepEntry()`: the file's format, else the
|
|
@@ -1840,20 +1979,56 @@ template editor (the testbed) and both ArchiMate editors have them. The **View**
|
|
|
1840
1979
|
(`editor.viewMenuItems()`) has **Zoom to fit** (`editor.zoomToFit()`: everything drawn in sight, in the
|
|
1841
1980
|
drawing's own units; `ViewFit` computes it, as a viewBox for the graph engine or a scale and pan for the
|
|
1842
1981
|
template editor), **Reset zoom**, **Highlight…** (below), and, when the editor has a `model`, **Check model…**
|
|
1843
|
-
(`editor.showModelCheck()`). The checks are the model's own (`model.checker(options)`): a `ViewSetChecker` for
|
|
1982
|
+
(`editor.showModelCheck()`; `showFindings(findings, { title, empty })` shows any findings in its dialog). The checks are the model's own (`model.checker(options)`): a `ViewSetChecker` for
|
|
1844
1983
|
any view set (empty views; an empty template is not one), and for an ArchiMate model an `ArchimateModelChecker`
|
|
1845
1984
|
that adds relationships the specification does not allow, likely duplicates (elements of one type whose names
|
|
1846
|
-
have the same words, ignoring case, punctuation, word order
|
|
1985
|
+
have the same words, ignoring case, punctuation, word order, plurals ("Customers" is "Customer", `singular(word)`)
|
|
1986
|
+
and filler words such as "of" and "the":
|
|
1847
1987
|
`nameKey(name)` and `duplicateGroups(elements)` in `diagrams/duplicates.js`), elements and relationships on no
|
|
1848
1988
|
view, nested elements with no relationship, and names or documentation with unprintable characters (control
|
|
1849
1989
|
characters, zero-width spaces; tabs and line breaks are fine in documentation). Each check is a method listed by
|
|
1850
1990
|
`checks()`, so a subclass changes the set; choosing a finding shows it (`editor.revealFinding(finding)`: its view,
|
|
1851
1991
|
its shape, or in the ArchiMate tree), and a finding can offer actions beside it (`editor.findingActions(finding)`).
|
|
1852
|
-
On the ArchiMate canvas, likely duplicates have **Merge
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1992
|
+
On the ArchiMate canvas, likely duplicates have **Merge…**, and **Find likely duplicates…**, a tool for the whole
|
|
1993
|
+
model, is on the model's tab menu and in **Tools** (a part's `modelMenuItems()`, the editor's for an `EditorPart`;
|
|
1994
|
+
in its **Edit** menu when no `ModelsPart` hosts it). It asks where to look (the whole model, this view, the elements
|
|
1995
|
+
selected: `editor.duplicateScopes()`) and how to compare, then lists the groups found (a `DuplicatesDialog`, one for
|
|
1996
|
+
every modeller): each with a box to tick, its message (choosing it shows it) and **Merge…**; **Merge selected** merges
|
|
1997
|
+
the ticked groups one after another. The list stays open while they are merged, found again after each merge, so what
|
|
1998
|
+
was merged leaves it (`editor.askDuplicates()`, `editor.showDuplicates(options, { within })`, `checker.possibleDuplicates(options, { within })`).
|
|
1999
|
+
Its form is one for every modeller (`duplicateSearchFields({ scope, compare, properties, specializations })`,
|
|
2000
|
+
`duplicateSearchOptions(values)`, `validateDuplicateSearch`). The options are the jArchi dedupe scripts' (`DUPLICATE_OPTIONS`,
|
|
2001
|
+
`duplicateGroups(elements, options)`, `duplicateKey(element, options)`, `wordKey(text, options)`): compare the
|
|
2002
|
+
names, the documentation (an element with none, or only noise, is left out) or a property's value (an application
|
|
2003
|
+
id); leave out noise words (`NOISE_WORDS`: filler, "tbc", "tba" …), numbers and version numbers ("2", "v2",
|
|
2004
|
+
"1.2.3"), one-letter words and words matching a pattern (`^[Aa][0-9A-Fa-f]{5}$` for id codes); take a plural as its
|
|
2005
|
+
word; look across types; leave out a specialization (Decommissioned). **Match** `exact` compares the text as it is,
|
|
2006
|
+
case and spacing aside; `words` (the default) by its words, as above. Any other field can be compared by naming it
|
|
2007
|
+
(`by: 'attributes'`; a list is its items' names together). The model check compares names by their words with the
|
|
2008
|
+
filler words only.
|
|
2009
|
+
|
|
2010
|
+
```js
|
|
2011
|
+
editor.showDuplicates({ by: 'documentation', noiseWords: NOISE_WORDS, ignoreNumbers: true });
|
|
2012
|
+
editor.showDuplicates({ by: 'property', property: 'AppId', acrossTypes: true });
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
With two or more elements selected, a shape's menu has **Merge N elements…** for them
|
|
2016
|
+
(`editor.elementIdsOf(boxIds)`: each box's element, once). Merging (`editor.askMerge(ids)`, resolving to the command
|
|
2017
|
+
or null) shows the elements side by side first (a `MergeCompareDialog`, one for every modeller: a column each, a row
|
|
2018
|
+
each for name, type, specialization, documentation, each property, relationships and views, the rows where they
|
|
2019
|
+
differ marked ≠; `editor.mergeComparison(elements)`), and under it asks which element to keep (each choice says how
|
|
2020
|
+
many views and relationships use it) and, where they differ, what it ends with: the documentation (all of it joined,
|
|
2021
|
+
the default, or one element's), the value of each property they have with different values, the specialization
|
|
2022
|
+
(`editor.mergeChoices(elements)`). **OK, merge** merges; Cancel leaves them. Elements that differ only in the views
|
|
2023
|
+
they are on merge into the first without asking. The others are merged into it, one undo step (`model.mergeCommand(keepId,
|
|
2024
|
+
otherIds, { documentation, properties, specialization })`, a `MergeElementsCommand`). The kept element always takes
|
|
2025
|
+
their relationships and their boxes on every view, and the properties it lacks; a relationship left joining it to
|
|
2026
|
+
itself, or the same as another, goes, its lines drawn as the other.
|
|
2027
|
+
|
|
2028
|
+
**View ▸ Remove repeated boxes (N)** takes off the view each box that only repeats another of its element: the same
|
|
2029
|
+
label shown, in the same box (or none), with the same lines drawn (each relationship, or connection, to the same
|
|
2030
|
+
thing at the other end), as one undo step with its lines (`view.repeatedBoxes()`, `editor.removeRepeatedBoxes()`).
|
|
2031
|
+
The first box stays; a box holding others is never taken off. The **File** menu (`EditorPart`'s, and the ArchiMate editor's) exports the current view
|
|
1857
2032
|
as an image: PNG, SVG or a copy to the clipboard (`editor.exportImage('png' | 'svg' | 'copy')`, without the grid
|
|
1858
2033
|
or selection; the ArchiMate editor's is off with `features.exportImage: false`). **Generate View For This**, on
|
|
1859
2034
|
an element's menu in the diagram or the tree, makes a view of the element with everything related to it
|
|
@@ -1894,7 +2069,8 @@ changed: `editor.model.duplicateView(id)` opens and shows the copy, deleting a v
|
|
|
1894
2069
|
|
|
1895
2070
|
**Canvases and templates.** As in Archi, the model tree's **Views** holds every view, canvases (`CanvasView`)
|
|
1896
2071
|
and sketches included, in the sub-folders the Archi file kept it in (`view.archiFolder`; `viewFolderNodes(views,
|
|
1897
|
-
depth)` makes a level of them)
|
|
2072
|
+
depth, emptyFolders)` makes a level of them). A sub-folder with no view in it is kept too, as its path
|
|
2073
|
+
(`model.emptyViewFolders`, read from an Archi or Exchange Format file and written back to either); templates are apart (`model.diagramViews()`, `model.canvases()`,
|
|
1898
2074
|
`model.templates()`). The **Templates** folder holds the model's own templates (canvases
|
|
1899
2075
|
marked `template`, saved in the `.archimate` file) and a **Library** of templates kept across models: a
|
|
1900
2076
|
`CanvasTemplateLibrary` (a `TemplateLibrary`, the base any kind of view's library extends: entries, built-ins
|
|
@@ -2307,7 +2483,7 @@ for something else in ArchiMate, each as one undo step:
|
|
|
2307
2483
|
|
|
2308
2484
|
`ArchimatePalette({version, onPick, document})` renders the element types grouped by layer
|
|
2309
2485
|
(`ArchimatePalette.groups(version)`: the notation's layers, then ArchiMate 4's common behaviour types),
|
|
2310
|
-
each tile only its notation glyph in its layer colour,
|
|
2486
|
+
each tile only its notation glyph in its layer colour, six to a row at 12px (`ArchimatePalette.TILE`; the name shows on hover); drag a tile onto the canvas or click it (`onPick`).
|
|
2311
2487
|
|
|
2312
2488
|
`ArchimateCanvasPart` (workspace bundle, type `archimate-canvas`; `examples/archimate.html`, and the testbed
|
|
2313
2489
|
`test/archimate-canvas-testbed.html`) is the
|
|
@@ -2421,7 +2597,11 @@ model's, then the tool's, then the default (`cascade(view, modelId)`; `getModelO
|
|
|
2421
2597
|
what the model level itself sets, `getModelSettings(modelId)` the model's resolved values). Global
|
|
2422
2598
|
Settings sets the tool's value and the model's (which may inherit the tool's); a view's Properties sets
|
|
2423
2599
|
the diagram's (which may inherit the model's). "Invalid relationships" (`invalidRelationships`:
|
|
2424
|
-
`highlight`, `tooltip`, `off`) is set this way.
|
|
2600
|
+
`highlight`, `tooltip`, `off`) is set this way. Global Settings also sets how the model tree searches and shows, for
|
|
2601
|
+
every model (tool level, `TREE_SETTINGS_PROPERTIES`): the leaves a folder shows before "more" (`treeLeafLimit`, 8)
|
|
2602
|
+
and the characters a search needs (`treeSearchMinChars`, 3). `settingsManager.treeOptions()` gives them as
|
|
2603
|
+
ModelTree's options (`{ leafLimit, minFilterChars }`); the canvas part makes its tree with them and redraws it when
|
|
2604
|
+
they change.
|
|
2425
2605
|
|
|
2426
2606
|
```js
|
|
2427
2607
|
settingsManager.saveToolSettings({ invalidRelationships: 'tooltip' }); // every model, unless it says otherwise
|
|
@@ -2670,6 +2850,11 @@ diagram). After an item acts, the bar re-renders. Items with live labels (the zo
|
|
|
2670
2850
|
| `ToolbarText(text, style)` | Live text |
|
|
2671
2851
|
| `ToolbarSeparator()` | Divider |
|
|
2672
2852
|
|
|
2853
|
+
`PopupMenu.show(x, y, items)` is the one popup or context menu (a toolbar menu's, an editor's, a host's
|
|
2854
|
+
right-click): items `{ label, action, danger?, checked?, disabled? }`, `{ label, items }` for a submenu, or
|
|
2855
|
+
`{ separator: true }`. One is open at a time; it moves back inside the window near an edge, and closes on a
|
|
2856
|
+
choice, a click elsewhere, Escape, or a scroll.
|
|
2857
|
+
|
|
2673
2858
|
```javascript
|
|
2674
2859
|
new MenuBar({ container, editor, items: [
|
|
2675
2860
|
new ToolbarMenu('File', [{ label: 'Export', action: exportIt }]),
|
|
@@ -2677,6 +2862,19 @@ new MenuBar({ container, editor, items: [
|
|
|
2677
2862
|
] }).render();
|
|
2678
2863
|
```
|
|
2679
2864
|
|
|
2865
|
+
`Dialog` is the one dialog frame, which `FormDialog` (a `<form>`) and any host's own dialog share: a title
|
|
2866
|
+
(drag it to move the dialog), an optional ✕ (`closeButton`), a body that scrolls between the title and the
|
|
2867
|
+
buttons, which stay in view, and Escape or a click outside to close. `buttons` are
|
|
2868
|
+
`[{ label, action(dialog), primary? }]`, `size` is `'tall'` or `'wide'`, and `onClose(applied)` is told once.
|
|
2869
|
+
`open()` shows it over the page; `open(host)` places it in `host` with no backdrop and no ✕, to show a dialog's
|
|
2870
|
+
content as a pane or a tab. A subclass gives its content as `content()` specs; a host fills `dialog.body` after
|
|
2871
|
+
`open()`, and adds to `dialog.titleBar` (a link) or `dialog.footer` (a pager).
|
|
2872
|
+
|
|
2873
|
+
```javascript
|
|
2874
|
+
const rows = new Dialog({ title: 'orders', closeButton: true, size: 'tall' }).open();
|
|
2875
|
+
rows.body.append(table);
|
|
2876
|
+
```
|
|
2877
|
+
|
|
2680
2878
|
`FormDialog` is the one modal form: `fields` of type `text`, `textarea`, `number`, `color`, `select`
|
|
2681
2879
|
or `checkbox`. A field with `inherit: { text, short? }` may be left unset; it then reads as `undefined` (a select
|
|
2682
2880
|
offers `text` as its first choice; any other field, a checkbox too, gets a checkbox named `short`, default
|
|
@@ -2747,6 +2945,88 @@ to it) / `deleteEdgeCommand(id)`. A subclass whose shapes and lines stand for so
|
|
|
2747
2945
|
overrides those factories (ArchimateCanvasEditor does). A template class says whether the editor
|
|
2748
2946
|
offers it a rotate handle (`static rotatable`, true by default; ArchiMate's templates are not). `pickFile(accept)` resolves with the chosen `File` (or `null`).
|
|
2749
2947
|
|
|
2948
|
+
## Navigation menus
|
|
2949
|
+
|
|
2950
|
+
`NavMenu` is a vertical navigation menu (a sidebar's), written as data: no markup. Each entry is
|
|
2951
|
+
one of three kinds, built from its JSON by `NavMenu.entry`:
|
|
2952
|
+
|
|
2953
|
+
| JSON | Class | Renders |
|
|
2954
|
+
|---|---|---|
|
|
2955
|
+
| `{ label, id?, icon?, href?, action?, selectable?, title?, cls?, visible? }` | `NavItem` | A link (`<a href>`) with `href`, otherwise a button |
|
|
2956
|
+
| `{ group, items, id?, open?, title?, cls?, visible? }` | `NavGroup` | A heading that opens and closes (`<details>`) over its items |
|
|
2957
|
+
| `{ section, title?, cls?, visible? }` | `NavSection` | A heading between entries |
|
|
2958
|
+
|
|
2959
|
+
```javascript
|
|
2960
|
+
import { NavMenu } from '@peter.prib/renderengine';
|
|
2961
|
+
|
|
2962
|
+
const menu = new NavMenu({
|
|
2963
|
+
label: 'Workbench',
|
|
2964
|
+
context: { role: 'analyst' },
|
|
2965
|
+
open: { devtools: true },
|
|
2966
|
+
items: [
|
|
2967
|
+
{ section: 'Notebooks' },
|
|
2968
|
+
{ id: 'new-nb', label: '+ New Notebook', action: newNotebook, selectable: false },
|
|
2969
|
+
{ group: 'Dev Tools', id: 'devtools', items: [
|
|
2970
|
+
{ id: 'datamodel', icon: '◫', label: 'Data Model', href: '#', action: (event) => {
|
|
2971
|
+
event.preventDefault();
|
|
2972
|
+
openTool('datamodel', { beside: event.altKey });
|
|
2973
|
+
} },
|
|
2974
|
+
] },
|
|
2975
|
+
{ id: 'admin', icon: '⚙', label: 'Admin', href: '/admin', visible: (ctx) => ctx.role === 'admin' },
|
|
2976
|
+
],
|
|
2977
|
+
onToggle: (groupId, open) => save(menu.serialize()),
|
|
2978
|
+
}).render(document.getElementById('nav'));
|
|
2979
|
+
```
|
|
2980
|
+
|
|
2981
|
+
- **A click** runs the item's `action(event, item, menu)`, then makes the item the active one (unless it
|
|
2982
|
+
has no `id`, or `selectable: false`), then calls `onSelect(item, event)`. A link still navigates unless
|
|
2983
|
+
the action calls `event.preventDefault()`.
|
|
2984
|
+
`href` may be a function of the context, for an address known only later (`setContext({ adminUrl })`). The active item has the class `ws-nav-item-active` and
|
|
2985
|
+
`aria-current="page"`; `setActive(id)` moves it in place (`null`: none).
|
|
2986
|
+
- **`visible`** is a boolean or a function of the menu's `context`. A group none of whose items shows is
|
|
2987
|
+
hidden too. `setContext({ role })` merges into the context and re-renders.
|
|
2988
|
+
- **State:** `serialize()` returns `{ activeId, open: { groupId: boolean } }` (groups with an `id`), and
|
|
2989
|
+
the constructor takes `activeId` and `open` back; `onToggle(groupId, open)` reports a group opened or
|
|
2990
|
+
closed.
|
|
2991
|
+
- `find(id)` returns an entry anywhere in the menu; `all()` lists them depth first.
|
|
2992
|
+
- **Styles** are the workspace's (`ensureStyles`): `ws-nav`, `ws-nav-item`, `ws-nav-icon`, `ws-nav-label`,
|
|
2993
|
+
`ws-nav-group`, `ws-nav-group-summary`, `ws-nav-section`. Colours are inherited, so the menu suits a
|
|
2994
|
+
dark sidebar and a light one; `cls` adds a host's own classes to an entry.
|
|
2995
|
+
|
|
2996
|
+
`NavMenu` imports nothing from the browser at module scope, so it is exported from the node entry
|
|
2997
|
+
(`src/index.js`) as well as the workspace bundle.
|
|
2998
|
+
|
|
2999
|
+
## Icons
|
|
3000
|
+
|
|
3001
|
+
`IconSet` holds named icons as data, and builds each as an `<svg>` spec for `el()`, so nothing is parsed. An
|
|
3002
|
+
icon is JSON: a `viewBox`, its `paint`, and its shapes as `[tag, attrs]` pairs.
|
|
3003
|
+
|
|
3004
|
+
```javascript
|
|
3005
|
+
import { ICONS, icon, IconSet } from '@peter.prib/renderengine';
|
|
3006
|
+
|
|
3007
|
+
el(icon('cpu')); // the built-in pool
|
|
3008
|
+
el(ICONS.spec('database', { size: 16, title: 'Catalog' }));
|
|
3009
|
+
const mine = ICONS.with({ // a host's own icons over the built-in ones
|
|
3010
|
+
ring: { viewBox: '0 0 24 24', shapes: [['circle', { cx: 12, cy: 12, r: 10 }]] },
|
|
3011
|
+
});
|
|
3012
|
+
```
|
|
3013
|
+
|
|
3014
|
+
- **`paint`:** `'stroke'` (the default) outlines the shapes in `currentColor`, so an icon takes its parent's
|
|
3015
|
+
colour. `'fill'` fills them in `currentColor`. `'none'` leaves the colours the shapes carry, as a brand mark does.
|
|
3016
|
+
- **`spec(name, { size = 24, title, cls })`:** returns an `<svg class="re-icon" data-icon="name">` spec. Without
|
|
3017
|
+
a `title` it is `aria-hidden`; with one it is `role="img"` and carries a `<title>`. An unknown name throws.
|
|
3018
|
+
- **Methods:** `define(name, icon)` checks the icon as it is added; `add(defs)`, `has(name)`, `get(name)` and
|
|
3019
|
+
`names()` work on the set; `with(defs)` returns a copy with `defs` added, leaving the original unchanged.
|
|
3020
|
+
- **The built-in icons** (`ICONS`, defined by `ICON_DEFS`): `dashboard`, `zap`, `clock`, `clipboard-list`,
|
|
3021
|
+
`book`, `cpu`, `memory`, `activity`, `database`, and the brand marks `google` and `microsoft`. They are named
|
|
3022
|
+
for the picture, not for where they are used.
|
|
3023
|
+
- **In a `NavMenu`:** an item's `icon` that names an icon in the menu's `icons` (default `ICONS`) is drawn as
|
|
3024
|
+
that icon; any other text, such as an emoji, is shown as it is.
|
|
3025
|
+
|
|
3026
|
+
`el()` builds an `svg` spec, and everything inside it, in the SVG namespace (`SVG_NS`, exported with `el` from
|
|
3027
|
+
the workspace bundle), until a `foreignObject`, whose children are HTML again. A spec's `ns` sets the namespace
|
|
3028
|
+
explicitly.
|
|
3029
|
+
|
|
2750
3030
|
## Where to go next
|
|
2751
3031
|
|
|
2752
3032
|
- **`ARCHITECTURE.md`** — module layout, the chart-type catalog mechanics, how to add a new chart type.
|