@peter.prib/renderengine 0.15.1 → 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.
- package/dist/renderengine-workspace.umd.js +6485 -3992
- package/dist/renderengine.umd.js +6251 -3877
- package/documentation/REFERENCE.md +384 -28
- package/package.json +1 -1
- package/src/archimateArchiFormat.js +114 -10
- package/src/archimateArchive.js +27 -4
- package/src/archimateExchangeFormat.js +12 -4
- package/src/archimateSpecializations.js +11 -1
- package/src/chartCatalog.js +6 -4
- package/src/charting/ArchimateDiagramEditor.js +7 -109
- package/src/charting/DiagramTypeRegistry.js +20 -1
- package/src/charting/DiagramViewPropertiesDialog.js +54 -10
- package/src/charting/Edge.js +22 -0
- package/src/charting/Legend.js +1 -2
- package/src/charting/Statistics.js +14 -0
- package/src/charting/archimate/ArchimateCanvasEditor.js +21 -5
- package/src/charting/archimate/ViewGroupTemplate.js +5 -2
- package/src/charting/charts/BarChart.js +59 -8
- package/src/charting/charts/HexbinChart.js +15 -0
- package/src/charting/charts/LollipopChart.js +42 -7
- package/src/charting/decision/DecisionWorkbench.js +71 -4
- package/src/charting/diagramModel/DiagramJsonFileFormat.js +32 -0
- package/src/charting/diagramModel/DiagramModel.js +39 -0
- package/src/charting/diagramModel/DiagramView.js +102 -61
- package/src/charting/diagramModel/index.js +11 -10
- package/src/charting/diagrams/AddGroupToViewCommand.js +35 -0
- package/src/charting/diagrams/ArchimateModel.js +53 -13
- package/src/charting/diagrams/GraphDiagram3D.js +15 -0
- package/src/charting/diagrams/NestInstanceCommand.js +2 -1
- package/src/charting/graph/GraphSource.js +72 -0
- package/src/charting/modelTree/NavigatorModel.js +22 -8
- package/src/charting/report/TemplatedReport.js +27 -377
- package/src/charting/report/ViewsReport.js +118 -0
- package/src/charting/report/reportWriters.js +496 -0
- package/src/charting/templates/C4BoundaryTemplate.js +34 -0
- package/src/charting/templates/C4ComponentTemplate.js +30 -0
- package/src/charting/templates/C4ContainerTemplate.js +49 -0
- package/src/charting/templates/C4PersonTemplate.js +35 -0
- package/src/charting/templates/C4SystemTemplate.js +32 -0
- package/src/charting/templates/C4Template.js +95 -0
- package/src/charting/templates/index.js +6 -0
- package/src/charting/templates/templateRegistry.js +77 -0
- package/src/compute/ComputeBackend.js +132 -0
- package/src/compute/CpuBackend.js +209 -0
- package/src/compute/WebGpuBackend.js +630 -0
- package/src/compute/index.js +5 -0
- package/src/compute/selectBackend.js +85 -0
- package/src/index.js +3403 -3370
- package/src/kernelExpr.js +80 -5
- package/src/workspace/parts/ArchimateEditorPart.js +2 -2
- package/src/workspace/parts/DiagramEditorPart.js +146 -131
- package/src/workspace/parts/ViewsPart.js +106 -0
package/src/charting/Legend.js
CHANGED
|
@@ -90,9 +90,8 @@ export class Legend {
|
|
|
90
90
|
const hidden = !!item.hidden;
|
|
91
91
|
const color = hidden ? '#ccc' : item.color;
|
|
92
92
|
const textColor = hidden ? '#ccc' : '#333';
|
|
93
|
-
const attrs = {};
|
|
93
|
+
const attrs = { legendItem: true };
|
|
94
94
|
if (item.dataIndex !== undefined) {
|
|
95
|
-
attrs.legendItem = true;
|
|
96
95
|
attrs.dataIndex = item.dataIndex;
|
|
97
96
|
}
|
|
98
97
|
|
|
@@ -131,4 +131,18 @@ export class Statistics {
|
|
|
131
131
|
}
|
|
132
132
|
return points;
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Asynchronous Gaussian-kernel density estimate, using GPU compute backend when available.
|
|
137
|
+
* @param {number[]} values
|
|
138
|
+
* @param {object} [options]
|
|
139
|
+
* @param {import('../compute/ComputeBackend.js').ComputeBackend} [options.backend]
|
|
140
|
+
* @returns {Promise<Array<{x: number, density: number}>>}
|
|
141
|
+
*/
|
|
142
|
+
static async kernelDensityEstimateAsync(values, options = {}) {
|
|
143
|
+
if (options.backend) {
|
|
144
|
+
return options.backend.kernelDensityEstimateAsync(values, options);
|
|
145
|
+
}
|
|
146
|
+
return Statistics.kernelDensityEstimate(values, options);
|
|
147
|
+
}
|
|
134
148
|
}
|
|
@@ -1034,15 +1034,28 @@ export class ArchimateCanvasEditor extends DiagramEditor {
|
|
|
1034
1034
|
return element ? treeLayerOf(element) : 'other';
|
|
1035
1035
|
}
|
|
1036
1036
|
|
|
1037
|
-
/** An element's box holds any box
|
|
1037
|
+
/** An element's box holds any box; a group holds any box or nested group; on a canvas, a block holds boxes. */
|
|
1038
1038
|
canHold(parentId, childId) {
|
|
1039
|
+
if (!parentId || !childId || parentId === childId) return false;
|
|
1039
1040
|
const parent = this.templates.get(parentId), child = this.templates.get(childId);
|
|
1040
|
-
if (!parent
|
|
1041
|
+
if (!parent || !child) return false;
|
|
1042
|
+
if (child instanceof ViewGroupTemplate && parent instanceof ViewGroupTemplate) {
|
|
1043
|
+
return !this.isAncestor(childId, parentId);
|
|
1044
|
+
}
|
|
1045
|
+
if (parent instanceof ViewGroupTemplate) {
|
|
1046
|
+
return !this.isAncestor(childId, parentId);
|
|
1047
|
+
}
|
|
1048
|
+
if (child instanceof ViewGroupTemplate) return false;
|
|
1049
|
+
if (!parent.instance || !child.instance) return false;
|
|
1041
1050
|
return !!this._elementOf(parentId) || parent.instance.content?.archiVisualType === 'canvas-model-block';
|
|
1042
1051
|
}
|
|
1043
1052
|
|
|
1044
|
-
/** An element dropped on another nests there when a relationship a nesting stands for (NESTING_KINDS) is allowed between them. */
|
|
1053
|
+
/** An element dropped on another nests there when a relationship a nesting stands for (NESTING_KINDS) is allowed between them, or when dropped into a group. */
|
|
1045
1054
|
nestsOnDrop(parentId, childId) {
|
|
1055
|
+
const parent = this.templates.get(parentId);
|
|
1056
|
+
if (parent instanceof ViewGroupTemplate) {
|
|
1057
|
+
return this.canHold(parentId, childId);
|
|
1058
|
+
}
|
|
1046
1059
|
if (!this._elementOf(parentId) || !this._elementOf(childId)) return false;
|
|
1047
1060
|
return this.allowedKinds(parentId, childId).some(kind => NESTING_KINDS.includes(kind));
|
|
1048
1061
|
}
|
|
@@ -1050,13 +1063,16 @@ export class ArchimateCanvasEditor extends DiagramEditor {
|
|
|
1050
1063
|
nestCommand(childId, parentId) {
|
|
1051
1064
|
const child = this.templates.get(childId)?.instance;
|
|
1052
1065
|
if (!child) return null;
|
|
1053
|
-
|
|
1066
|
+
const parentTemplate = parentId ? this.templates.get(parentId) : null;
|
|
1067
|
+
const parentTarget = parentTemplate?.instance ?? (parentTemplate instanceof ViewGroupTemplate ? parentTemplate : null);
|
|
1068
|
+
return new NestInstanceCommand(child, parentTarget);
|
|
1054
1069
|
}
|
|
1055
1070
|
|
|
1056
|
-
/** After nesting an element in another: offer the relationship the nesting stands for (nestingKinds). */
|
|
1071
|
+
/** After nesting an element in another: offer the relationship the nesting stands for (nestingKinds). Groups have no relationship. */
|
|
1057
1072
|
nested(changes) {
|
|
1058
1073
|
let chain = Promise.resolve();
|
|
1059
1074
|
for (const { childId, parentId } of changes) {
|
|
1075
|
+
if (this.templates.get(parentId) instanceof ViewGroupTemplate) continue;
|
|
1060
1076
|
chain = chain.then(() => this.offerNestingRelationship(parentId, childId)); // taken out: nothing to offer
|
|
1061
1077
|
}
|
|
1062
1078
|
return chain;
|
|
@@ -47,8 +47,11 @@ export class ViewGroupTemplate extends GlyphTemplate {
|
|
|
47
47
|
return this.view.model.getFolder(this.id);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
get name() { return this.folder?.name ?? 'Group'; }
|
|
51
|
-
set name(value) {
|
|
50
|
+
get name() { return this.folder?.name ?? this.layout?.name ?? 'Group'; }
|
|
51
|
+
set name(value) {
|
|
52
|
+
if (this.folder) this.folder.name = value || 'Group';
|
|
53
|
+
if (this.layout) this.layout.name = value || 'Group';
|
|
54
|
+
}
|
|
52
55
|
|
|
53
56
|
// The layout keeps the top-left corner; a template is placed by its centre.
|
|
54
57
|
get x() { return this.layout.x + this.width / 2; }
|
|
@@ -3,6 +3,7 @@ import { Legend } from '../Legend.js';
|
|
|
3
3
|
import { DataFilter } from '../DataFilter.js';
|
|
4
4
|
import { BarChartBase } from './BarChartBase.js';
|
|
5
5
|
import { Annotations } from '../Annotations.js';
|
|
6
|
+
import { ChartData } from '../ChartData.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* BarChart — vertical bar chart with optional legend and hidden items.
|
|
@@ -15,6 +16,32 @@ export class BarChart extends BarChartBase {
|
|
|
15
16
|
this._dataFilter = dataFilter;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
_setup() {
|
|
20
|
+
const data = this.data;
|
|
21
|
+
const options = this.options;
|
|
22
|
+
this._catField = options.colorCol
|
|
23
|
+
?? (Array.isArray(data) && data.some(d => d?.category != null) ? 'category'
|
|
24
|
+
: Array.isArray(data) && data.some(d => d?.group != null) ? 'group' : null);
|
|
25
|
+
super._setup();
|
|
26
|
+
this._catColors = new Map();
|
|
27
|
+
if (this._catField && Array.isArray(data)) {
|
|
28
|
+
for (const d of data) {
|
|
29
|
+
const cat = ChartData.readColorCol(d, this._catField);
|
|
30
|
+
if (cat != null && !this._catColors.has(cat)) {
|
|
31
|
+
this._catColors.set(cat, this._palette.colorAt(this._catColors.size));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const showCatLegend = Boolean(this._catField && this._catColors.size > 0 && options.legend !== false && options.showLegend !== false);
|
|
36
|
+
if (showCatLegend && options.width === undefined) {
|
|
37
|
+
const legendItems = [...this._catColors].map(([cat, color]) => ({ label: String(cat), color }));
|
|
38
|
+
this._plotBox.width = Legend.plotWidthBeside(this._plotBox, this.renderer.width, {
|
|
39
|
+
items: legendItems,
|
|
40
|
+
itemHeight: 20, markerSize: 10,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
_computeMaxValue() {
|
|
19
46
|
const data = this.data;
|
|
20
47
|
const rawMax = Math.max(...data.map(d => d.value)) || 1;
|
|
@@ -42,16 +69,27 @@ export class BarChart extends BarChartBase {
|
|
|
42
69
|
const { bandWidth, margin } = this._bandScale(data.length);
|
|
43
70
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
44
71
|
|
|
72
|
+
const colorFor = (d, i) => {
|
|
73
|
+
if (this._catField) {
|
|
74
|
+
const cat = ChartData.readColorCol(d, this._catField);
|
|
75
|
+
if (cat != null && this._catColors.has(cat)) {
|
|
76
|
+
return this._catColors.get(cat);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return colors ? this._palette.colorAt(i) : barColor;
|
|
80
|
+
};
|
|
81
|
+
|
|
45
82
|
if (this._isHorizontal()) {
|
|
46
83
|
data.forEach((d, i) => {
|
|
47
84
|
if (this._dataFilter.has(i)) return;
|
|
48
|
-
const color =
|
|
85
|
+
const color = colorFor(d, i);
|
|
49
86
|
const bar = this._barSpan(d.value);
|
|
50
87
|
const barY = y + i * (bandWidth + margin);
|
|
88
|
+
const series = this._catField ? ChartData.readColorCol(d, this._catField) : null;
|
|
51
89
|
|
|
52
90
|
this._datum(this.renderer.rect(bar.top, barY, bar.height, bandWidth, color, null, {
|
|
53
91
|
bar: true, dataIndex: i, tooltip: `${d.label}: ${d.value}`,
|
|
54
|
-
}), { label: d.label });
|
|
92
|
+
}), { label: d.label, series: series != null ? String(series) : null });
|
|
55
93
|
|
|
56
94
|
// Category label on left
|
|
57
95
|
this.renderer.text(d.label, x - 8, barY + bandWidth / 2, {
|
|
@@ -73,11 +111,12 @@ export class BarChart extends BarChartBase {
|
|
|
73
111
|
return; // Skip rendering the bar if it's hidden
|
|
74
112
|
}
|
|
75
113
|
|
|
76
|
-
const color =
|
|
114
|
+
const color = colorFor(d, i);
|
|
77
115
|
const bar = this._barSpan(d.value);
|
|
78
116
|
const barX = x + i * (bandWidth + margin);
|
|
117
|
+
const series = this._catField ? ChartData.readColorCol(d, this._catField) : null;
|
|
79
118
|
|
|
80
|
-
this._datum(this.renderer.rect(barX, bar.top, bandWidth, bar.height, color, null, { bar: true, dataIndex: i, tooltip: `${d.label}: ${d.value}` }), { label: d.label });
|
|
119
|
+
this._datum(this.renderer.rect(barX, bar.top, bandWidth, bar.height, color, null, { bar: true, dataIndex: i, tooltip: `${d.label}: ${d.value}` }), { label: d.label, series: series != null ? String(series) : null });
|
|
81
120
|
// The value just past the bar's end: above a bar going up, below one going down.
|
|
82
121
|
this.renderer.text(String(d.value), barX + bandWidth / 2, d.value < 0 ? bar.end + 12 : bar.end - 5, { textAnchor, fontSize: '10px', fill: '#333' });
|
|
83
122
|
if (i % labelStep === 0) this.renderer.text(d.label, barX + bandWidth / 2, y + height + 20, { textAnchor });
|
|
@@ -88,9 +127,19 @@ export class BarChart extends BarChartBase {
|
|
|
88
127
|
const data = this.data;
|
|
89
128
|
const options = this.options;
|
|
90
129
|
const { x, y, width } = this._plotBox;
|
|
91
|
-
const
|
|
130
|
+
const colors = options.colors;
|
|
131
|
+
const showCatLegend = Boolean(this._catField && this._catColors.size > 0 && options.legend !== false && options.showLegend !== false);
|
|
92
132
|
|
|
93
|
-
if (
|
|
133
|
+
if (showCatLegend) {
|
|
134
|
+
const legendItems = [...this._catColors].map(([cat, color]) => ({
|
|
135
|
+
label: String(cat),
|
|
136
|
+
color,
|
|
137
|
+
}));
|
|
138
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
139
|
+
items: legendItems,
|
|
140
|
+
itemHeight: 20, markerSize: 10,
|
|
141
|
+
}).draw(this.renderer);
|
|
142
|
+
} else if ((options.showLegend || options.legend) && colors) {
|
|
94
143
|
const legendItems = data.map((d, i) => ({
|
|
95
144
|
label: d.label,
|
|
96
145
|
color: this._palette.colorAt(i),
|
|
@@ -111,7 +160,9 @@ export class BarChart extends BarChartBase {
|
|
|
111
160
|
*/
|
|
112
161
|
onClick(event, hitShape) {
|
|
113
162
|
if (!hitShape || !hitShape.legendItem) return;
|
|
114
|
-
|
|
115
|
-
|
|
163
|
+
if (hitShape.dataIndex != null) {
|
|
164
|
+
this._dataFilter.toggle(hitShape.dataIndex);
|
|
165
|
+
this.render();
|
|
166
|
+
}
|
|
116
167
|
}
|
|
117
168
|
}
|
|
@@ -72,6 +72,21 @@ export class HexbinChart extends Chart {
|
|
|
72
72
|
return bins;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Asynchronous 2D hexagonal binning, offloading to GPU when backend provided.
|
|
77
|
+
* @param {Array<{px: number, py: number, [key: string]: any}>} points
|
|
78
|
+
* @param {number} r
|
|
79
|
+
* @param {object} [options]
|
|
80
|
+
* @param {import('../../compute/ComputeBackend.js').ComputeBackend} [options.backend]
|
|
81
|
+
* @returns {Promise<Map<string, {cx: number, cy: number, count: number, points: Array}>>}
|
|
82
|
+
*/
|
|
83
|
+
static async binPointsAsync(points, r, options = {}) {
|
|
84
|
+
if (options.backend) {
|
|
85
|
+
return options.backend.binHexbinsAsync(points, r, options);
|
|
86
|
+
}
|
|
87
|
+
return HexbinChart.binPoints(points, r);
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
_render() {
|
|
76
91
|
const data = this.data;
|
|
77
92
|
if (!Array.isArray(data) || !data.length) return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Axis } from '../Axis.js';
|
|
2
2
|
import { Legend } from '../Legend.js';
|
|
3
3
|
import { BarChartBase } from './BarChartBase.js';
|
|
4
|
+
import { ChartData } from '../ChartData.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* LollipopChart — stem-and-dot alternative to a bar chart for high-cardinality or multi-series
|
|
@@ -29,13 +30,34 @@ export class LollipopChart extends BarChartBase {
|
|
|
29
30
|
_setup() {
|
|
30
31
|
this._norm = LollipopChart._normalize(this.data);
|
|
31
32
|
super._setup();
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
this._catField = !this._norm.isMulti
|
|
34
|
+
? (this.options.colorCol
|
|
35
|
+
?? (Array.isArray(this.data) && this.data.some(d => d?.category != null) ? 'category'
|
|
36
|
+
: Array.isArray(this.data) && this.data.some(d => d?.group != null) ? 'group' : null))
|
|
37
|
+
: null;
|
|
38
|
+
this._catColors = new Map();
|
|
39
|
+
if (this._catField && Array.isArray(this.data)) {
|
|
40
|
+
for (const d of this.data) {
|
|
41
|
+
const cat = ChartData.readColorCol(d, this._catField);
|
|
42
|
+
if (cat != null && !this._catColors.has(cat)) {
|
|
43
|
+
this._catColors.set(cat, this._palette.colorAt(this._catColors.size));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const hasCatLegend = Boolean(this._catField && this._catColors.size > 0);
|
|
48
|
+
const showLegend = this.options.legend ?? (this._norm.isMulti || hasCatLegend);
|
|
49
|
+
if (showLegend && (this._norm.isMulti || hasCatLegend) && this.options.width === undefined) {
|
|
34
50
|
this._plotBox.width = Legend.plotWidthBeside(this._plotBox, this.renderer.width, this._legendOptions());
|
|
35
51
|
}
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
_legendOptions() {
|
|
55
|
+
if (this._catField && this._catColors.size > 0) {
|
|
56
|
+
return {
|
|
57
|
+
items: [...this._catColors].map(([cat, color]) => ({ label: String(cat), color })),
|
|
58
|
+
itemHeight: 20, markerSize: 10, marker: 'circle',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
39
61
|
return {
|
|
40
62
|
items: this._norm.series.map((s, i) => ({ label: s.name, color: this._palette.colorAt(i), dataIndex: i })),
|
|
41
63
|
itemHeight: 20, markerSize: 10,
|
|
@@ -74,18 +96,30 @@ export class LollipopChart extends BarChartBase {
|
|
|
74
96
|
? bandX + bandWidth / 2
|
|
75
97
|
: bandX + ((sIdx + 0.5) / seriesCount) * bandWidth;
|
|
76
98
|
const span = this._barSpan(pt.value);
|
|
77
|
-
|
|
99
|
+
|
|
100
|
+
let col;
|
|
101
|
+
let catVal = null;
|
|
102
|
+
if (!isMulti && this._catField) {
|
|
103
|
+
catVal = ChartData.readColorCol(pt, this._catField);
|
|
104
|
+
col = catVal != null && this._catColors.has(catVal)
|
|
105
|
+
? this._catColors.get(catVal)
|
|
106
|
+
: (colors ? this._palette.colorAt(i) : singleColor);
|
|
107
|
+
} else {
|
|
108
|
+
col = isMulti || colors ? this._palette.colorAt(sIdx) : singleColor;
|
|
109
|
+
}
|
|
110
|
+
|
|
78
111
|
const tip = isMulti ? `${s.name} - ${label}: ${pt.value}` : `${label}: ${pt.value}`;
|
|
79
112
|
|
|
80
113
|
this.renderer.path(`M${cx},${zeroY} L${cx},${span.end}`, 'none', { color: col, width: stemWidth });
|
|
114
|
+
const datumSeries = isMulti ? s.name : (catVal != null ? String(catVal) : null);
|
|
81
115
|
if (typeof this.renderer.circle === 'function') {
|
|
82
116
|
this._datum(this.renderer.circle(cx, span.end, radius, col, { color: '#fff', width: 1.5 }, {
|
|
83
117
|
tooltip: tip,
|
|
84
|
-
}), { label, series:
|
|
118
|
+
}), { label, series: datumSeries });
|
|
85
119
|
} else {
|
|
86
120
|
this._datum(this.renderer.rect(cx - radius, span.end - radius, radius * 2, radius * 2, col, null, {
|
|
87
121
|
tooltip: tip,
|
|
88
|
-
}), { label, series:
|
|
122
|
+
}), { label, series: datumSeries });
|
|
89
123
|
}
|
|
90
124
|
});
|
|
91
125
|
if (i % labelStep === 0) {
|
|
@@ -95,8 +129,9 @@ export class LollipopChart extends BarChartBase {
|
|
|
95
129
|
}
|
|
96
130
|
|
|
97
131
|
_afterBars() {
|
|
98
|
-
const
|
|
99
|
-
|
|
132
|
+
const hasCatLegend = Boolean(this._catField && this._catColors.size > 0);
|
|
133
|
+
const showLegend = this.options.legend ?? (this._norm.isMulti || hasCatLegend);
|
|
134
|
+
if (showLegend && (this._norm.isMulti || hasCatLegend)) {
|
|
100
135
|
const { x, y, width } = this._plotBox;
|
|
101
136
|
Legend.beside({ x, y, width }, this.renderer.width, this._legendOptions()).draw(this.renderer);
|
|
102
137
|
}
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
*
|
|
6
6
|
* It reads decisionengine's JSON, not decisionengine: a workbench document
|
|
7
7
|
*
|
|
8
|
-
* { kind: 'sadis-workbench', report, plan?, before? }
|
|
8
|
+
* { kind: 'sadis-workbench', report, plan?, before?, graph? }
|
|
9
9
|
*
|
|
10
10
|
* holds a report's `toJSON()` (the ranked candidates, each with its score's
|
|
11
11
|
* parts), optionally a delivery plan's `toJSON()` and an earlier report to
|
|
12
|
-
* replay against
|
|
12
|
+
* replay against, and the scored portfolio graph (`{ nodes, edges }`) to explore. Each view is a
|
|
13
|
+
* `WorkbenchView` subclass naming the charts it
|
|
13
14
|
* shows (`{ title, chartType, data, options }`, `chartType` a CHART_CATALOG key)
|
|
14
15
|
* and notes for what a chart cannot show; a view needing what the document
|
|
15
16
|
* lacks (a plan, perspectives, an earlier report) is not offered.
|
|
@@ -17,7 +18,11 @@
|
|
|
17
18
|
* DOM-free: a workspace page puts each chart in a ChartPart.
|
|
18
19
|
*/
|
|
19
20
|
|
|
21
|
+
import { NavigatorModel } from '../modelTree/NavigatorModel.js';
|
|
22
|
+
|
|
20
23
|
const KIND = 'sadis-workbench';
|
|
24
|
+
/** SADIS's relationships that carry value up the basin, and those that make one candidate wait on another. */
|
|
25
|
+
const CONTRIBUTES = ['ENABLES', 'IMPLEMENTS', 'REALIZES', 'SUPPORTS'];
|
|
21
26
|
const DAY = 24 * 60 * 60 * 1000;
|
|
22
27
|
const pct = (share) => Math.round(share * 1000) / 10;
|
|
23
28
|
const round = (n) => Math.round(n * 1000) / 1000;
|
|
@@ -115,6 +120,64 @@ export class ArchitectureView extends WorkbenchView {
|
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The first candidate's neighbourhood in the portfolio graph: the ways its value goes up to each objective
|
|
125
|
+
* it reaches, and what waits on it. Read with `NavigatorModel` over the graph document (a `DocumentGraph`),
|
|
126
|
+
* the same walker the ArchiMate modeller's Navigator uses; drawn as a graph diagram.
|
|
127
|
+
*/
|
|
128
|
+
export class ExploreView extends WorkbenchView {
|
|
129
|
+
static id = 'explore';
|
|
130
|
+
static title = 'Explore';
|
|
131
|
+
|
|
132
|
+
available() { return Boolean(this.workbench.graph); }
|
|
133
|
+
|
|
134
|
+
/** The candidate explored, the navigator, the ways up and what waits on it. */
|
|
135
|
+
explore() {
|
|
136
|
+
const { workbench } = this;
|
|
137
|
+
const top = workbench.entries[0];
|
|
138
|
+
const navigator = NavigatorModel.forGraph(workbench.graph, { rootId: top.id });
|
|
139
|
+
const ways = top.basin.reaches.map((r) => navigator.findPaths(top.id, r.id, { direction: 'outgoing', maxDepth: r.hops, allowedKinds: CONTRIBUTES })[0])
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
const waiting = [
|
|
142
|
+
navigator.traceImpact(top.id, { direction: 'incoming', allowedKinds: ['DEPENDS_ON'] }),
|
|
143
|
+
navigator.traceImpact(top.id, { direction: 'outgoing', allowedKinds: ['BLOCKS'] }),
|
|
144
|
+
];
|
|
145
|
+
return { top, navigator, ways, waiting };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
charts() {
|
|
149
|
+
const { top, navigator, ways, waiting } = this.explore();
|
|
150
|
+
const nodes = new Map([[top.id, navigator.model.getElement(top.id)]]);
|
|
151
|
+
const edges = new Map();
|
|
152
|
+
for (const way of ways) {
|
|
153
|
+
for (const element of way.elements) nodes.set(element.id, element);
|
|
154
|
+
for (const rel of way.relationships) edges.set(rel.id, rel);
|
|
155
|
+
}
|
|
156
|
+
for (const impact of waiting) {
|
|
157
|
+
for (const element of impact.elements) nodes.set(element.id, element);
|
|
158
|
+
for (const rel of impact.relationships) edges.set(rel.id, rel);
|
|
159
|
+
}
|
|
160
|
+
return [{
|
|
161
|
+
title: `${top.name}: its ways to the objectives, and what waits on it`,
|
|
162
|
+
chartType: 'graph',
|
|
163
|
+
data: null,
|
|
164
|
+
options: {
|
|
165
|
+
nodes: [...nodes.values()].filter(Boolean).map((e) => ({ id: e.id, label: e.name, group: e.type })),
|
|
166
|
+
edges: [...edges.values()].map((r) => ({ source: r.source, target: r.target, label: r.kind })),
|
|
167
|
+
},
|
|
168
|
+
}];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
notes() {
|
|
172
|
+
const { top, ways, waiting } = this.explore();
|
|
173
|
+
const waits = [...new Map(waiting.flatMap((i) => i.elements).map((e) => [e.id, e.name])).values()];
|
|
174
|
+
return [
|
|
175
|
+
...ways.map((way) => `${way.elements.map((e) => e.name).join(' → ')} (${way.length} ${way.length === 1 ? 'step' : 'steps'})`),
|
|
176
|
+
waits.length ? `Waiting on ${top.name}: ${waits.join(', ')}` : `Nothing waits on ${top.name}`,
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
118
181
|
/** The delivery plan: when each candidate is worked on, and how full each period is. */
|
|
119
182
|
export class DeliveryView extends WorkbenchView {
|
|
120
183
|
static id = 'delivery';
|
|
@@ -245,7 +308,7 @@ export class ReplayView extends WorkbenchView {
|
|
|
245
308
|
}
|
|
246
309
|
}
|
|
247
310
|
|
|
248
|
-
export const WORKBENCH_VIEWS = Object.freeze([ExecutiveView, ArchitectureView, DeliveryView, PortfolioView, ReplayView]);
|
|
311
|
+
export const WORKBENCH_VIEWS = Object.freeze([ExecutiveView, ArchitectureView, ExploreView, DeliveryView, PortfolioView, ReplayView]);
|
|
249
312
|
|
|
250
313
|
export class DecisionWorkbench {
|
|
251
314
|
static KIND = KIND;
|
|
@@ -261,6 +324,9 @@ export class DecisionWorkbench {
|
|
|
261
324
|
throw new Error('A plan, when given, is decisionengine\'s plan.toJSON(): periods and value');
|
|
262
325
|
}
|
|
263
326
|
if (doc.before !== undefined && !isReport(doc.before)) throw new Error('An earlier report, when given, is a report.toJSON() too');
|
|
327
|
+
if (doc.graph !== undefined && !(doc.graph && Array.isArray(doc.graph.nodes) && Array.isArray(doc.graph.edges))) {
|
|
328
|
+
throw new Error('A graph, when given, is the scored portfolio as a graph document: { nodes, edges }');
|
|
329
|
+
}
|
|
264
330
|
return new DecisionWorkbench(doc, calendar);
|
|
265
331
|
}
|
|
266
332
|
|
|
@@ -269,7 +335,7 @@ export class DecisionWorkbench {
|
|
|
269
335
|
* @param {{start?: string|number, periodDays?: number}} [calendar] - when period 1 starts (default: the report's
|
|
270
336
|
* as-of business time, else today) and how long a period is (default 14 days), for the delivery plan's dates
|
|
271
337
|
*/
|
|
272
|
-
constructor({ report, plan = null, before = null }, { start, periodDays = 14 } = {}) {
|
|
338
|
+
constructor({ report, plan = null, before = null, graph = null }, { start, periodDays = 14 } = {}) {
|
|
273
339
|
const from = start ?? report.asOf?.businessTime ?? Date.now();
|
|
274
340
|
const ms = typeof from === 'number' ? from : Date.parse(from);
|
|
275
341
|
if (!Number.isFinite(ms)) throw new Error(`The calendar's start is ${JSON.stringify(start)}, not a date`);
|
|
@@ -278,6 +344,7 @@ export class DecisionWorkbench {
|
|
|
278
344
|
this.report = report;
|
|
279
345
|
this.plan = plan;
|
|
280
346
|
this.before = before;
|
|
347
|
+
this.graph = graph;
|
|
281
348
|
this.entries = [...report.entries].sort((a, b) => a.rank - b.rank);
|
|
282
349
|
this.byId = new Map(this.entries.map((e) => [e.id, e]));
|
|
283
350
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ModelFileFormat } from '../ModelFileFormat.js';
|
|
2
|
+
import { DiagramModel } from './DiagramModel.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DiagramJsonFileFormat — a diagram model as a file (`.diagram.json` or `.json`):
|
|
6
|
+
* its name, palette category, line style defaults, views with templates, edges,
|
|
7
|
+
* waypoints and arrangements (DiagramModel#toJSON), read back by DiagramModel.fromJSON.
|
|
8
|
+
*/
|
|
9
|
+
export class DiagramJsonFileFormat extends ModelFileFormat {
|
|
10
|
+
get label() { return '.diagram.json'; }
|
|
11
|
+
get accept() { return '.diagram.json,.json'; }
|
|
12
|
+
get fileName() { return 'diagrams.diagram.json'; }
|
|
13
|
+
get featureName() { return 'DiagramJson'; }
|
|
14
|
+
|
|
15
|
+
/** JSON with a diagram model's shape: templateType and views array, or views array of diagrams. */
|
|
16
|
+
recognizes(text) {
|
|
17
|
+
try {
|
|
18
|
+
const json = JSON.parse(text);
|
|
19
|
+
return !!json && typeof json === 'object' && (Array.isArray(json.views) || typeof json.templateType === 'string');
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export(model) {
|
|
26
|
+
return { data: JSON.stringify(model.toJSON(), null, 2), mimeType: 'application/json' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
import(text) {
|
|
30
|
+
return DiagramModel.fromJSON(JSON.parse(text));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -36,4 +36,43 @@ export class DiagramModel extends ViewSetModel {
|
|
|
36
36
|
setTemplateType(templateType) {
|
|
37
37
|
this.templateType = templateType;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/** The diagram model as plain JSON: its name, templateType, line style defaults, active view and views. */
|
|
41
|
+
toJSON() {
|
|
42
|
+
return {
|
|
43
|
+
version: 1,
|
|
44
|
+
name: this.name,
|
|
45
|
+
templateType: this.templateType,
|
|
46
|
+
edgeStyle: JSON.parse(JSON.stringify(this.edgeStyle ?? {})),
|
|
47
|
+
currentViewId: this.currentViewId,
|
|
48
|
+
views: this.views().map(v => v.toJSON()),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Reconstruct a DiagramModel from plain JSON. */
|
|
53
|
+
static fromJSON(json, historyOptions = {}) {
|
|
54
|
+
if (!json) throw new TypeError('DiagramModel.fromJSON requires a JSON object');
|
|
55
|
+
const model = new DiagramModel({
|
|
56
|
+
name: json.name ?? '',
|
|
57
|
+
templateType: json.templateType ?? 'generic',
|
|
58
|
+
edgeStyle: json.edgeStyle ?? {},
|
|
59
|
+
historyOptions,
|
|
60
|
+
});
|
|
61
|
+
const views = (json.views ?? []).map(vData => DiagramView.fromJSON(vData, model.historyOptions));
|
|
62
|
+
for (const view of views) {
|
|
63
|
+
model._insertView(model._adopt(view), { current: view.id === json.currentViewId });
|
|
64
|
+
}
|
|
65
|
+
if (json.currentViewId && model._views.has(json.currentViewId)) {
|
|
66
|
+
model.currentViewId = json.currentViewId;
|
|
67
|
+
} else if (model._views.size > 0 && !model.currentViewId) {
|
|
68
|
+
model.currentViewId = model._views.keys().next().value;
|
|
69
|
+
}
|
|
70
|
+
const maxNum = Math.max(0, ...model.views().map(v => {
|
|
71
|
+
const m = String(v.id).match(/\d+$/);
|
|
72
|
+
return m ? parseInt(m[0], 10) : 0;
|
|
73
|
+
}));
|
|
74
|
+
model._nextId = maxNum + 1;
|
|
75
|
+
model.markSaved();
|
|
76
|
+
return model;
|
|
77
|
+
}
|
|
39
78
|
}
|