@peter.prib/renderengine 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +215 -213
  2. package/REFERENCE.md +94 -6
  3. package/dist/renderengine-workspace.umd.js +3254 -2089
  4. package/dist/renderengine.umd.js +3016 -2086
  5. package/examples/data-model.html +324 -0
  6. package/package.json +70 -68
  7. package/src/charting/CheckboxTreeSelector.js +373 -304
  8. package/src/charting/DiagramEditor.js +48 -6
  9. package/src/charting/DiagramTypeRegistry.js +5 -1
  10. package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
  11. package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
  12. package/src/charting/datamodel/DataModel.js +299 -0
  13. package/src/charting/datamodel/DataModelContext.js +108 -0
  14. package/src/charting/datamodel/DataModelPerspective.js +84 -0
  15. package/src/charting/datamodel/LevelMapping.js +38 -0
  16. package/src/charting/datamodel/LevelSchema.js +41 -0
  17. package/src/charting/datamodel/index.js +5 -0
  18. package/src/charting/diagrams/DataModelDiagram.js +160 -86
  19. package/src/charting/diagrams/DataModelRelationshipStrategy.js +86 -38
  20. package/src/charting/diagrams/nesting.js +31 -0
  21. package/src/charting/modelTree/ModelTree.js +202 -164
  22. package/src/charting/modelTree/ModelTreePanel.js +36 -3
  23. package/src/charting/modelTree/TreeViewMode.js +131 -110
  24. package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
  25. package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
  26. package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
  27. package/src/datamodel.js +132 -122
  28. package/src/diagramCatalog.js +35 -30
  29. package/src/index.js +6 -1
  30. package/src/workspace/index.js +1 -0
  31. package/src/workspace/parts/ArchimateCanvasPart.js +5 -2
  32. package/src/workspace/parts/ChartPart.js +6 -1
  33. package/src/workspace/parts/DataModelPart.js +234 -0
package/src/datamodel.js CHANGED
@@ -1,122 +1,132 @@
1
- // Copyright 2026 Jaroslav Peter Prib, Purveyor of Excellence
2
- /**
3
- * Data-modelling notation — deliberately its own file/OO module, same split
4
- * as src/archimate.js: keeps this domain's vocabulary (levels, notation
5
- * styles, cardinality) out of RenderEngine itself, queried through the
6
- * `DataModelNotation` registry rather than flat string-keyed maps.
7
- *
8
- * **Three levels, one entity.** A conceptual/logical/physical data model
9
- * isn't three unrelated diagrams — it's the same entities and relationships
10
- * shown at increasing fidelity. `dataModelDiagram` (see RenderEngine)
11
- * expresses that as ONE entity object carrying per-level detail
12
- * (`entity.logical.attributes`, `entity.physical.attributes`, ...) under a
13
- * shared `name`/`description` — the same "object" (level-agnostic identity)
14
- * vs "visual/detail" (this level's own presentation) split archimateDiagram
15
- * already draws between an element and its diagram — with `options.level`
16
- * picking which one's on screen. That's the cross-mapping: switching level
17
- * re-renders the SAME entities/relationships, not a second, separately
18
- * maintained diagram, so a conceptual entity, its logical attributes, and
19
- * its physical column types can never drift out of correspondence.
20
- *
21
- * **Three notations, one cardinality vocabulary.** A relationship's
22
- * cardinality at each end is stored once, as a UML-style multiplicity
23
- * string (`'0..1'|'1'|'0..*'|'*'|'1..*'`) — CARDINALITIES below — and each
24
- * `NotationStyle` renders it its own way: Chen turns the relationship
25
- * itself into a diamond-shaped node with a plain text label ('1'/'N') on
26
- * each connecting edge; Crow's Foot renders it as an arrowhead-style glyph
27
- * at the entity end (fork for "many", tick for "one", circle for
28
- * "optional" — see the `crowsfoot-*` markers in SVGRenderer.js); UML shows
29
- * the multiplicity string itself as a text label near that end
30
- * (`edge.sourceLabel`/`targetLabel`, a plain graphDiagram feature, not
31
- * data-model-specific). None of these are pixel-perfect renditions of the
32
- * real notations' full rule set (Chen's own attribute-oval convention,
33
- * every crow's-foot optionality combination, UML's aggregation/composition
34
- * diamonds) — they're a representative, genuinely-readable version of each,
35
- * the same calibration archimateDiagram's own icons use.
36
- */
37
-
38
- /** One data-modelling abstraction level. See DataModelNotation.levels(). */
39
- export class DataModelLevel {
40
- constructor(name, description) {
41
- this.name = name;
42
- this.description = description;
43
- }
44
- }
45
-
46
- /** The canonical cardinality vocabulary every NotationStyle renders
47
- * differently — UML-style multiplicity strings, since they're the only one
48
- * of the three notations expressive enough to BE the canonical form
49
- * without losing information (Chen/Crow's Foot each collapse some of these
50
- * distinctions on purpose). */
51
- const CARDINALITIES = new Set(['0..1', '1', '0..*', '*', '1..*']);
52
-
53
- function chenCardinalityLabel(cardinality) {
54
- return cardinality === '1' || cardinality === '0..1' ? '1' : 'N';
55
- }
56
-
57
- const CROWSFOOT_ARROWHEADS = {
58
- '1': 'crowsfoot-one',
59
- '0..1': 'crowsfoot-one-optional',
60
- '1..*': 'crowsfoot-many-mandatory',
61
- '0..*': 'crowsfoot-many',
62
- '*': 'crowsfoot-many',
63
- };
64
-
65
- /**
66
- * One drawing technique for entities/relationships — Chen, Crow's Foot, or
67
- * UML (class-diagram style). `entityShape` picks the graphDiagram node shape
68
- * (`'rect'` for a plain box, `'table'` for a header + attribute-rows card);
69
- * `relationshipAsNode` (Chen only) turns a relationship into its own
70
- * diamond-shaped node with two plain edges instead of one direct edge;
71
- * `cardinalityArrowhead`/`cardinalityLabel` (each `(cardinality) => value or
72
- * undefined`) pick how a relationship's own cardinality at one end renders
73
- * — at most one of the two is ever set for a given notation, the other
74
- * always returns undefined.
75
- */
76
- export class NotationStyle {
77
- constructor(name, { entityShape, relationshipAsNode, cardinalityArrowhead, cardinalityLabel }) {
78
- this.name = name;
79
- this.entityShape = entityShape;
80
- this.relationshipAsNode = relationshipAsNode;
81
- this.cardinalityArrowhead = cardinalityArrowhead;
82
- this.cardinalityLabel = cardinalityLabel;
83
- }
84
- }
85
-
86
- const LEVELS = {
87
- conceptual: new DataModelLevel('conceptual', 'Entities and their relationships only — no attributes, no keys, no types.'),
88
- logical: new DataModelLevel('logical', 'Attributes and keys, still database-agnostic — no column types or engine specifics.'),
89
- physical: new DataModelLevel('physical', 'Real column types, keys, and constraints as they exist in an actual database.'),
90
- };
91
-
92
- const NOTATIONS = {
93
- chen: new NotationStyle('chen', {
94
- entityShape: 'rect',
95
- relationshipAsNode: true,
96
- cardinalityArrowhead: () => undefined,
97
- cardinalityLabel: chenCardinalityLabel,
98
- }),
99
- crowsfoot: new NotationStyle('crowsfoot', {
100
- entityShape: 'table',
101
- relationshipAsNode: false,
102
- cardinalityArrowhead: (cardinality) => CROWSFOOT_ARROWHEADS[cardinality],
103
- cardinalityLabel: () => undefined,
104
- }),
105
- uml: new NotationStyle('uml', {
106
- entityShape: 'table',
107
- relationshipAsNode: false,
108
- cardinalityArrowhead: () => undefined,
109
- cardinalityLabel: (cardinality) => cardinality,
110
- }),
111
- };
112
-
113
- export const DataModelNotation = {
114
- levels() { return Object.values(LEVELS); },
115
- levelNames() { return Object.keys(LEVELS); },
116
- level(name) { return LEVELS[name]; },
117
- notations() { return Object.values(NOTATIONS); },
118
- notationNames() { return Object.keys(NOTATIONS); },
119
- notation(name) { return NOTATIONS[name] || NOTATIONS.crowsfoot; },
120
- isCardinality(value) { return CARDINALITIES.has(value); },
121
- cardinalities() { return [...CARDINALITIES]; },
122
- };
1
+ // Copyright 2026 Jaroslav Peter Prib, Purveyor of Excellence
2
+ /**
3
+ * Data-modelling notation — deliberately its own file/OO module, same split
4
+ * as src/archimate.js: keeps this domain's vocabulary (levels, notation
5
+ * styles, cardinality) out of RenderEngine itself, queried through the
6
+ * `DataModelNotation` registry rather than flat string-keyed maps.
7
+ *
8
+ * **Three levels, one entity.** A conceptual/logical/physical data model
9
+ * isn't three unrelated diagrams — it's the same entities and relationships
10
+ * shown at increasing fidelity. `dataModelDiagram` (see RenderEngine)
11
+ * expresses that as ONE entity object carrying per-level detail
12
+ * (`entity.logical.attributes`, `entity.physical.attributes`, ...) under a
13
+ * shared `name`/`description` — the same "object" (level-agnostic identity)
14
+ * vs "visual/detail" (this level's own presentation) split archimateDiagram
15
+ * already draws between an element and its diagram — with `options.level`
16
+ * picking which one's on screen. That's the cross-mapping: switching level
17
+ * re-renders the SAME entities/relationships, not a second, separately
18
+ * maintained diagram, so a conceptual entity, its logical attributes, and
19
+ * its physical column types can never drift out of correspondence.
20
+ *
21
+ * **Three notations, one cardinality vocabulary.** A relationship's
22
+ * cardinality at each end is stored once, as a UML-style multiplicity
23
+ * string (`'0..1'|'1'|'0..*'|'*'|'1..*'`) — CARDINALITIES below — and each
24
+ * `NotationStyle` renders it its own way: Chen turns the relationship
25
+ * itself into a diamond-shaped node with a plain text label ('1'/'N') on
26
+ * each connecting edge; Crow's Foot renders it as an arrowhead-style glyph
27
+ * at the entity end (fork for "many", tick for "one", circle for
28
+ * "optional" — see the `crowsfoot-*` markers in SVGRenderer.js); UML shows
29
+ * the multiplicity string itself as a text label near that end
30
+ * (`edge.sourceLabel`/`targetLabel`, a plain graphDiagram feature, not
31
+ * data-model-specific). None of these are pixel-perfect renditions of the
32
+ * real notations' full rule set (Chen's own attribute-oval convention,
33
+ * every crow's-foot optionality combination, UML's aggregation/composition
34
+ * diamonds) — they're a representative, genuinely-readable version of each,
35
+ * the same calibration archimateDiagram's own icons use.
36
+ */
37
+
38
+ /** One data-modelling abstraction level. See DataModelNotation.levels(). */
39
+ export class DataModelLevel {
40
+ constructor(name, description) {
41
+ this.name = name;
42
+ this.description = description;
43
+ }
44
+
45
+ /** What a menu calls it ('Conceptual'). */
46
+ get label() {
47
+ return this.name.charAt(0).toUpperCase() + this.name.slice(1);
48
+ }
49
+ }
50
+
51
+ /** The canonical cardinality vocabulary every NotationStyle renders
52
+ * differently — UML-style multiplicity strings, since they're the only one
53
+ * of the three notations expressive enough to BE the canonical form
54
+ * without losing information (Chen/Crow's Foot each collapse some of these
55
+ * distinctions on purpose). */
56
+ const CARDINALITIES = new Set(['0..1', '1', '0..*', '*', '1..*']);
57
+
58
+ function chenCardinalityLabel(cardinality) {
59
+ return cardinality === '1' || cardinality === '0..1' ? '1' : 'N';
60
+ }
61
+
62
+ const CROWSFOOT_ARROWHEADS = {
63
+ '1': 'crowsfoot-one',
64
+ '0..1': 'crowsfoot-one-optional',
65
+ '1..*': 'crowsfoot-many-mandatory',
66
+ '0..*': 'crowsfoot-many',
67
+ '*': 'crowsfoot-many',
68
+ };
69
+
70
+ /**
71
+ * One drawing technique for entities/relationships — Chen, Crow's Foot, or
72
+ * UML (class-diagram style). `entityShape` picks the graphDiagram node shape
73
+ * (`'rect'` for a plain box, `'table'` for a header + attribute-rows card);
74
+ * `relationshipAsNode` (Chen only) turns a relationship into its own
75
+ * diamond-shaped node with two plain edges instead of one direct edge;
76
+ * `cardinalityArrowhead`/`cardinalityLabel` (each `(cardinality) => value or
77
+ * undefined`) pick how a relationship's own cardinality at one end renders
78
+ * — at most one of the two is ever set for a given notation, the other
79
+ * always returns undefined.
80
+ */
81
+ export class NotationStyle {
82
+ constructor(name, { label = name, entityShape, relationshipAsNode, cardinalityArrowhead, cardinalityLabel }) {
83
+ this.name = name;
84
+ /** What a menu calls it ("Crow's Foot"). */
85
+ this.label = label;
86
+ this.entityShape = entityShape;
87
+ this.relationshipAsNode = relationshipAsNode;
88
+ this.cardinalityArrowhead = cardinalityArrowhead;
89
+ this.cardinalityLabel = cardinalityLabel;
90
+ }
91
+ }
92
+
93
+ const LEVELS = {
94
+ conceptual: new DataModelLevel('conceptual', 'Entities and their relationships only — no attributes, no keys, no types.'),
95
+ logical: new DataModelLevel('logical', 'Attributes and keys, still database-agnostic — no column types or engine specifics.'),
96
+ physical: new DataModelLevel('physical', 'Real column types, keys, and constraints as they exist in an actual database.'),
97
+ };
98
+
99
+ const NOTATIONS = {
100
+ chen: new NotationStyle('chen', {
101
+ label: 'Chen',
102
+ entityShape: 'rect',
103
+ relationshipAsNode: true,
104
+ cardinalityArrowhead: () => undefined,
105
+ cardinalityLabel: chenCardinalityLabel,
106
+ }),
107
+ crowsfoot: new NotationStyle('crowsfoot', {
108
+ label: "Crow's Foot",
109
+ entityShape: 'table',
110
+ relationshipAsNode: false,
111
+ cardinalityArrowhead: (cardinality) => CROWSFOOT_ARROWHEADS[cardinality],
112
+ cardinalityLabel: () => undefined,
113
+ }),
114
+ uml: new NotationStyle('uml', {
115
+ label: 'UML',
116
+ entityShape: 'table',
117
+ relationshipAsNode: false,
118
+ cardinalityArrowhead: () => undefined,
119
+ cardinalityLabel: (cardinality) => cardinality,
120
+ }),
121
+ };
122
+
123
+ export const DataModelNotation = {
124
+ levels() { return Object.values(LEVELS); },
125
+ levelNames() { return Object.keys(LEVELS); },
126
+ level(name) { return LEVELS[name]; },
127
+ notations() { return Object.values(NOTATIONS); },
128
+ notationNames() { return Object.keys(NOTATIONS); },
129
+ notation(name) { return NOTATIONS[name] || NOTATIONS.crowsfoot; },
130
+ isCardinality(value) { return CARDINALITIES.has(value); },
131
+ cardinalities() { return [...CARDINALITIES]; },
132
+ };
@@ -1,30 +1,35 @@
1
- /**
2
- * Single source of truth for "what diagram types does RenderEngine support".
3
- *
4
- * Mirrors src/chartCatalog.js: the registry is the object-oriented lookup
5
- * layer over the flat catalog, and `getDiagramCatalogEntry` is the legacy-style
6
- * helper kept for backward compatibility.
7
- */
8
-
9
- import { DiagramRegistry } from './charting/DiagramRegistry.js';
10
- import { GraphDiagram } from './charting/diagrams/GraphDiagram.js';
11
- import { GraphDiagram3D } from './charting/diagrams/GraphDiagram3D.js';
12
- import { ArchimateDiagram } from './charting/diagrams/ArchimateDiagram.js';
13
- import { DataModelDiagram } from './charting/diagrams/DataModelDiagram.js';
14
- import { EbnfDiagram } from './charting/diagrams/EbnfDiagram.js';
15
-
16
- export const DIAGRAM_CATALOG = [
17
- { key: 'graph', method: 'graphDiagram', DiagramClass: GraphDiagram },
18
- { key: 'graph3d', method: 'graphDiagram3D', DiagramClass: GraphDiagram3D },
19
- { key: 'archimate', method: 'archimateDiagram', DiagramClass: ArchimateDiagram },
20
- { key: 'dataModel', method: 'dataModelDiagram', DiagramClass: DataModelDiagram },
21
- { key: 'ebnf', method: 'ebnfDiagram', DiagramClass: EbnfDiagram },
22
- ];
23
-
24
- /** Canonical registry instance seeded with the built-in diagram types. */
25
- export const diagramRegistry = new DiagramRegistry(DIAGRAM_CATALOG);
26
-
27
- /** Look up one catalog entry by key, or undefined if the key is unknown. */
28
- export function getDiagramCatalogEntry(key) {
29
- return diagramRegistry.get(key);
30
- }
1
+ /**
2
+ * Single source of truth for "what diagram types does RenderEngine support".
3
+ *
4
+ * Mirrors src/chartCatalog.js: the registry is the object-oriented lookup
5
+ * layer over the flat catalog, and `getDiagramCatalogEntry` is the legacy-style
6
+ * helper kept for backward compatibility.
7
+ */
8
+
9
+ import { DiagramRegistry } from './charting/DiagramRegistry.js';
10
+ import { GraphDiagram } from './charting/diagrams/GraphDiagram.js';
11
+ import { GraphDiagram3D } from './charting/diagrams/GraphDiagram3D.js';
12
+ import { ArchimateDiagram } from './charting/diagrams/ArchimateDiagram.js';
13
+ import { DataModelDiagram } from './charting/diagrams/DataModelDiagram.js';
14
+ import { EbnfDiagram } from './charting/diagrams/EbnfDiagram.js';
15
+
16
+ /**
17
+ * Each entry: `key`, the RenderEngine `method` that draws it, its `DiagramClass`,
18
+ * and `args`: the options a registered diagram (RenderEngine#register) keeps its
19
+ * data in, in the order the method takes them before its options.
20
+ */
21
+ export const DIAGRAM_CATALOG = [
22
+ { key: 'graph', method: 'graphDiagram', DiagramClass: GraphDiagram, args: ['nodes', 'edges'] },
23
+ { key: 'graph3d', method: 'graphDiagram3D', DiagramClass: GraphDiagram3D, args: ['nodes', 'edges'] },
24
+ { key: 'archimate', method: 'archimateDiagram', DiagramClass: ArchimateDiagram, args: ['elements', 'relationships'] },
25
+ { key: 'dataModel', method: 'dataModelDiagram', DiagramClass: DataModelDiagram, args: ['entities', 'relationships'] },
26
+ { key: 'ebnf', method: 'ebnfDiagram', DiagramClass: EbnfDiagram, args: ['rule'] },
27
+ ];
28
+
29
+ /** Canonical registry instance seeded with the built-in diagram types. */
30
+ export const diagramRegistry = new DiagramRegistry(DIAGRAM_CATALOG);
31
+
32
+ /** Look up one catalog entry by key, or undefined if the key is unknown. */
33
+ export function getDiagramCatalogEntry(key) {
34
+ return diagramRegistry.get(key);
35
+ }
package/src/index.js CHANGED
@@ -165,6 +165,7 @@ import { buildAnalysisPanel, buildAnalysisData } from './charting/modelTree/Anal
165
165
  import { buildTypeCategoryTree, buildPropertyTree } from './charting/modelTree/buildFilterTrees.js';
166
166
  import { DiagramTabs } from './charting/modelTree/DiagramTabs.js';
167
167
  import { DataModelNotation, DataModelLevel, NotationStyle } from './datamodel.js';
168
+ import { DataModel, DataModelPerspective, DataModelContext, LevelSchema, LevelMapping } from './charting/datamodel/index.js';
168
169
  import {
169
170
  ARCHI_VISUAL_OBJECTS, ARCHI_VIEW_TYPES,
170
171
  parseXML, parseArchiModel, toRenderEngineModel, toArchimateModel, toArchiXML, fromArchimateView, toArchiExport,
@@ -221,6 +222,7 @@ export {
221
222
  RadialLayout, DerivedRelationships, EdgeGeometry,
222
223
  buildModelTreePanel, buildAnalysisPanel, buildAnalysisData, buildTypeCategoryTree, buildPropertyTree, DiagramTabs,
223
224
  DataModelNotation, DataModelLevel, NotationStyle,
225
+ DataModel, DataModelPerspective, DataModelContext, LevelSchema, LevelMapping,
224
226
  exportElementsCSV, exportRelationshipsCSV, exportModelCSV,
225
227
  importElementsCSV, importRelationshipsCSV, importModelCSV,
226
228
  parseArchiModel, toArchimateModel, fromArchimateView, toArchiExport,
@@ -2594,7 +2596,10 @@ export class RenderEngine {
2594
2596
  const methodName = chartEntry?.method ?? diagramEntry?.method ?? config.type;
2595
2597
  if (typeof this[methodName] !== 'function') return;
2596
2598
 
2597
- if (config.options.nodes !== undefined) {
2599
+ if (diagramEntry?.args) {
2600
+ // A diagram takes its data as positional args (its catalog entry names them), then the options.
2601
+ this[methodName](...diagramEntry.args.map(arg => config.options[arg] ?? []), config.options);
2602
+ } else if (config.options.nodes !== undefined) {
2598
2603
  // Graph-shaped registration: two positional data args, not one.
2599
2604
  this[methodName](config.options.nodes, config.options.edges || [], config.options);
2600
2605
  } else {
@@ -56,6 +56,7 @@ export {
56
56
  LIGHT_THEME, DARK_THEME, MIDNIGHT_THEME, SEPIA_THEME,
57
57
  } from './themes.js';
58
58
  export { ChartPart } from './parts/ChartPart.js';
59
+ export { DataModelPart } from './parts/DataModelPart.js';
59
60
  export { EditorPart } from './parts/EditorPart.js';
60
61
  export { DiagramEditorPart } from './parts/DiagramEditorPart.js';
61
62
  export { ArchimateEditorPart } from './parts/ArchimateEditorPart.js';
@@ -55,6 +55,8 @@ export class ArchimateCanvasPart extends EditorPart {
55
55
  this.viewpoint = spec.viewpoint ?? 'All';
56
56
  this.viewpointMode = spec.viewpointMode ?? 'fade';
57
57
  this.showDerived = !!spec.showDerived;
58
+ /** Whether a line to a box nested in its source is drawn (Archi leaves it out). */
59
+ this.showNestedLines = !!spec.showNestedLines;
58
60
  this.settings = new SettingsManager();
59
61
  // A settings change (Model settings…, a view's properties) redraws the canvas.
60
62
  this._unsubscribeSettings = this.settings.onSettingsChange(() => { if (this.mounted) this.editor?.render(); });
@@ -224,7 +226,7 @@ export class ArchimateCanvasPart extends EditorPart {
224
226
  diagramContainerId: canvasId,
225
227
  view,
226
228
  model,
227
- nodeOptions: { archimateVersion: this.version, useDistinctShapes: this.distinctShapes, viewpoint: this.viewpoint, viewpointMode: this.viewpointMode, showDerived: this.showDerived },
229
+ nodeOptions: { archimateVersion: this.version, useDistinctShapes: this.distinctShapes, viewpoint: this.viewpoint, viewpointMode: this.viewpointMode, showDerived: this.showDerived, showNestedLines: this.showNestedLines },
228
230
  chooseKind: (kinds, from, to, options) => this.chooseKind(kinds, from, to, options),
229
231
  // The view's settings: background, title, watermark, how invalid relationships show.
230
232
  viewSettings: () => this.settings.getEffectiveSettings(this.model.currentView, 'default'),
@@ -234,8 +236,9 @@ export class ArchimateCanvasPart extends EditorPart {
234
236
  // Canvas templates: placed on a canvas, saved from its shapes (the browser's library).
235
237
  templateMenu: this.templateMenu,
236
238
  // Version and distinct shapes (⚙ Settings) change the palette too.
237
- onNodeOptionsChange: ({ archimateVersion, useDistinctShapes, viewpoint, viewpointMode, showDerived }) => {
239
+ onNodeOptionsChange: ({ archimateVersion, useDistinctShapes, viewpoint, viewpointMode, showDerived, showNestedLines }) => {
238
240
  this.showDerived = !!showDerived;
241
+ this.showNestedLines = !!showNestedLines;
239
242
  this.version = archimateVersion;
240
243
  this.distinctShapes = !!useDistinctShapes;
241
244
  this.viewpoint = viewpoint;
@@ -94,10 +94,15 @@ export class ChartPart extends Part {
94
94
  // The renderer is sized at construction, so a resize means a new engine.
95
95
  this.canvasHost.textContent = '';
96
96
  this.engine = new RenderEngine(this.canvasHost, { rendererType: this.rendererType });
97
- this.engine.register(this.chartId, this.chartType, this.chartOptions);
97
+ this.engine.register(this.chartId, this.chartType, this.drawOptions());
98
98
  this.engine.refreshChart(this.chartId);
99
99
  }
100
100
 
101
+ /** What is drawn: the part's options (its data among them). A subclass that draws something made from them says so. */
102
+ drawOptions() {
103
+ return this.chartOptions;
104
+ }
105
+
101
106
  /** Append a point to a bounded window — see REFERENCE.md's streaming section. */
102
107
  addDataPoint(pointOrFactory, opts) {
103
108
  this.engine?.addDataPoint(this.chartId, pointOrFactory, opts);
@@ -0,0 +1,234 @@
1
+ import { ChartPart } from './ChartPart.js';
2
+ import { el } from '../dom.js';
3
+ import { MenuBar } from '../../charting/MenuBar.js';
4
+ import { ToolbarSelect, ToolbarMenu, ToolbarText } from '../../charting/toolbar/index.js';
5
+ import { FormDialog } from '../../charting/FormDialog.js';
6
+ import { DataModelNotation } from '../../datamodel.js';
7
+ import { DataModel, DataModelPerspective, DataModelContext } from '../../charting/datamodel/index.js';
8
+ import { downloadPng, downloadSvg } from '../../exportImage.js';
9
+
10
+ /**
11
+ * DataModelPart — a data model (DataModel, drawn by dataModelDiagram) in a
12
+ * pane: its conceptual, logical and physical levels, each its own schema, and
13
+ * how they map; seen through a perspective (whose concepts, under whose names)
14
+ * read in a context (what they mean here, the rules that hold, where and
15
+ * when), in a notation (Chen, Crow's Foot, UML). Its toolbar switches each
16
+ * of them, Level offering Mapping (the levels side by side, joined where they
17
+ * map), and its Model menu makes, changes and removes perspectives and
18
+ * contexts. Boxes and notes stay where they were dragged. Export ▸ saves the
19
+ * picture (PNG, SVG). The drawing, its sizing and redrawing on resize are
20
+ * ChartPart's.
21
+ *
22
+ * Spec: `{ id, type: 'data-model', title?, levels?, mappings?, mappingLayout?,
23
+ * perspectives?, contexts?, perspective?, context?, level?, notation?, options? }`
24
+ * — the model as DataModel takes it (or `entities`/`relationships` in the older
25
+ * one-entity form); `perspective`/`context` the ids chosen; `level` default
26
+ * 'logical' ('mapping' for the mapping view), `notation` default 'crowsfoot';
27
+ * `options` anything else dataModelDiagram takes (editable, resizable by default).
28
+ */
29
+ export class DataModelPart extends ChartPart {
30
+ static type = 'data-model';
31
+
32
+ constructor(spec = {}) {
33
+ const { perspective = null, context = null, level = 'logical', notation = 'crowsfoot', options = {} } = spec;
34
+ super({ ...spec, chartType: 'dataModel', options: { editable: true, resizable: true, ...options, level, notation } });
35
+ /** The levels, their mappings, perspectives and contexts. */
36
+ this.model = new DataModel(spec);
37
+ /** The ids chosen, or null. */
38
+ this.perspectiveId = perspective;
39
+ this.contextId = context ?? this.model.perspective(perspective)?.context ?? null;
40
+ /** The level the drawing on screen is of (where a drag is kept). */
41
+ this._drawnLevel = level;
42
+ }
43
+
44
+ /** The entities at the level shown (the concepts in the mapping view). */
45
+ get entities() {
46
+ return this.model.level(this.level === DataModel.MAPPING ? 'conceptual' : this.level).entities;
47
+ }
48
+ get level() { return this.chartOptions.level; }
49
+ get notation() { return this.chartOptions.notation; }
50
+ /** The perspective chosen (DataModelPerspective), or null. */
51
+ get perspective() { return this.model.perspective(this.perspectiveId); }
52
+ /** The context chosen (DataModelContext), or null. */
53
+ get context() { return this.model.context(this.contextId); }
54
+
55
+ /** Show another level ('conceptual', 'logical', 'physical'), or how they map ('mapping'). */
56
+ setLevel(level) {
57
+ this.chartOptions.level = level;
58
+ this._redraw();
59
+ }
60
+
61
+ /** Draw the model in another notation ('chen', 'crowsfoot', 'uml'). */
62
+ setNotation(notation) {
63
+ this.chartOptions.notation = notation;
64
+ this._redraw();
65
+ }
66
+
67
+ /** See the model through a perspective (an id; null: the whole schema), in the context it is read in, if it names one. */
68
+ setPerspective(id) {
69
+ this.perspectiveId = id || null;
70
+ const own = this.perspective?.context;
71
+ if (own) this.contextId = own;
72
+ this._redraw();
73
+ }
74
+
75
+ /** Read the model in a context (an id; null: the schema's own descriptions). */
76
+ setContext(id) {
77
+ this.contextId = id || null;
78
+ this._redraw();
79
+ }
80
+
81
+ /** What is drawn: the perspective's view of the level (or the mapping) in the context (DataModel#view), in the notation. */
82
+ drawOptions() {
83
+ return { ...this.chartOptions, ...this.model.view({ perspective: this.perspectiveId, context: this.contextId, level: this.level }) };
84
+ }
85
+
86
+ render() {
87
+ Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', height: '100%' });
88
+ const toolbar = this.container.appendChild(el({}));
89
+ super.render();
90
+ Object.assign(this.canvasHost.style, { flex: '1 1 auto', minHeight: '0', height: 'auto' });
91
+ this.menuBar = new MenuBar({ container: toolbar, items: this.menuBarItems(), document: this.container.ownerDocument }).render();
92
+ }
93
+
94
+ /**
95
+ * Model ▸ (perspectives and contexts of one's own), Perspective and Context
96
+ * (when the model has any; the context's scope beside them), Level (each
97
+ * level from DataModelNotation, then Mapping) and Notation, and Export ▸.
98
+ */
99
+ menuBarItems() {
100
+ const { perspectives, contexts } = this.model;
101
+ return [
102
+ new ToolbarMenu('Model', () => this.modelItems()),
103
+ ...(perspectives.length ? [new ToolbarSelect('Perspective',
104
+ () => [['', 'Whole schema'], ...this.model.perspectives.map(p => [p.id, p.name, p.description])],
105
+ () => this.perspectiveId ?? '', (v) => this.setPerspective(v))] : []),
106
+ ...(contexts.length ? [
107
+ new ToolbarSelect('Context',
108
+ () => [['', 'None'], ...this.model.contexts.map(c => [c.id, c.name, c.description])],
109
+ () => this.contextId ?? '', (v) => this.setContext(v)),
110
+ new ToolbarText(() => this.context?.scope ?? ''),
111
+ ] : []),
112
+ new ToolbarSelect('Level', () => [
113
+ ...DataModelNotation.levels().map(l => [l.name, l.label, l.description]),
114
+ [DataModel.MAPPING, 'Mapping', 'The levels side by side, joined where one realises another'],
115
+ ], () => this.level, (v) => this.setLevel(v)),
116
+ new ToolbarSelect('Notation', () => DataModelNotation.notations().map(n => [n.name, n.label]), () => this.notation, (v) => this.setNotation(v)),
117
+ new ToolbarMenu('Export', () => this.exportItems()),
118
+ ];
119
+ }
120
+
121
+ /** Model ▸: make, change or remove a perspective or a context (the one chosen). */
122
+ modelItems() {
123
+ return [
124
+ { label: 'New perspective…', action: () => this.editPerspective(null) },
125
+ { label: 'Edit perspective…', disabled: !this.perspective || undefined, action: () => this.editPerspective(this.perspective) },
126
+ { label: 'Delete perspective', disabled: !this.perspective || undefined, action: () => this.removePerspective(this.perspectiveId) },
127
+ { separator: true },
128
+ { label: 'New context…', action: () => this.editContext(null) },
129
+ { label: 'Edit context…', disabled: !this.context || undefined, action: () => this.editContext(this.context) },
130
+ { label: 'Delete context', disabled: !this.context || undefined, action: () => this.removeContext(this.contextId) },
131
+ ];
132
+ }
133
+
134
+ /** A dialog making a perspective (none given) or changing one (DataModelPerspective.fields); applied, it is the one seen through. */
135
+ editPerspective(perspective) {
136
+ return this._dialog(perspective ? `Perspective: ${perspective.name}` : 'New perspective', DataModelPerspective.fields(this.model, perspective), (values) => {
137
+ const spec = DataModelPerspective.specFrom(values, this.model);
138
+ const saved = perspective ? this.model.updatePerspective(perspective.id, spec) : this.model.addPerspective(spec);
139
+ this._changed();
140
+ this.setPerspective(saved.id);
141
+ });
142
+ }
143
+
144
+ /** A dialog making a context (none given) or changing one (DataModelContext.fields); applied, it is the one read in. */
145
+ editContext(context) {
146
+ return this._dialog(context ? `Context: ${context.name}` : 'New context', DataModelContext.fields(this.model, context), (values) => {
147
+ const spec = DataModelContext.specFrom(values, this.model, context);
148
+ const saved = context ? this.model.updateContext(context.id, spec) : this.model.addContext(spec);
149
+ this._changed();
150
+ this.setContext(saved.id);
151
+ });
152
+ }
153
+
154
+ removePerspective(id) {
155
+ this.model.removePerspective(id);
156
+ this._changed();
157
+ this.setPerspective(null);
158
+ }
159
+
160
+ removeContext(id) {
161
+ this.model.removeContext(id);
162
+ this._changed();
163
+ this.setContext(null);
164
+ }
165
+
166
+ /** Open a form (a name is needed). @private */
167
+ _dialog(title, fields, apply) {
168
+ return new FormDialog({
169
+ title, fields, submitLabel: 'Save', idPrefix: 'data-model-',
170
+ document: this.container?.ownerDocument ?? globalThis.document,
171
+ validate: ({ name }) => ((name ?? '').trim() ? null : 'Give it a name'),
172
+ onApply: apply,
173
+ }).open();
174
+ }
175
+
176
+ /** The perspectives or contexts changed: the toolbar shows them anew. @private */
177
+ _changed() {
178
+ if (this.menuBar) this.menuBar.items = this.menuBarItems();
179
+ this.menuBar?.render?.();
180
+ }
181
+
182
+ /** Export ▸: the picture, as PNG or SVG, named after the part. */
183
+ exportItems() {
184
+ const name = this.title || 'data-model';
185
+ return [
186
+ { label: 'Image (PNG)', action: () => downloadPng(this.canvasHost, { name }) },
187
+ { label: 'Image (SVG)', action: () => downloadSvg(this.canvasHost, { name }) },
188
+ ];
189
+ }
190
+
191
+ /** Redraw now (a choice made), when there is somewhere to draw. @private */
192
+ _redraw() {
193
+ if (this.canvasHost) this._draw();
194
+ }
195
+
196
+ /**
197
+ * Keep where the boxes and notes were dragged (the drawing is made anew each
198
+ * time): on the level shown, or in the mapping view's own layout. Then draw.
199
+ */
200
+ _draw() {
201
+ const pos = this.engine?._graph?.pos;
202
+ if (pos && this._drawnLevel) {
203
+ if (this._drawnLevel === DataModel.MAPPING) {
204
+ for (const [id, p] of pos) if (id.includes(':')) this.model.mappingLayout[id] = { x: p.x, y: p.y };
205
+ } else {
206
+ for (const entity of this.model.level(this._drawnLevel).entities) {
207
+ const p = pos.get(entity.id);
208
+ if (p) { entity.x = p.x; entity.y = p.y; }
209
+ }
210
+ for (const rule of this.model.contexts.flatMap(c => c.constraints)) {
211
+ const p = pos.get(rule.id);
212
+ if (p) this.model.placeNote(this._drawnLevel, rule, p);
213
+ }
214
+ }
215
+ }
216
+ this._drawnLevel = this.level;
217
+ super._draw();
218
+ }
219
+
220
+ dispose() {
221
+ this.menuBar?.dispose?.();
222
+ super.dispose();
223
+ }
224
+
225
+ /** The spec again: the model (with where its boxes are), what is chosen, and the other options. */
226
+ serialize() {
227
+ const { options, chartType, ...base } = super.serialize();
228
+ const { level, notation, ...rest } = options;
229
+ return {
230
+ ...base, ...this.model.toJSON(),
231
+ perspective: this.perspectiveId, context: this.contextId, level, notation, options: rest,
232
+ };
233
+ }
234
+ }