@peter.prib/renderengine 0.15.2 → 0.15.3

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 (42) hide show
  1. package/dist/renderengine-workspace.umd.js +5963 -3766
  2. package/dist/renderengine.umd.js +5863 -3772
  3. package/documentation/REFERENCE.md +37 -3
  4. package/package.json +1 -1
  5. package/src/archimateArchiFormat.js +114 -10
  6. package/src/archimateArchive.js +27 -4
  7. package/src/archimateExchangeFormat.js +12 -4
  8. package/src/archimateSpecializations.js +11 -1
  9. package/src/chartCatalog.js +6 -4
  10. package/src/charting/ArchimateDiagramEditor.js +5 -108
  11. package/src/charting/DiagramTypeRegistry.js +20 -1
  12. package/src/charting/DiagramViewPropertiesDialog.js +54 -10
  13. package/src/charting/Legend.js +1 -2
  14. package/src/charting/Statistics.js +14 -0
  15. package/src/charting/charts/BarChart.js +59 -8
  16. package/src/charting/charts/HexbinChart.js +15 -0
  17. package/src/charting/charts/LollipopChart.js +42 -7
  18. package/src/charting/decision/DecisionWorkbench.js +71 -4
  19. package/src/charting/diagramModel/DiagramView.js +7 -2
  20. package/src/charting/diagrams/GraphDiagram3D.js +15 -0
  21. package/src/charting/graph/GraphSource.js +72 -0
  22. package/src/charting/modelTree/NavigatorModel.js +22 -8
  23. package/src/charting/report/TemplatedReport.js +27 -377
  24. package/src/charting/report/ViewsReport.js +118 -0
  25. package/src/charting/report/reportWriters.js +496 -0
  26. package/src/charting/templates/C4BoundaryTemplate.js +34 -0
  27. package/src/charting/templates/C4ComponentTemplate.js +30 -0
  28. package/src/charting/templates/C4ContainerTemplate.js +49 -0
  29. package/src/charting/templates/C4PersonTemplate.js +35 -0
  30. package/src/charting/templates/C4SystemTemplate.js +32 -0
  31. package/src/charting/templates/C4Template.js +95 -0
  32. package/src/charting/templates/index.js +6 -0
  33. package/src/charting/templates/templateRegistry.js +7 -0
  34. package/src/compute/ComputeBackend.js +132 -0
  35. package/src/compute/CpuBackend.js +209 -0
  36. package/src/compute/WebGpuBackend.js +630 -0
  37. package/src/compute/index.js +5 -0
  38. package/src/compute/selectBackend.js +85 -0
  39. package/src/index.js +3403 -3371
  40. package/src/kernelExpr.js +80 -5
  41. package/src/workspace/parts/DiagramEditorPart.js +3 -1
  42. package/src/workspace/parts/ViewsPart.js +106 -0
@@ -2907,6 +2907,7 @@ new RectangleTemplate('r1', 'Billing', { fontFamily: 'Georgia', fontSize: '14pt'
2907
2907
  | Boxes | `RectangleTemplate`, `RoundedRectangleTemplate`, `StadiumTemplate`, `PredefinedProcessTemplate`, `ClassTemplate`, `ComponentTemplate`, `SwimlaneTemplate`, `PackageTemplate`, `BoxTemplate` (3D) |
2908
2908
  | Round | `CircleTemplate`, `EllipseTemplate`, `FinalStateTemplate`, `CloudTemplate` |
2909
2909
  | Polygons | `DiamondTemplate`, `TriangleTemplate`, `PentagonTemplate`, `HexagonTemplate`, `OctagonTemplate`, `StarTemplate`, `ParallelogramTemplate`, `TrapezoidTemplate`, `ManualInputTemplate` |
2910
+ | C4 Architecture | `C4PersonTemplate`, `C4SystemTemplate`, `C4ContainerTemplate`, `C4ComponentTemplate`, `C4BoundaryTemplate` |
2910
2911
  | Other | `CylinderTemplate`, `DocumentTemplate`, `NoteTemplate`, `DataStoreTemplate`, `ActorTemplate`, `LifelineTemplate` |
2911
2912
 
2912
2913
  A template draws its outline in `shapeElements(doc)`, or in `points()` when it extends
@@ -2939,6 +2940,19 @@ empty canvas, or on the shape at the other end, cancels. This is one undo step
2939
2940
 
2940
2941
  **Container nesting and hierarchy.** Shapes such as `PackageTemplate`, `SwimlaneTemplate`, and `BoxTemplate` are containers (`isContainer = true`). When an element is dropped inside a container (`nestsOnDrop(parentId, childId)`), it is parented via `NestElementCommand` (setting `child.parentId`). Moving a container recursively moves all nested descendants (`moversFor(templateId)`). Containers can automatically expand their bounds to fit their contents via `fitNesting(changes, { padding })`. Ancestor loops are prevented (`isAncestor(childId, parentId)`).
2941
2942
 
2943
+ **C4 Model notation & architecture diagrams.** The `'c4-architecture'` category in `DIAGRAM_TYPES` (and `editor.setPaletteCategory('c4-architecture')`) provides first-class support for Simon Brown's C4 model across Context, Container, Component, and Code levels:
2944
+ - `C4PersonTemplate`: A human user, actor, persona, or stakeholder (`#08427b`, white text) with an integrated person silhouette badge and `[Person]` tagline.
2945
+ - `C4SystemTemplate`: Software system in focus (`#1168bd`, white text, `[Software System]`) or external software systems (`external: true`, `#999999`, `[Software System: External]`).
2946
+ - `C4ContainerTemplate`: Applications, web services, or microservices (`#438dd5`, white text, `[Container: <technology>]`), and database datastores (`containerType: 'database'`, cylinder icon).
2947
+ - `C4ComponentTemplate`: Modular components, controllers, and services (`#85bbf0`, dark text, `[Component: <technology>]`) with the UML component glyph.
2948
+ - `C4BoundaryTemplate`: Enterprise, system, or container scope boundaries (`isContainer = true`, dashed border `#707070`, bracketed header `[<boundaryType> Boundary]`) that enclose nested child elements.
2949
+
2950
+ ```javascript
2951
+ const person = new C4PersonTemplate('p1', 'Customer', { description: 'Personal banking customer' });
2952
+ const system = new C4SystemTemplate('s1', 'Internet Banking System', { description: 'Allows customers to view accounts and make payments' });
2953
+ const boundary = new C4BoundaryTemplate('b1', 'Big Bank plc', { boundaryType: 'Enterprise' });
2954
+ ```
2955
+
2942
2956
  **Line style levels.** A line's style resolves through `EdgeStyleCascade`: the line itself,
2943
2957
  then each level in `editor.edgeStyleLevels` (most specific first, `[{ key, label, style }]`),
2944
2958
  then the schema default. A value a level doesn't set is inherited. `EDGE_STYLE_PROPERTIES` is the
@@ -3506,7 +3520,7 @@ drawn with this library's charts. It reads decisionengine's JSON, not decisionen
3506
3520
  document
3507
3521
 
3508
3522
  ```js
3509
- { kind: 'sadis-workbench', report, plan?, before? } // report.toJSON(), plan.toJSON(), an earlier report.toJSON()
3523
+ { kind: 'sadis-workbench', report, plan?, before?, graph? } // report.toJSON(), plan.toJSON(), an earlier report.toJSON(), the scored graph
3510
3524
  ```
3511
3525
 
3512
3526
  which decisionengine's SADIS Explorer downloads ("Download for the Decision Workbench").
@@ -3525,6 +3539,7 @@ for (const view of workbench.views()) {
3525
3539
  |---|---|---|
3526
3540
  | Executive | a report | `kpi-tile` (what to build next; half the value delivered by), `stackedBar` (score part by part), `line` (value delivered by period) |
3527
3541
  | Architecture | a report | `scatter` (visible rating against basin position: foundations sit high and left), `lollipop` (share each frees) |
3542
+ | Explore | the scored graph | `graph` diagram of the first candidate's neighbourhood: its ways up to each objective it reaches (shortest, along contributions) and what waits on it (depends on it, or is blocked by it); notes spell each way out. Walked with `NavigatorModel.forGraph` |
3528
3543
  | Delivery | a plan | `gantt` (the plan on a calendar), `stackedBar` (capacity used per period, per team when planned by team) |
3529
3544
  | Portfolio | a report with perspectives | `heatmap` (rank by perspective), `dumbbell` (best and worst rank, most contested first) |
3530
3545
  | Replay | an earlier report | `slopegraph` (rank then → now); notes say what entered, left (and why) and moved |
@@ -3682,7 +3697,7 @@ nav.back(); // returns to 'billing-app'
3682
3697
  const paths = nav.findPaths('billing-app', 'ledger-db', 5);
3683
3698
 
3684
3699
  // Trace transitive downstream or upstream impact:
3685
- const impact = nav.findImpact('billing-app', { maxDepth: 4, direction: 'outgoing' });
3700
+ const impact = nav.traceImpact('billing-app', { maxDepth: 4, direction: 'outgoing' });
3686
3701
 
3687
3702
  // 2. Build or render the Navigator UI panel:
3688
3703
  const panel = new NavigatorPanel({
@@ -3697,7 +3712,26 @@ const panel = new NavigatorPanel({
3697
3712
  - **Cycle detection and path tracking:** Tracks visited nodes along active branches to prevent infinite recursion in cyclic relationship graphs, annotating cycles in tree outputs.
3698
3713
  - **Directional filters:** Traversal can follow outgoing relationships, incoming relationships, or bidirectional connections (`setDirection('outgoing' | 'incoming' | 'both')`).
3699
3714
  - **History stack:** Maintains full forward and backward navigation stacks (`focus(id)`, `back()`, `forward()`, `canGoBack`, `canGoForward`).
3700
- - **Impact and path finding:** Built-in BFS/DFS algorithms find connecting pathways (`findPaths(fromId, toId, maxDepth)`) and downstream dependency cascades (`findImpact(elementId, { maxDepth, direction })`).
3715
+ - **Impact and path finding:** Built-in BFS/DFS algorithms find connecting pathways (`findPaths(fromId, toId, maxDepth)`) and downstream dependency cascades (`traceImpact(elementId, { maxDepth, direction })`).
3716
+
3717
+
3718
+ **Any graph, not only ArchiMate.** `NavigatorModel` reads its graph only through four questions —
3719
+ `getElement(id)`, `relationshipsFor(id)` (`{ incoming, outgoing }`, each `{ id, source, target, kind }`),
3720
+ `viewsContaining(id)` and `layerOf(element)` — the `GraphSource` interface
3721
+ (`src/charting/graph/GraphSource.js`). `ArchimateModel` answers them already (its layers from
3722
+ `treeLayerOf`); `DocumentGraph` answers them for a graph document, `{ nodes: [{ id, labels, props }],
3723
+ edges: [{ from, to, type, props }] }` — sqlengine's graphs and decisionengine's portfolios — a node's type its
3724
+ first label, its layer its type:
3725
+
3726
+ ```javascript
3727
+ import { NavigatorModel, DocumentGraph } from '@peter.prib/renderengine';
3728
+ const nav = NavigatorModel.forGraph(graphDocument, { rootId: 'cap-semantic-registry' });
3729
+ nav.findPaths('cap-semantic-registry', 'obj-trusted-data', { direction: 'outgoing', allowedKinds: ['ENABLES', 'SUPPORTS'] });
3730
+ nav.traceImpact('cap-semantic-registry', { direction: 'incoming', allowedKinds: ['DEPENDS_ON'] });
3731
+ ```
3732
+
3733
+ Why an interface rather than a second walker: traversal is the same for every graph; only these reads
3734
+ differ, so each kind of graph answers them and the one walker (paths, impact, history, the tree) serves all.
3701
3735
 
3702
3736
  ## Templated Reports (`TemplatedReport`)
3703
3737
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peter.prib/renderengine",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "ArchiMate modeller, data modeller, diagram editor, OLAP cube & pivot chart studio, and 50+ type chart library for the browser, with no runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,6 +1,7 @@
1
1
  import { ArchimateNotation } from './archimate.js';
2
2
  import { archiElementType, typeProfileId, typeDisplayName, ARCHI_TYPE_FEATURE, ARCHI_TYPE_PROFILE_PREFIX } from './archimateArchiTypes.js';
3
- import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
3
+ import { ArchimateModel } from './charting/diagrams/ArchimateModel.js';
4
+ import { ArchimateSpecializationRegistry } from './archimateSpecializations.js';
4
5
  import './charting/diagrams/CanvasView.js'; // registers the canvas view type
5
6
  import './charting/diagrams/SketchView.js'; // registers the sketch view type
6
7
  import { ElementInstance } from './charting/diagrams/ElementInstance.js';
@@ -720,7 +721,39 @@ function _childText(el, tagName) {
720
721
  * @param {Object} archiModel - parsed Archi XML model
721
722
  * @returns {ArchimateModel} with one view per original view (elementIds track which elements appear in each view)
722
723
  * Now applies visual bounds/waypoints per view. */
723
- export function toArchimateModel(archiModel) {
724
+ /** Register any model-level Archi specializations (<profile>) with ArchimateSpecializationRegistry. @private */
725
+ function _registerModelProfiles(archiModel) {
726
+ const profilesById = new Map();
727
+ for (const e of archiModel?.archi?.elements || []) {
728
+ if (e.tag === 'profile' && !String(e.attributes?.id ?? '').startsWith(ARCHI_TYPE_PROFILE_PREFIX)) {
729
+ const id = e.attributes?.id || e.attributes?.name;
730
+ const name = e.attributes?.name || e.attributes?.id;
731
+ const conceptType = e.attributes?.conceptType;
732
+ const imagePath = e.attributes?.imagePath;
733
+ const archiImagePath = e.attributes?.archiImagePath;
734
+ const profileDef = {
735
+ id,
736
+ name,
737
+ conceptType,
738
+ baseTypes: conceptType ? [_toKebabType(conceptType)] : [],
739
+ iconUrl: imagePath || undefined,
740
+ archiImagePath,
741
+ };
742
+ if (id) profilesById.set(id, profileDef);
743
+ if (name) profilesById.set(name, profileDef);
744
+ ArchimateSpecializationRegistry.register({
745
+ id,
746
+ name,
747
+ baseTypes: profileDef.baseTypes,
748
+ iconUrl: profileDef.iconUrl,
749
+ });
750
+ }
751
+ }
752
+ return profilesById;
753
+ }
754
+
755
+ export function toArchimateModel(archiModel) {
756
+ const profilesById = _registerModelProfiles(archiModel);
724
757
  const elements = [];
725
758
  const relationships = [];
726
759
  const folders = [];
@@ -731,7 +764,7 @@ export function toArchimateModel(archiModel) {
731
764
 
732
765
  // Map all elements first
733
766
  for (const el of archiModel.elements || []) {
734
- const mapped = _mapElement(el);
767
+ const mapped = _mapElement(el, profilesById);
735
768
  elementById.set(el.id, mapped);
736
769
  elements.push(mapped);
737
770
  }
@@ -978,7 +1011,8 @@ function _viewInstances(archiView, { elementById, relationshipById }) {
978
1011
  * Returns `{ elements, relationships, options }` ready for `archimateDiagram()`.
979
1012
  * If `viewId` is omitted, the first view is used.
980
1013
  * Kept as original for backward compatibility: applies visual bounds/features and handles all view visuals. */
981
- export function toRenderEngineModel(archiModel, viewId) {
1014
+ export function toRenderEngineModel(archiModel, viewId) {
1015
+ const profilesById = _registerModelProfiles(archiModel);
982
1016
  const view = viewId
983
1017
  ? archiModel.views.find(v => v.id === viewId)
984
1018
  : archiModel.views[0];
@@ -990,7 +1024,7 @@ export function toRenderEngineModel(archiModel, viewId) {
990
1024
  const relationshipById = new Map();
991
1025
 
992
1026
  for (const el of archiModel.elements || []) {
993
- const mapped = _mapElement(el);
1027
+ const mapped = _mapElement(el, profilesById);
994
1028
  elementById.set(el.id, mapped);
995
1029
  elements.push(mapped);
996
1030
  }
@@ -1020,7 +1054,7 @@ export function toRenderEngineModel(archiModel, viewId) {
1020
1054
  return { elements, relationships, folders, folderLayout, elementFolders, options };
1021
1055
  }
1022
1056
 
1023
- function _mapElement(el) {
1057
+ function _mapElement(el, profilesById = null) {
1024
1058
  const archiType = _toKebabType(el.type);
1025
1059
  const layer = ArchimateNotation.elementLayer({ type: archiType });
1026
1060
  // A type Archi has no class for was written as the nearest one, with its own kept as a feature.
@@ -1032,6 +1066,21 @@ function _mapElement(el) {
1032
1066
  layer: layer !== 'other' ? layer : undefined,
1033
1067
  documentation: el.documentation || undefined,
1034
1068
  };
1069
+ const profileAttr = el.archi?.attributes?.profiles || el.attributes?.profiles;
1070
+ if (profileAttr && !profileAttr.startsWith(ARCHI_TYPE_PROFILE_PREFIX)) {
1071
+ const firstId = profileAttr.trim().split(/\s+/)[0];
1072
+ const profileDef = profilesById?.get(firstId) || ArchimateSpecializationRegistry.resolve(firstId);
1073
+ mapped.specialization = profileDef?.name || firstId;
1074
+ if (profileDef?.iconUrl && !mapped.iconUrl) {
1075
+ mapped.iconUrl = profileDef.iconUrl;
1076
+ }
1077
+ } else if (el.specialization) {
1078
+ mapped.specialization = el.specialization;
1079
+ const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
1080
+ if (spec?.iconUrl && !mapped.iconUrl) {
1081
+ mapped.iconUrl = spec.iconUrl;
1082
+ }
1083
+ }
1035
1084
  const props = _propertiesToTypedArray(el.properties);
1036
1085
  if (props) mapped.properties = props;
1037
1086
  if (el.archiFolder) mapped.archiFolder = el.archiFolder;
@@ -1388,15 +1437,56 @@ export function toArchiExport(model) {
1388
1437
  archi: view.archi,
1389
1438
  children: fromArchimateView(view),
1390
1439
  }));
1440
+ const archiElements = [...(model.archi?.elements ?? [])];
1441
+ const elements = model.elements().map(el => {
1442
+ if (!el.specialization) return el;
1443
+ const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
1444
+ if (!spec) return el;
1445
+ const specId = spec.id.startsWith('id-') ? spec.id : `id-prof-${spec.id}`;
1446
+ const existing = archiElements.find(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name));
1447
+ if (!existing) {
1448
+ archiElements.push({
1449
+ tag: 'profile',
1450
+ attributes: {
1451
+ id: specId,
1452
+ name: spec.name,
1453
+ conceptType: _toPascalType(el.type),
1454
+ ...(spec.iconUrl && { imagePath: spec.iconUrl }),
1455
+ },
1456
+ });
1457
+ } else if (spec.iconUrl && !existing.attributes?.imagePath) {
1458
+ existing.attributes = { ...existing.attributes, imagePath: spec.iconUrl };
1459
+ }
1460
+ if (!el.archi?.attributes?.profiles) {
1461
+ return {
1462
+ ...el,
1463
+ archi: {
1464
+ ...(el.archi ?? {}),
1465
+ attributes: {
1466
+ ...(el.archi?.attributes ?? {}),
1467
+ profiles: specId,
1468
+ },
1469
+ },
1470
+ };
1471
+ }
1472
+ return el;
1473
+ });
1474
+
1475
+ const archi = (model.archi || archiElements.length) ? {
1476
+ ...(model.archi ?? {}),
1477
+ attributes: { ...(model.archi?.attributes ?? {}) },
1478
+ elements: archiElements,
1479
+ } : null;
1480
+
1391
1481
  return [{
1392
1482
  id: model.id || 'model-1',
1393
1483
  name: model.name || 'ArchiMate Model',
1394
1484
  version: '5.0.0',
1395
1485
  documentation: model.documentation,
1396
1486
  properties: model.properties,
1397
- archi: model.archi,
1487
+ archi,
1398
1488
  emptyViewFolders: model.emptyViewFolders ?? [],
1399
- elements: model.elements(),
1489
+ elements,
1400
1490
  relationships: model.relationships(),
1401
1491
  }, views];
1402
1492
  }
@@ -1510,6 +1600,20 @@ export function toArchiXML(model, views) {
1510
1600
  if (original && !el.archi?.attributes?.profiles) {
1511
1601
  profile = typeProfileId(original);
1512
1602
  typeProfiles.set(profile, { name: typeDisplayName(original), conceptType: _toPascalType(type) });
1603
+ } else if (el.specialization && !el.archi?.attributes?.profiles) {
1604
+ const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
1605
+ if (spec) {
1606
+ const specId = spec.id.startsWith('id-') ? spec.id : ('id-prof-' + spec.id);
1607
+ profile = specId;
1608
+ const existing = (model.archi?.elements ?? []).find(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name));
1609
+ if (!existing && !typeProfiles.has(specId)) {
1610
+ typeProfiles.set(specId, {
1611
+ name: spec.name,
1612
+ conceptType: _toPascalType(type),
1613
+ ...(spec.iconUrl && { imagePath: spec.iconUrl }),
1614
+ });
1615
+ }
1616
+ }
1513
1617
  }
1514
1618
  byFolder.get(folder.type).push({ el, type, original, profile });
1515
1619
  }
@@ -1540,8 +1644,8 @@ export function toArchiXML(model, views) {
1540
1644
 
1541
1645
  // What else the model carried (profiles, ...), its own properties, then its documentation (Archi's "purpose").
1542
1646
  lines.push(..._extraElementLines(model.archi?.elements, ' '));
1543
- for (const [id, { name, conceptType }] of typeProfiles) {
1544
- lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"/>`);
1647
+ for (const [id, { name, conceptType, imagePath }] of typeProfiles) {
1648
+ lines.push(` <profile name="${_escapeAttr(name)}" id="${id}" conceptType="${conceptType}"${imagePath ? ` imagePath="${_escapeAttr(imagePath)}"` : ''}/>`);
1545
1649
  }
1546
1650
  lines.push(..._propertyLines(model.properties, ' '));
1547
1651
  if (model.documentation) lines.push(` <purpose>${_escapeXml(model.documentation)}</purpose>`);
@@ -1,4 +1,5 @@
1
- import { parseArchiModel, toArchiXML } from './archimateArchiFormat.js';
1
+ import { parseArchiModel, toArchiXML, toPascalType } from './archimateArchiFormat.js';
2
+ import { ArchimateSpecializationRegistry } from './archimateSpecializations.js';
2
3
  import { ZipArchive } from './zip/ZipArchive.js';
3
4
 
4
5
  /**
@@ -95,15 +96,37 @@ export function toArchiFile(model, views) {
95
96
  _eachVisual(copies, (visual) => registerPicture(visual));
96
97
 
97
98
  let modelForXml = model;
98
- if (model?.archi?.elements?.some(e => e.attributes?.imagePath?.startsWith('data:'))) {
99
+ let archiElements = [...(model?.archi?.elements || [])];
100
+ for (const el of model?.elements || []) {
101
+ if (el.specialization) {
102
+ const spec = ArchimateSpecializationRegistry.resolve(el.specialization);
103
+ if (spec?.iconUrl?.startsWith('data:')) {
104
+ const specId = spec.id.startsWith('id-') ? spec.id : ('id-prof-' + spec.id);
105
+ if (!archiElements.some(e => e.tag === 'profile' && (e.attributes?.id === specId || e.attributes?.name === spec.name))) {
106
+ archiElements.push({
107
+ tag: 'profile',
108
+ attributes: {
109
+ id: specId,
110
+ name: spec.name,
111
+ conceptType: toPascalType(el.type),
112
+ imagePath: spec.iconUrl,
113
+ },
114
+ });
115
+ }
116
+ }
117
+ }
118
+ }
119
+ if (archiElements.some(e => e.attributes?.imagePath?.startsWith('data:'))) {
99
120
  modelForXml = {
100
121
  ...model,
101
122
  archi: {
102
- ...model.archi,
103
- elements: model.archi.elements.map(e => {
123
+ ...(model?.archi ?? {}),
124
+ attributes: { ...(model?.archi?.attributes ?? {}) },
125
+ elements: archiElements.map(e => {
104
126
  if (!e.attributes?.imagePath?.startsWith('data:')) return e;
105
127
  const clonedAttr = { ...e.attributes };
106
128
  registerPicture(clonedAttr);
129
+ delete clonedAttr.archiImagePath;
107
130
  return { ...e, attributes: clonedAttr };
108
131
  }),
109
132
  },
@@ -144,12 +144,20 @@ function _organizationPaths(root) {
144
144
  return { paths, emptyViewFolders };
145
145
  }
146
146
 
147
+ function _asPropertyPairs(pairs) {
148
+ const list = pairs.map(p => ({ ...p }));
149
+ for (const { key, value } of pairs) {
150
+ if (!(key in list)) list[key] = value;
151
+ }
152
+ return list;
153
+ }
154
+
147
155
  /** An element as parseArchiModel gives it (type: Archi's class name). @private */
148
156
  function _parseElement(el, propertyPairs) {
149
157
  const xsiType = _xsiType(el);
150
- const properties = Object.fromEntries(propertyPairs.map(({ key, value }) => [key, value]));
151
- const ownType = properties[EXCHANGE_TYPE_PROPERTY];
152
- delete properties[EXCHANGE_TYPE_PROPERTY];
158
+ const ownType = propertyPairs.find(p => p.key === EXCHANGE_TYPE_PROPERTY)?.value;
159
+ const filteredPairs = propertyPairs.filter(p => p.key !== EXCHANGE_TYPE_PROPERTY);
160
+ const properties = _asPropertyPairs(filteredPairs);
153
161
  const attributes = {};
154
162
  const elements = [];
155
163
  if (JUNCTION_TYPES[xsiType]) attributes.type = 'or';
@@ -180,7 +188,7 @@ function _parseRelationship(el, propertyPairs) {
180
188
  target: el.getAttribute('target'),
181
189
  ...(el.getAttribute('isDirected') === 'true' && { directed: true }),
182
190
  documentation: _text(el, 'documentation'),
183
- properties: Object.fromEntries(propertyPairs.map(({ key, value }) => [key, value])),
191
+ properties: _asPropertyPairs(propertyPairs),
184
192
  ...(Object.keys(attributes).length && { archi: { attributes, elements: [] } }),
185
193
  };
186
194
  }
@@ -99,6 +99,12 @@ export class ArchimateSpecializationRegistry {
99
99
  /** Register or replace a specialization. */
100
100
  static register(spec) {
101
101
  const s = new ArchimateSpecialization(spec);
102
+ const existingByName = this.list().find(e => e.name === s.name && e.id !== s.id);
103
+ if (existingByName) {
104
+ if (s.iconUrl && !existingByName.iconUrl) existingByName.iconUrl = s.iconUrl;
105
+ if (s.icon && !existingByName.icon) existingByName.icon = s.icon;
106
+ if (s.color && !existingByName.color) existingByName.color = s.color;
107
+ }
102
108
  this._store.set(s.id, s);
103
109
  return s;
104
110
  }
@@ -116,7 +122,11 @@ export class ArchimateSpecializationRegistry {
116
122
  /** Look up by id or by exact name. */
117
123
  static resolve(idOrName) {
118
124
  if (!idOrName) return undefined;
119
- return this._store.get(idOrName) || this.list().find(s => s.name === idOrName);
125
+ const byId = this._store.get(idOrName);
126
+ if (byId) return byId;
127
+ const matches = this.list().filter(s => s.name === idOrName);
128
+ if (!matches.length) return undefined;
129
+ return matches.find(s => s.iconUrl || s.icon) || matches[0];
120
130
  }
121
131
 
122
132
  /** All registered specializations. */
@@ -91,12 +91,13 @@ export const CAMERA_MODE = {
91
91
  export const CHART_CATALOG = [
92
92
  {
93
93
  key: 'bar', label: 'Bar', method: 'barChart', category: '2d',
94
- coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
94
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.GROUPED_FIELD, camera: CAMERA_MODE.NONE,
95
95
  axes: [
96
96
  { role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
97
97
  { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y_LENGTH, required: true },
98
+ { role: 'group', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
98
99
  ],
99
- dataShape: 'Array<{label, value}>',
100
+ dataShape: 'Array<{label, value}> — optional `colorCol` (field name or compiled formula) colors bars by group',
100
101
  streamShape: 'flat',
101
102
  },
102
103
  {
@@ -609,14 +610,15 @@ export const CHART_CATALOG = [
609
610
  },
610
611
  {
611
612
  key: 'lollipop', label: 'Lollipop', method: 'lollipopChart', category: '2d',
612
- coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.PALETTE, camera: CAMERA_MODE.NONE,
613
+ coordSystem: COORD_SYSTEM.CARTESIAN, title: true, legend: true, colorMode: COLOR_MODE.GROUPED_FIELD, camera: CAMERA_MODE.NONE,
613
614
  seriesCapable: true,
614
615
  axes: [
615
616
  { role: 'category', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.POSITION_X, required: true },
616
617
  { role: 'value', kind: AXIS_KIND.NUMERIC, channel: ENCODING_CHANNEL.POSITION_Y, required: true },
617
618
  { role: 'series', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
619
+ { role: 'group', kind: AXIS_KIND.CATEGORICAL, channel: ENCODING_CHANNEL.COLOR, required: false },
618
620
  ],
619
- dataShape: 'Array<{label, value}> or multi-series Array<{name, values:Array<{label,value}>}> — stem-and-dot alternative to bars',
621
+ dataShape: 'Array<{label, value}> or multi-series Array<{name, values:Array<{label,value}>}> — stem-and-dot alternative to bars; `colorCol` supported on 1D data',
620
622
  streamShape: 'flat',
621
623
  },
622
624
  {
@@ -141,11 +141,6 @@ export class ArchimateDiagramEditor extends DiagramEditor {
141
141
 
142
142
  // Enable tree drag-drop (delayed to allow DOM to be ready)
143
143
  setTimeout(() => this.enableTreeDragDrop(), 100);
144
-
145
- // Wire controls if not using auto-toolbar
146
- if (!this.features) {
147
- this.wireControls();
148
- }
149
144
  }
150
145
 
151
146
  /**
@@ -1366,19 +1361,10 @@ export class ArchimateDiagramEditor extends DiagramEditor {
1366
1361
  }
1367
1362
 
1368
1363
  /**
1369
- * Get a control value from the DOM.
1364
+ * Get a control value.
1370
1365
  */
1371
1366
  getOption(controlId, defaultValue) {
1372
- if (this._options?.has(controlId)) return super.getOption(controlId, defaultValue);
1373
- // Hosts that bring their own controls (wireControls) keep them in the DOM.
1374
- try {
1375
- const el = this.document.getElementById(controlId);
1376
- if (!el) return defaultValue;
1377
- if (el.type === 'checkbox') return el.checked;
1378
- return el.value || defaultValue;
1379
- } catch {
1380
- return defaultValue;
1381
- }
1367
+ return super.getOption(controlId, defaultValue);
1382
1368
  }
1383
1369
 
1384
1370
  /** Save specializations to localStorage. */
@@ -1572,98 +1558,9 @@ export class ArchimateDiagramEditor extends DiagramEditor {
1572
1558
  }
1573
1559
 
1574
1560
  /**
1575
- * Wire up all control event listeners (version, viewpoint, distinct shapes, etc.).
1576
- * Triggers render on change. Optional: provide custom handler functions.
1561
+ * @deprecated Legacy control wiring. Controls are now auto-rendered into the toolbar via `features`.
1577
1562
  */
1578
- wireControls(options = {}) {
1579
- const {
1580
- onVersionChange,
1581
- onViewpointChange,
1582
- onViewpointModeChange,
1583
- onDistinctShapesChange,
1584
- onShowDerivedChange,
1585
- onManageViewpoints,
1586
- onManageSpecializations,
1587
- populateViewpointSelect,
1588
- } = options;
1589
-
1590
- // Version selector
1591
- const versionEl = this.document.getElementById('archimate-version');
1592
- if (versionEl) {
1593
- versionEl.addEventListener('change', () => {
1594
- if (onVersionChange) onVersionChange();
1595
- this.render();
1596
- });
1597
- }
1598
-
1599
- // Viewpoint selector and mode
1600
- const viewpointEl = this.document.getElementById('archimate-viewpoint');
1601
- const viewpointModeEl = this.document.getElementById('archimate-viewpoint-mode');
1602
- if (viewpointEl) {
1603
- viewpointEl.addEventListener('change', () => {
1604
- if (onViewpointChange) onViewpointChange();
1605
- this.render();
1606
- });
1607
- }
1608
- if (viewpointModeEl) {
1609
- viewpointModeEl.addEventListener('change', () => {
1610
- if (onViewpointModeChange) onViewpointModeChange();
1611
- this.render();
1612
- });
1613
- }
1614
-
1615
- // Distinct shapes checkbox
1616
- const distinctShapesEl = this.document.getElementById('archimate-distinct-shapes');
1617
- if (distinctShapesEl) {
1618
- distinctShapesEl.addEventListener('change', () => {
1619
- if (onDistinctShapesChange) onDistinctShapesChange();
1620
- this.render();
1621
- });
1622
- }
1623
-
1624
- // Show derived relationships checkbox
1625
- const showDerivedEl = this.document.getElementById('archimate-show-derived');
1626
- if (showDerivedEl) {
1627
- showDerivedEl.addEventListener('change', () => {
1628
- if (onShowDerivedChange) onShowDerivedChange();
1629
- this.render();
1630
- });
1631
- }
1632
-
1633
- // Manage viewpoints dialog
1634
- const manageVpEl = this.document.getElementById('archimate-manage-viewpoints');
1635
- if (manageVpEl && onManageViewpoints) {
1636
- manageVpEl.addEventListener('click', onManageViewpoints);
1637
- }
1638
-
1639
- // Manage specializations dialog
1640
- const manageSpecEl = this.document.getElementById('archimate-manage-specializations');
1641
- if (manageSpecEl && onManageSpecializations) {
1642
- manageSpecEl.addEventListener('click', onManageSpecializations);
1643
- }
1644
-
1645
- // Settings dialog
1646
- const settingsEl = this.document.getElementById('archimate-settings');
1647
- if (settingsEl) {
1648
- settingsEl.addEventListener('click', () => {
1649
- this.workspace.showGlobalSettingsDialog(this.document, this.settingsManager, 'default');
1650
- });
1651
- }
1652
-
1653
- // Filter controls
1654
- const filterInput = this.document.getElementById('archimate-filter');
1655
- const filterClearEl = this.document.getElementById('archimate-filter-clear');
1656
- if (filterInput && this.currentEngine) {
1657
- const applyFilter = () => {
1658
- this.currentEngine.filterGraphNodes(filterInput.value);
1659
- };
1660
- filterInput.addEventListener('input', applyFilter);
1661
- if (filterClearEl) {
1662
- filterClearEl.addEventListener('click', () => {
1663
- filterInput.value = '';
1664
- applyFilter();
1665
- });
1666
- }
1667
- }
1563
+ wireControls(_options = {}) {
1564
+ // No-op: toolbar and controls are managed via features and MenuBar
1668
1565
  }
1669
1566
  }
@@ -12,7 +12,8 @@ import {
12
12
  EllipseTemplate, DiamondTemplate, TriangleTemplate, PentagonTemplate, HexagonTemplate,
13
13
  OctagonTemplate, StarTemplate, ParallelogramTemplate, TrapezoidTemplate, ManualInputTemplate,
14
14
  CylinderTemplate, DocumentTemplate, NoteTemplate, CloudTemplate, ActorTemplate,
15
- PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate,
15
+ PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate,
16
+ C4PersonTemplate, C4SystemTemplate, C4ContainerTemplate, C4ComponentTemplate, C4BoundaryTemplate,
16
17
  } from './templates/index.js';
17
18
 
18
19
  const C = {
@@ -202,6 +203,24 @@ export const DIAGRAM_TYPES = {
202
203
  ],
203
204
  },
204
205
 
206
+ 'c4-architecture': {
207
+ name: 'C4 Architecture',
208
+ group: 'Architecture',
209
+ description: 'Hierarchical software architecture: Person, System, Container, Component, and Boundaries',
210
+ templates: [
211
+ shape('c4-person', 'Person', C4PersonTemplate, { fillColor: '#08427b', textColor: '#ffffff' }, 'A human user, actor, persona, or stakeholder interacting with systems'),
212
+ shape('c4-system', 'Software System', C4SystemTemplate, { fillColor: '#1168bd', textColor: '#ffffff' }, 'The software system in focus delivering business value'),
213
+ shape('c4-system-external', 'External System', C4SystemTemplate, { external: true, fillColor: '#999999', textColor: '#ffffff' }, 'An external software system outside the boundary of change'),
214
+ shape('c4-container', 'Container', C4ContainerTemplate, { fillColor: '#438dd5', textColor: '#ffffff' }, 'An application, web service, mobile app, or microservice'),
215
+ shape('c4-database', 'Database Container', C4ContainerTemplate, { containerType: 'database', fillColor: '#438dd5', textColor: '#ffffff' }, 'A database, persistence store, cache, or datastore container'),
216
+ shape('c4-component', 'Component', C4ComponentTemplate, { fillColor: '#85bbf0', textColor: '#000000' }, 'A modular component, controller, or service within a container'),
217
+ shape('c4-system-boundary', 'System Boundary', C4BoundaryTemplate, { boundaryType: 'System', zIndex: -1 }, 'Boundary box enclosing containers belonging to a Software System'),
218
+ shape('c4-enterprise-boundary', 'Enterprise Boundary', C4BoundaryTemplate, { boundaryType: 'Enterprise', zIndex: -2 }, 'Boundary box enclosing all systems belonging to an enterprise'),
219
+ shape('c4-container-boundary', 'Container Boundary', C4BoundaryTemplate, { boundaryType: 'Container', zIndex: -1 }, 'Boundary box enclosing components belonging to a Container'),
220
+ note,
221
+ ],
222
+ },
223
+
205
224
  'er-diagram': {
206
225
  name: 'Entity-Relationship',
207
226
  group: 'Data',