@peter.prib/renderengine 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +480 -302
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49217 -37484
  4. package/dist/renderengine.umd.js +24899 -13093
  5. package/documentation/REFERENCE.md +68 -1
  6. package/package.json +13 -3
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +269 -3
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +7 -2
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +142 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/ask/AskBar.js +388 -0
  34. package/src/charting/ask/AskView.js +400 -0
  35. package/src/charting/charts/AlluvialDiagram.js +311 -0
  36. package/src/charting/charts/AreaChart.js +69 -15
  37. package/src/charting/charts/BarChart.js +38 -3
  38. package/src/charting/charts/BarChart3D.js +6 -4
  39. package/src/charting/charts/BarChart3DOrbit.js +3 -1
  40. package/src/charting/charts/BarChartBase.js +26 -6
  41. package/src/charting/charts/BeeswarmChart.js +164 -0
  42. package/src/charting/charts/BoxPlotChart.js +25 -3
  43. package/src/charting/charts/BubbleChart.js +4 -1
  44. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  45. package/src/charting/charts/CandlestickChart.js +172 -4
  46. package/src/charting/charts/ChordDiagram.js +107 -4
  47. package/src/charting/charts/CirclePackChart.js +81 -0
  48. package/src/charting/charts/ComboChart.js +233 -0
  49. package/src/charting/charts/ContourChart.js +310 -0
  50. package/src/charting/charts/DensityChart.js +1 -1
  51. package/src/charting/charts/DonutChart3D.js +13 -0
  52. package/src/charting/charts/DumbbellChart.js +217 -0
  53. package/src/charting/charts/EcdfChart.js +150 -0
  54. package/src/charting/charts/FunnelChart.js +5 -2
  55. package/src/charting/charts/GeoMapChart.js +132 -11
  56. package/src/charting/charts/GroupedBarChart.js +44 -3
  57. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  58. package/src/charting/charts/HeatmapChart.js +6 -3
  59. package/src/charting/charts/HexbinChart.js +137 -0
  60. package/src/charting/charts/HierarchyChart.js +77 -3
  61. package/src/charting/charts/HistogramChart.js +2 -2
  62. package/src/charting/charts/HorizonChart.js +105 -0
  63. package/src/charting/charts/IcicleChart.js +47 -0
  64. package/src/charting/charts/KagiChart.js +167 -0
  65. package/src/charting/charts/LineChart.js +74 -18
  66. package/src/charting/charts/LollipopChart.js +104 -0
  67. package/src/charting/charts/MarimekkoChart.js +75 -0
  68. package/src/charting/charts/OhlcChart.js +79 -0
  69. package/src/charting/charts/OutlineChart.js +10 -8
  70. package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
  71. package/src/charting/charts/ParetoChart.js +92 -0
  72. package/src/charting/charts/PieChart.js +195 -17
  73. package/src/charting/charts/PieChart3D.js +137 -0
  74. package/src/charting/charts/PipeChart.js +5 -2
  75. package/src/charting/charts/PointAndFigureChart.js +198 -0
  76. package/src/charting/charts/PolarAreaChart.js +189 -0
  77. package/src/charting/charts/PopulationPyramidChart.js +4 -4
  78. package/src/charting/charts/RadarChart.js +12 -5
  79. package/src/charting/charts/RadialBarChart.js +6 -3
  80. package/src/charting/charts/RenkoChart.js +178 -0
  81. package/src/charting/charts/RidgelineChart.js +113 -0
  82. package/src/charting/charts/SankeyDiagram.js +129 -6
  83. package/src/charting/charts/ScatterChart.js +19 -3
  84. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  85. package/src/charting/charts/SlopegraphChart.js +205 -0
  86. package/src/charting/charts/SparklineTableChart.js +166 -0
  87. package/src/charting/charts/StackedBarChart.js +125 -11
  88. package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
  89. package/src/charting/charts/StreamGraphChart.js +18 -3
  90. package/src/charting/charts/SunburstChart.js +28 -8
  91. package/src/charting/charts/SurfaceChart3D.js +157 -0
  92. package/src/charting/charts/TreemapChart.js +31 -11
  93. package/src/charting/charts/ViolinChart.js +20 -2
  94. package/src/charting/charts/WaterfallChart.js +2 -2
  95. package/src/charting/charts/WordCloudChart.js +2 -2
  96. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  97. package/src/charting/datamodel/DataLineage.js +150 -0
  98. package/src/charting/datamodel/DataModel.js +83 -1
  99. package/src/charting/datamodel/ModelMapping.js +87 -0
  100. package/src/charting/datamodel/SqlDdl.js +110 -85
  101. package/src/charting/datamodel/index.js +3 -0
  102. package/src/charting/decision/DecisionWorkbench.js +295 -0
  103. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  104. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  105. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  106. package/src/charting/diagrams/SketchView.js +121 -0
  107. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  108. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  109. package/src/charting/modelTree/NavigatorModel.js +410 -0
  110. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  111. package/src/charting/pivot/PivotChart.js +310 -13
  112. package/src/charting/pivot/PivotGridChart.js +322 -34
  113. package/src/charting/pivot/PivotView.js +626 -21
  114. package/src/charting/report/TemplatedReport.js +547 -0
  115. package/src/charting/templates/BoxTemplate.js +28 -27
  116. package/src/charting/templates/PackageTemplate.js +26 -25
  117. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  118. package/src/charting/templates/Template.js +219 -212
  119. package/src/cli.js +146 -0
  120. package/src/index.js +3370 -2899
  121. package/src/workspace/Workspace.js +126 -7
  122. package/src/workspace/archimateServices.js +4 -0
  123. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  124. package/src/workspace/styles.js +36 -1
@@ -5,18 +5,22 @@ import { HierarchyChart } from './HierarchyChart.js';
5
5
  /**
6
6
  * TreemapChart — rectangles nested/sized proportional to value (squarified
7
7
  * layout), the standard chart for "composition of a whole across a hierarchy".
8
+ * Supports breadcrumb drill-down.
8
9
  */
9
10
  export class TreemapChart extends HierarchyChart {
10
11
  static chartType = 'treemap';
11
12
 
12
13
  _render() {
13
- const data = this.data;
14
- const options = this.options;
14
+ const data = this._activeData();
15
+ const options = this.options || {};
15
16
  const box = ChartBox.fromRenderer(this.renderer);
16
- const { x = box.x, y = box.y, width = box.width, height = box.height, title } = options;
17
- const plotBox = { x, y, width, height };
17
+ let { x = box.x, y = box.y, width = box.width, height = box.height, title } = options;
18
18
 
19
- new ChartLabels({ title }).draw(this.renderer, plotBox);
19
+ new ChartLabels({ title }).draw(this.renderer, { x, y, width, height });
20
+
21
+ const crumbH = this._drawBreadcrumbs(this.renderer, { x, y, width });
22
+ y += crumbH;
23
+ height -= crumbH;
20
24
 
21
25
  const layout = Hierarchy.treemapLayout(data, x, y, width, height, 0, null, TreemapChart.HEADER_H);
22
26
  const textAnchor = this.renderer.centerTextAnchor;
@@ -27,20 +31,36 @@ export class TreemapChart extends HierarchyChart {
27
31
  const textColor = this._textColorForNode(topAncestor, depth);
28
32
 
29
33
  if (isLeaf) {
30
- this.renderer.rect(rx, ry, Math.max(0, rw), Math.max(0, rh), color, { color: '#fff', width: 1 }, {
34
+ this._datum(this.renderer.rect(rx, ry, Math.max(0, rw), Math.max(0, rh), color, { color: '#fff', width: 1 }, {
31
35
  tooltip: `${node.label}: ${this._formatNodeValue(node)}`,
32
- });
36
+ }), { label: node.label });
33
37
  if (rw > 30 && rh > 14) {
34
- this.renderer.text(node.label, rx + rw / 2, ry + rh / 2, {
38
+ const showVals = options.showValues && rh > 28;
39
+ const valText = typeof options.format === 'function' ? options.format(node.value) : this._formatNodeValue(node);
40
+ this.renderer.text(node.label, rx + rw / 2, ry + (showVals ? rh / 2 - 6 : rh / 2), {
35
41
  textAnchor, verticalAlign: 'middle', fontSize: '10px', fill: textColor, 'pointer-events': 'none',
36
42
  });
43
+ if (showVals) {
44
+ this.renderer.text(valText, rx + rw / 2, ry + rh / 2 + 6, {
45
+ textAnchor, verticalAlign: 'middle', fontSize: '9px', fill: textColor, opacity: 0.9, 'pointer-events': 'none',
46
+ });
47
+ }
37
48
  }
38
49
  } else {
39
50
  const headerH = Math.min(rh, TreemapChart.HEADER_H);
40
51
  this.renderer.rect(rx, ry, Math.max(0, rw), Math.max(0, rh), 'rgba(0,0,0,0.03)', { color: '#ddd', width: 1 });
41
- this.renderer.rect(rx, ry, Math.max(0, rw), headerH, color, null, {
42
- tooltip: `${node.label}: ${this._formatNodeValue(node)}`,
43
- });
52
+ const headerRect = this._datum(this.renderer.rect(rx, ry, Math.max(0, rw), headerH, color, null, {
53
+ cursor: options.drilldown !== false ? 'pointer' : 'default',
54
+ tooltip: `${node.label}: ${this._formatNodeValue(node)} (Click to drill down)`,
55
+ }), { label: node.label });
56
+
57
+ if (options.drilldown !== false && typeof headerRect?.addEventListener === 'function') {
58
+ headerRect.addEventListener('click', (e) => {
59
+ e.stopPropagation();
60
+ this.drillTo(node);
61
+ });
62
+ }
63
+
44
64
  if (rw > 30) {
45
65
  this.renderer.text(node.label, rx + 4, ry + headerH / 2, {
46
66
  textAnchor: 'start', verticalAlign: 'middle', fontSize: '9px', fontWeight: 'bold', fill: textColor, 'pointer-events': 'none',
@@ -18,6 +18,24 @@ export class ViolinChart extends GroupedDistributionChart {
18
18
  const color = this._palette.colorAt(i);
19
19
  const curve = Statistics.kernelDensityEstimate(d.values, { min: this._domain.vMin, max: this._domain.vMax });
20
20
  const maxDensity = Math.max(...curve.map(p => p.density)) || 1;
21
+
22
+ if (this._isHorizontal()) {
23
+ const cy = cx;
24
+ const slotHeight = slotWidth;
25
+ const halfHeight = slotHeight * 0.4;
26
+ const topPts = curve.map(p => ({ px: this._toX(p.x), py: cy - (p.density / maxDensity) * halfHeight }));
27
+ const bottomPts = curve.map(p => ({ px: this._toX(p.x), py: cy + (p.density / maxDensity) * halfHeight }));
28
+ const path = 'M' + topPts.map(p => `${p.px},${p.py}`).join(' L')
29
+ + ' L' + [...bottomPts].reverse().map(p => `${p.px},${p.py}`).join(' L') + ' Z';
30
+ this._datum(this.renderer.path(path, color, { color: '#333', width: 1 }, {
31
+ tooltip: `${d.label}: n=${d.values.length}`,
32
+ }), { label: d.label, series: d.label });
33
+
34
+ const median = Statistics.quantile([...d.values].sort((a, b) => a - b), 0.5);
35
+ this.renderer.path(`M${this._toX(median)},${cy - halfHeight} L${this._toX(median)},${cy + halfHeight}`, 'none', { color: '#fff', width: 2 });
36
+ return;
37
+ }
38
+
21
39
  const halfWidth = slotWidth * 0.4;
22
40
 
23
41
  // Mirrored silhouette: right edge low-to-high value, then left
@@ -27,9 +45,9 @@ export class ViolinChart extends GroupedDistributionChart {
27
45
  const leftPts = curve.map(p => ({ px: cx - (p.density / maxDensity) * halfWidth, py: this._toY(p.x) }));
28
46
  const path = 'M' + rightPts.map(p => `${p.px},${p.py}`).join(' L')
29
47
  + ' L' + [...leftPts].reverse().map(p => `${p.px},${p.py}`).join(' L') + ' Z';
30
- this.renderer.path(path, color, { color: '#333', width: 1 }, {
48
+ this._datum(this.renderer.path(path, color, { color: '#333', width: 1 }, {
31
49
  tooltip: `${d.label}: n=${d.values.length}`,
32
- });
50
+ }), { label: d.label, series: d.label });
33
51
 
34
52
  const median = Statistics.quantile([...d.values].sort((a, b) => a - b), 0.5);
35
53
  this.renderer.path(`M${cx - halfWidth},${this._toY(median)} L${cx + halfWidth},${this._toY(median)}`, 'none', { color: '#fff', width: 2 });
@@ -58,9 +58,9 @@ export class WaterfallChart extends Chart {
58
58
  const top = toY(Math.max(band.from, band.to));
59
59
  const bottom = toY(Math.min(band.from, band.to));
60
60
  const color = band.isTotal ? totalColor : (band.to >= band.from ? upColor : downColor);
61
- this.renderer.rect(barX, top, barWidth, Math.max(1, bottom - top), color, null, {
61
+ this._datum(this.renderer.rect(barX, top, barWidth, Math.max(1, bottom - top), color, null, {
62
62
  tooltip: `${d.label}: ${band.isTotal ? band.to : (d.value >= 0 ? '+' : '') + d.value} (total: ${Axis.formatValue(band.to)})`,
63
- });
63
+ }), { label: d.label });
64
64
  if (i % labelStep === 0) this.renderer.text(d.label, barX + barWidth / 2, y + height + 20, { textAnchor, fontSize: '10px' });
65
65
  });
66
66
  }
@@ -66,10 +66,10 @@ export class WordCloudChart extends Chart {
66
66
  if (!pos) return; // Could not place; skip.
67
67
  placed.push({ x: pos.x, y: pos.y, w: box.w, h: box.h });
68
68
  const color = palette.colorAt(i);
69
- this.renderer.text(d.text, pos.x, pos.y + fontSize * 0.35, {
69
+ this._datum(this.renderer.text(d.text, pos.x, pos.y + fontSize * 0.35, {
70
70
  textAnchor, fontSize: `${fontSize}px`, fill: color, fontFamily,
71
71
  tooltip: `${d.text}: ${d.value}`,
72
- });
72
+ }), { label: d.text });
73
73
  });
74
74
  }
75
75
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * ArchimateDataLink — connects data model concepts to ArchiMate model elements.
3
+ * Aligns with DMBOK2's Data Architecture and TOGAF/ArchiMate standards:
4
+ * - Conceptual concepts realise ArchiMate Business Objects (Business Layer)
5
+ * - Logical and physical entities realise ArchiMate Data Objects (Application Layer) or Artifacts (Technology Layer)
6
+ * - Service domains link to ArchiMate Business Services or Application Services
7
+ */
8
+ export class ArchimateDataLink {
9
+ /**
10
+ * @param {Object} spec
11
+ * @param {string} spec.dataModelEntityId - Entity id in DataModel
12
+ * @param {'conceptual'|'logical'|'physical'} [spec.level='conceptual']
13
+ * @param {string} spec.archimateElementId - ArchiMate element id
14
+ * @param {string} [spec.archimateType='business-object'] - 'business-object', 'data-object', 'artifact', etc.
15
+ * @param {string} [spec.archimateModelId]
16
+ * @param {string} [spec.notes]
17
+ */
18
+ constructor({
19
+ dataModelEntityId,
20
+ level = 'conceptual',
21
+ archimateElementId,
22
+ archimateType = 'business-object',
23
+ archimateModelId = null,
24
+ notes = '',
25
+ } = {}) {
26
+ if (!dataModelEntityId || !archimateElementId) {
27
+ throw new TypeError('ArchimateDataLink requires dataModelEntityId and archimateElementId');
28
+ }
29
+ this.dataModelEntityId = dataModelEntityId;
30
+ this.level = level;
31
+ this.archimateElementId = archimateElementId;
32
+ this.archimateType = archimateType;
33
+ this.archimateModelId = archimateModelId;
34
+ this.notes = notes;
35
+ }
36
+
37
+ /**
38
+ * Validates whether the ArchiMate type aligns with DMBOK2 layer expectations.
39
+ * Conceptual entities should realise Business Objects;
40
+ * Logical and physical entities should realise Data Objects or Artifacts.
41
+ * @returns {{ valid: boolean, recommendation?: string }}
42
+ */
43
+ validateLayerAlignment() {
44
+ if (this.level === 'conceptual' && this.archimateType !== 'business-object') {
45
+ return {
46
+ valid: false,
47
+ recommendation: 'Conceptual data model concepts should realise ArchiMate Business Objects in the Business layer.',
48
+ };
49
+ }
50
+ if ((this.level === 'logical' || this.level === 'physical') &&
51
+ !['data-object', 'artifact'].includes(this.archimateType)) {
52
+ return {
53
+ valid: false,
54
+ recommendation: `${this.level} data entities should realise ArchiMate Data Objects (Application layer) or Artifacts (Technology layer).`,
55
+ };
56
+ }
57
+ return { valid: true };
58
+ }
59
+
60
+ toJSON() {
61
+ return {
62
+ dataModelEntityId: this.dataModelEntityId,
63
+ level: this.level,
64
+ archimateElementId: this.archimateElementId,
65
+ archimateType: this.archimateType,
66
+ archimateModelId: this.archimateModelId,
67
+ notes: this.notes,
68
+ };
69
+ }
70
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * DataLineage — end-to-end data lineage and impact analysis across models and levels.
3
+ * Traces source-to-target provenance forward and backward (DMBOK2 Integration & Interoperability).
4
+ */
5
+ export class DataLineage {
6
+ /**
7
+ * @param {Object} [options]
8
+ * @param {Array<ModelMapping>} [options.modelMappings]
9
+ * @param {Array<LevelMapping>} [options.levelMappings]
10
+ */
11
+ constructor({ modelMappings = [], levelMappings = [] } = {}) {
12
+ this.modelMappings = [...modelMappings];
13
+ this.levelMappings = [...levelMappings];
14
+ }
15
+
16
+ addModelMapping(mapping) {
17
+ this.modelMappings.push(mapping);
18
+ }
19
+
20
+ /**
21
+ * Trace upstream lineage of a target attribute back to its source attributes.
22
+ * Traverses multi-hop transformations across models.
23
+ * @param {string} targetEntityId
24
+ * @param {string} [targetAttribute]
25
+ * @returns {Array<Object>} Chain of lineage hops with transformations
26
+ */
27
+ traceLineage(targetEntityId, targetAttribute = null) {
28
+ const chain = [];
29
+ const visited = new Set();
30
+ const queue = [{ entity: targetEntityId, attribute: targetAttribute, depth: 0 }];
31
+
32
+ while (queue.length) {
33
+ const current = queue.shift();
34
+ const key = `${current.entity}:${current.attribute || '*'}`;
35
+ if (visited.has(key)) continue;
36
+ visited.add(key);
37
+
38
+ for (const map of this.modelMappings) {
39
+ for (const attrMap of map.attributeMappings) {
40
+ if (attrMap.targetEntityId === current.entity &&
41
+ (!current.attribute || attrMap.targetAttribute === current.attribute)) {
42
+ const step = {
43
+ sourceModel: map.sourceModel,
44
+ targetModel: map.targetModel,
45
+ sourceEntityId: attrMap.sourceEntityId,
46
+ sourceAttribute: attrMap.sourceAttribute,
47
+ targetEntityId: attrMap.targetEntityId,
48
+ targetAttribute: attrMap.targetAttribute,
49
+ transformation: attrMap.transformation,
50
+ rule: attrMap.rule,
51
+ depth: current.depth + 1,
52
+ };
53
+ chain.push(step);
54
+ queue.push({
55
+ entity: attrMap.sourceEntityId,
56
+ attribute: attrMap.sourceAttribute,
57
+ depth: current.depth + 1,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return chain;
64
+ }
65
+
66
+ /**
67
+ * Trace downstream impact of changing a source attribute forward to targets.
68
+ * Traverses multi-hop dependencies across models.
69
+ * @param {string} sourceEntityId
70
+ * @param {string} [sourceAttribute]
71
+ * @returns {Array<Object>} Affected downstream entities and attributes
72
+ */
73
+ traceImpact(sourceEntityId, sourceAttribute = null) {
74
+ const impacts = [];
75
+ const visited = new Set();
76
+ const queue = [{ entity: sourceEntityId, attribute: sourceAttribute, depth: 0 }];
77
+
78
+ while (queue.length) {
79
+ const current = queue.shift();
80
+ const key = `${current.entity}:${current.attribute || '*'}`;
81
+ if (visited.has(key)) continue;
82
+ visited.add(key);
83
+
84
+ for (const map of this.modelMappings) {
85
+ for (const attrMap of map.attributeMappings) {
86
+ if (attrMap.sourceEntityId === current.entity &&
87
+ (!current.attribute || attrMap.sourceAttribute === current.attribute)) {
88
+ const step = {
89
+ sourceModel: map.sourceModel,
90
+ targetModel: map.targetModel,
91
+ sourceEntityId: attrMap.sourceEntityId,
92
+ sourceAttribute: attrMap.sourceAttribute,
93
+ targetEntityId: attrMap.targetEntityId,
94
+ targetAttribute: attrMap.targetAttribute,
95
+ transformation: attrMap.transformation,
96
+ rule: attrMap.rule,
97
+ depth: current.depth + 1,
98
+ };
99
+ impacts.push(step);
100
+ queue.push({
101
+ entity: attrMap.targetEntityId,
102
+ attribute: attrMap.targetAttribute,
103
+ depth: current.depth + 1,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return impacts;
110
+ }
111
+
112
+ /**
113
+ * Generates a graph representation of the lineage suitable for diagramming or export.
114
+ * @returns {{ nodes: Array<{id: string, entity: string, attribute: string, model: string}>, edges: Array<{source: string, target: string, transformation: string, rule: string}> }}
115
+ */
116
+ toGraph() {
117
+ const nodes = new Map();
118
+ const edges = [];
119
+
120
+ for (const map of this.modelMappings) {
121
+ for (const m of map.attributeMappings) {
122
+ const srcId = `${map.sourceModel || 'source'}:${m.sourceEntityId}.${m.sourceAttribute}`;
123
+ const tgtId = `${map.targetModel || 'target'}:${m.targetEntityId}.${m.targetAttribute}`;
124
+ if (!nodes.has(srcId)) {
125
+ nodes.set(srcId, {
126
+ id: srcId,
127
+ model: map.sourceModel || 'source',
128
+ entity: m.sourceEntityId,
129
+ attribute: m.sourceAttribute,
130
+ });
131
+ }
132
+ if (!nodes.has(tgtId)) {
133
+ nodes.set(tgtId, {
134
+ id: tgtId,
135
+ model: map.targetModel || 'target',
136
+ entity: m.targetEntityId,
137
+ attribute: m.targetAttribute,
138
+ });
139
+ }
140
+ edges.push({
141
+ source: srcId,
142
+ target: tgtId,
143
+ transformation: m.transformation,
144
+ rule: m.rule,
145
+ });
146
+ }
147
+ }
148
+ return { nodes: [...nodes.values()], edges };
149
+ }
150
+ }
@@ -15,6 +15,9 @@ import { NamingStandard } from './naming/NamingStandard.js';
15
15
  import { ClassificationScheme } from './ClassificationScheme.js';
16
16
  import { ModelScorecard } from './scorecard/ModelScorecard.js';
17
17
  import { ServiceLandscape } from './ServiceLandscape.js';
18
+ import { ModelMapping } from './ModelMapping.js';
19
+ import { DataLineage } from './DataLineage.js';
20
+ import { ArchimateDataLink } from './ArchimateDataLink.js';
18
21
  import {
19
22
  LevelEntitiesCompared, LevelRelationshipsCompared, MappingsCompared, ContextsCompared, PerspectivesCompared, DataTypesCompared,
20
23
  } from './DataModelCompared.js';
@@ -44,7 +47,7 @@ const LEVELS = Object.freeze(['conceptual', 'logical', 'physical']);
44
47
  */
45
48
  export class DataModel extends EditableModel {
46
49
  /** What a snapshot puts back (EditableModel#restore). */
47
- static STATE = Object.freeze(['name', 'levels', 'mappings', 'mappingLayout', 'noteLayout', 'perspectives', 'contexts', 'dataTypes', 'namingStandard', 'classificationScheme', 'scorecard']);
50
+ static STATE = Object.freeze(['name', 'levels', 'mappings', 'mappingLayout', 'noteLayout', 'perspectives', 'contexts', 'dataTypes', 'namingStandard', 'classificationScheme', 'scorecard', 'modelMappings', 'archimateLinks']);
48
51
 
49
52
  /** The levels, outermost first. */
50
53
  static LEVELS = LEVELS;
@@ -149,6 +152,8 @@ export class DataModel extends EditableModel {
149
152
  this.classificationScheme = spec.classificationScheme instanceof ClassificationScheme ? spec.classificationScheme : new ClassificationScheme(spec.classificationScheme ?? {});
150
153
  /** What a reviewer said on the Scorecard, and the weights reviewed by (ModelScorecard), or null. */
151
154
  this.scorecard = spec.scorecard ? JSON.parse(JSON.stringify(spec.scorecard)) : null;
155
+ this.modelMappings = (spec.modelMappings ?? []).map(m => m instanceof ModelMapping ? m : new ModelMapping(m));
156
+ this.archimateLinks = (spec.archimateLinks ?? []).map(l => l instanceof ArchimateDataLink ? l : new ArchimateDataLink(l));
152
157
  }
153
158
 
154
159
  /** The model scored on the Data Model Scorecard (ModelScorecard). */
@@ -919,6 +924,83 @@ ${theirs}` : theirs;
919
924
  ...(this.namingStandard && { namingStandard: this.namingStandard.toJSON() }),
920
925
  ...(!this.classificationScheme.isDefault && { classificationScheme: this.classificationScheme.toJSON() }),
921
926
  ...(this.scorecard && { scorecard: JSON.parse(JSON.stringify(this.scorecard)) }),
927
+ ...(this.modelMappings.length && { modelMappings: this.modelMappings.map(m => m.toJSON()) }),
928
+ ...(this.archimateLinks.length && { archimateLinks: this.archimateLinks.map(l => l.toJSON()) }),
922
929
  };
923
930
  }
931
+
932
+ /** Add a cross-model mapping (DMBOK2 Integration & Interoperability). */
933
+ addModelMapping(mapping) {
934
+ const m = mapping instanceof ModelMapping ? mapping : new ModelMapping(mapping);
935
+ this.modelMappings.push(m);
936
+ return m;
937
+ }
938
+
939
+ /** End-to-end data lineage across models and internal level mappings (DMBOK2). */
940
+ crossModelLineage() {
941
+ return new DataLineage({ modelMappings: this.modelMappings, levelMappings: this.mappings });
942
+ }
943
+
944
+ /** Link a data model concept to an ArchiMate model element (DMBOK2 Data Architecture). */
945
+ linkToArchimate({ entityId, level = 'conceptual', archimateElementId, archimateType = 'business-object', archimateModelId = null, notes = '' }) {
946
+ this.unlinkFromArchimate(entityId, archimateElementId);
947
+ const link = new ArchimateDataLink({
948
+ dataModelEntityId: entityId,
949
+ level,
950
+ archimateElementId,
951
+ archimateType,
952
+ archimateModelId,
953
+ notes,
954
+ });
955
+ this.archimateLinks.push(link);
956
+ return link;
957
+ }
958
+
959
+ /** Remove a link to an ArchiMate element. */
960
+ unlinkFromArchimate(entityId, archimateElementId) {
961
+ this.archimateLinks = this.archimateLinks.filter(l => !(l.dataModelEntityId === entityId && l.archimateElementId === archimateElementId));
962
+ }
963
+
964
+ /** Links for a specific data model entity. */
965
+ archimateLinksFor(entityId) {
966
+ return this.archimateLinks.filter(l => l.dataModelEntityId === entityId);
967
+ }
968
+
969
+ /**
970
+ * Validate linked ArchiMate elements against an ArchiMate model (DMBOK2 Data Architecture).
971
+ * Verifies existence, element type and layer alignment.
972
+ * @param {ArchimateModel} archimateModel
973
+ * @returns {Array<{ link: ArchimateDataLink, valid: boolean, errors: Array<string> }>}
974
+ */
975
+ validateArchimateRealization(archimateModel) {
976
+ const results = [];
977
+ for (const link of this.archimateLinks) {
978
+ const errors = [];
979
+ const el = typeof archimateModel.getElement === 'function' ? archimateModel.getElement(link.archimateElementId) : null;
980
+ if (!el) {
981
+ errors.push(`Referenced ArchiMate element "${link.archimateElementId}" not found in ArchiMate model`);
982
+ } else if (el.type !== link.archimateType) {
983
+ errors.push(`Type mismatch: link specifies "${link.archimateType}" but model element is "${el.type}"`);
984
+ }
985
+ const layerCheck = link.validateLayerAlignment();
986
+ if (!layerCheck.valid) {
987
+ errors.push(layerCheck.recommendation);
988
+ }
989
+ results.push({ link, valid: errors.length === 0, errors });
990
+ }
991
+ return results;
992
+ }
993
+
994
+ /**
995
+ * Create an ArchiMate realization view on the given ArchiMate model showing the data architecture realization.
996
+ * @param {ArchimateModel} archimateModel
997
+ * @param {string} [viewTitle='Data Architecture Realization']
998
+ * @returns {ArchimateView}
999
+ */
1000
+ createArchimateRealizationView(archimateModel, viewTitle = 'Data Architecture Realization') {
1001
+ const elementIds = [...new Set(this.archimateLinks.map(l => l.archimateElementId))];
1002
+ const view = archimateModel.createView(viewTitle);
1003
+ view.elementIds = elementIds;
1004
+ return view;
1005
+ }
924
1006
  }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * ModelMapping — mappings between distinct data models (DMBOK2 Integration & Interoperability).
3
+ * Maps source model entities and attributes to target model entities and attributes,
4
+ * with transformation rules and expressions.
5
+ */
6
+ export class ModelMapping {
7
+ /**
8
+ * @param {Object} spec
9
+ * @param {string} [spec.id]
10
+ * @param {string} [spec.name]
11
+ * @param {string} [spec.sourceModel] - Name or identifier of the source data model
12
+ * @param {string} [spec.targetModel] - Name or identifier of the target data model
13
+ * @param {Array<Object>} [spec.entityMappings]
14
+ * @param {Array<Object>} [spec.attributeMappings]
15
+ * @param {string} [spec.documentation]
16
+ */
17
+ constructor({
18
+ id,
19
+ name = 'Cross-Model Mapping',
20
+ sourceModel = '',
21
+ targetModel = '',
22
+ entityMappings = [],
23
+ attributeMappings = [],
24
+ documentation = '',
25
+ } = {}) {
26
+ this.id = id || `mapping-${Date.now().toString(36)}`;
27
+ this.name = name;
28
+ this.sourceModel = sourceModel;
29
+ this.targetModel = targetModel;
30
+ this.documentation = documentation;
31
+ this.entityMappings = entityMappings.map(e => ({ ...e }));
32
+ this.attributeMappings = attributeMappings.map(a => ({ ...a }));
33
+ }
34
+
35
+ /**
36
+ * Add an entity-level mapping between source and target entities.
37
+ */
38
+ addEntityMapping({ sourceEntityId, targetEntityId, name = '', description = '' } = {}) {
39
+ if (!sourceEntityId || !targetEntityId) {
40
+ throw new TypeError('Entity mapping requires sourceEntityId and targetEntityId');
41
+ }
42
+ if (!this.entityMappings.some(m => m.sourceEntityId === sourceEntityId && m.targetEntityId === targetEntityId)) {
43
+ this.entityMappings.push({ sourceEntityId, targetEntityId, name, description });
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Add an attribute-level field mapping with transformation rule.
49
+ */
50
+ addAttributeMapping({ sourceEntityId, sourceAttribute, targetEntityId, targetAttribute, transformation = 'direct', rule = '' } = {}) {
51
+ if (!sourceEntityId || !sourceAttribute || !targetEntityId || !targetAttribute) {
52
+ throw new TypeError('Attribute mapping requires sourceEntityId, sourceAttribute, targetEntityId, and targetAttribute');
53
+ }
54
+ const existing = this.attributeMappings.find(m =>
55
+ m.sourceEntityId === sourceEntityId && m.sourceAttribute === sourceAttribute &&
56
+ m.targetEntityId === targetEntityId && m.targetAttribute === targetAttribute
57
+ );
58
+ if (existing) {
59
+ existing.transformation = transformation;
60
+ existing.rule = rule;
61
+ } else {
62
+ this.attributeMappings.push({ sourceEntityId, sourceAttribute, targetEntityId, targetAttribute, transformation, rule });
63
+ }
64
+ }
65
+
66
+ /** All attribute mappings targeting an entity. */
67
+ attributesTargeting(entityId) {
68
+ return this.attributeMappings.filter(m => m.targetEntityId === entityId);
69
+ }
70
+
71
+ /** All attribute mappings sourced from an entity. */
72
+ attributesSourcedFrom(entityId) {
73
+ return this.attributeMappings.filter(m => m.sourceEntityId === entityId);
74
+ }
75
+
76
+ toJSON() {
77
+ return {
78
+ id: this.id,
79
+ name: this.name,
80
+ sourceModel: this.sourceModel,
81
+ targetModel: this.targetModel,
82
+ documentation: this.documentation,
83
+ entityMappings: this.entityMappings.map(e => ({ ...e })),
84
+ attributeMappings: this.attributeMappings.map(a => ({ ...a })),
85
+ };
86
+ }
87
+ }