@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.
- package/dist/renderengine-workspace.umd.js +5963 -3766
- package/dist/renderengine.umd.js +5863 -3772
- package/documentation/REFERENCE.md +37 -3
- 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 +5 -108
- package/src/charting/DiagramTypeRegistry.js +20 -1
- package/src/charting/DiagramViewPropertiesDialog.js +54 -10
- package/src/charting/Legend.js +1 -2
- package/src/charting/Statistics.js +14 -0
- 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/DiagramView.js +7 -2
- package/src/charting/diagrams/GraphDiagram3D.js +15 -0
- 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 +7 -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 -3371
- package/src/kernelExpr.js +80 -5
- package/src/workspace/parts/DiagramEditorPart.js +3 -1
- package/src/workspace/parts/ViewsPart.js +106 -0
|
@@ -2,26 +2,33 @@ import { ViewPropertiesDialog } from './ViewPropertiesDialog.js';
|
|
|
2
2
|
import { inheritableFields, readInheritableFields } from './SettingsCascade.js';
|
|
3
3
|
import { EDGE_STYLE_PROPERTIES } from './edgeStyle/EdgeStyleSchema.js';
|
|
4
4
|
import { SetStyleCommand } from './edgeStyle/SetStyleCommand.js';
|
|
5
|
+
import { CompositeCommand } from './CommandHistory.js';
|
|
6
|
+
|
|
7
|
+
const TITLE_POSITIONS = [['none', 'None'], ['top', 'Top'], ['bottom', 'Bottom']];
|
|
8
|
+
const WATERMARK_ROTATIONS = [['0°', 'Horizontal (0°)'], ['-45°', 'Diagonal (-45°)'], ['-90°', 'Vertical (-90°)']];
|
|
9
|
+
const WATERMARK_REPEATS = [['none', 'Center (single)'], ['repeat', 'Repeated grid'], ['tile', 'Tiled pattern']];
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* DiagramViewPropertiesDialog — a template-editor diagram's properties plus
|
|
8
|
-
* its line-style defaults
|
|
9
|
-
* (diagram → model → tool)
|
|
10
|
-
*
|
|
11
|
-
* DiagramEditorPart.
|
|
13
|
+
* its line-style defaults and visual appearance (background, title band, watermark):
|
|
14
|
+
* the diagram level of the line-style cascade (diagram → model → tool) and
|
|
15
|
+
* the diagram appearance settings. Changes are undoable on the diagram's history.
|
|
16
|
+
* Used by DiagramEditorPart.
|
|
12
17
|
*/
|
|
13
18
|
export class DiagramViewPropertiesDialog extends ViewPropertiesDialog {
|
|
14
19
|
/**
|
|
15
20
|
* @param {Object} options - As ViewPropertiesDialog, plus:
|
|
16
21
|
* @param {EdgeStyleCascade} options.cascade - The diagram's line-style levels, the diagram's own keyed `levelKey`
|
|
17
22
|
* @param {string} [options.levelKey] - Which level is this diagram's
|
|
18
|
-
* @param {() => void} [options.onStyleChange] - After the style changes
|
|
23
|
+
* @param {() => void} [options.onStyleChange] - After the line style changes
|
|
24
|
+
* @param {() => void} [options.onSettingsChange] - After the appearance settings change
|
|
19
25
|
*/
|
|
20
|
-
constructor({ cascade, levelKey = 'view', onStyleChange = () => {}, ...rest }) {
|
|
26
|
+
constructor({ cascade, levelKey = 'view', onStyleChange = () => {}, onSettingsChange = () => {}, ...rest }) {
|
|
21
27
|
super(rest);
|
|
22
28
|
this.cascade = cascade;
|
|
23
29
|
this.levelKey = levelKey;
|
|
24
30
|
this.onStyleChange = onStyleChange;
|
|
31
|
+
this.onSettingsChange = onSettingsChange;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/** The diagram's own line style (its level's style object). */
|
|
@@ -30,17 +37,54 @@ export class DiagramViewPropertiesDialog extends ViewPropertiesDialog {
|
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
sections() {
|
|
40
|
+
const ownSettings = this.view.viewSettings ?? {};
|
|
33
41
|
return [
|
|
34
42
|
{ type: 'heading', label: 'Line style' },
|
|
35
43
|
...inheritableFields(EDGE_STYLE_PROPERTIES, this.style, this.cascade.inheritedBy(this.levelKey)),
|
|
44
|
+
{ type: 'heading', label: 'Appearance' },
|
|
45
|
+
{ key: 'background', label: 'Background', type: 'color', value: ownSettings.background || '#ffffff' },
|
|
46
|
+
{ key: 'backgroundImage', label: 'Background image', type: 'text', value: ownSettings.backgroundImage ?? '', placeholder: 'URL (optional)' },
|
|
47
|
+
{ key: 'bandTitle', label: 'Title band', type: 'text', value: ownSettings.title ?? '', placeholder: 'Text shown above or below the diagram' },
|
|
48
|
+
{ key: 'titlePosition', label: 'Title position', type: 'select', value: ownSettings.titlePosition || 'none', choices: TITLE_POSITIONS },
|
|
49
|
+
{ key: 'watermark', label: 'Watermark', type: 'text', value: ownSettings.watermark ?? '', placeholder: 'CONFIDENTIAL, DRAFT, ...' },
|
|
50
|
+
{ key: 'watermarkRotation', label: 'Watermark rotation', type: 'select', value: ownSettings.watermarkRotation || '-45°', choices: WATERMARK_ROTATIONS },
|
|
51
|
+
{ key: 'watermarkOpacity', label: 'Watermark opacity', type: 'number', min: 0, max: 100, step: 5, value: ownSettings.watermarkOpacity ?? 20 },
|
|
52
|
+
{ key: 'watermarkRepeat', label: 'Watermark repeat', type: 'select', value: ownSettings.watermarkRepeat || 'none', choices: WATERMARK_REPEATS },
|
|
36
53
|
];
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
applySections(values) {
|
|
57
|
+
// Line style
|
|
40
58
|
const patch = readInheritableFields(EDGE_STYLE_PROPERTIES, values);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
59
|
+
const styleChanged = Object.entries(patch).some(([k, v]) => this.style[k] !== v);
|
|
60
|
+
|
|
61
|
+
// Appearance
|
|
62
|
+
const ownSettings = this.view.viewSettings ?? (this.view.viewSettings = {});
|
|
63
|
+
const appearancePatch = {};
|
|
64
|
+
if ('background' in values) appearancePatch.background = values.background;
|
|
65
|
+
if ('backgroundImage' in values) appearancePatch.backgroundImage = values.backgroundImage?.trim() || undefined;
|
|
66
|
+
if ('bandTitle' in values) appearancePatch.title = values.bandTitle?.trim() || undefined;
|
|
67
|
+
if ('titlePosition' in values) appearancePatch.titlePosition = values.titlePosition && values.titlePosition !== 'none' ? values.titlePosition : undefined;
|
|
68
|
+
if ('watermark' in values) appearancePatch.watermark = values.watermark?.trim() || undefined;
|
|
69
|
+
if ('watermarkRotation' in values) appearancePatch.watermarkRotation = values.watermarkRotation && values.watermarkRotation !== '-45°' ? values.watermarkRotation : undefined;
|
|
70
|
+
if ('watermarkOpacity' in values) appearancePatch.watermarkOpacity = values.watermarkOpacity != null && Number(values.watermarkOpacity) !== 20 ? Number(values.watermarkOpacity) : undefined;
|
|
71
|
+
if ('watermarkRepeat' in values) appearancePatch.watermarkRepeat = values.watermarkRepeat && values.watermarkRepeat !== 'none' ? values.watermarkRepeat : undefined;
|
|
72
|
+
|
|
73
|
+
const appearanceChanged = Object.entries(appearancePatch).some(([k, v]) => ownSettings[k] !== v);
|
|
74
|
+
|
|
75
|
+
if (styleChanged && appearanceChanged) {
|
|
76
|
+
this.view.history.execute(new CompositeCommand('Diagram properties', [
|
|
77
|
+
new SetStyleCommand(this.style, patch, 'Diagram line style'),
|
|
78
|
+
new SetStyleCommand(ownSettings, appearancePatch, 'Diagram appearance'),
|
|
79
|
+
]));
|
|
80
|
+
this.onStyleChange();
|
|
81
|
+
this.onSettingsChange();
|
|
82
|
+
} else if (styleChanged) {
|
|
83
|
+
this.view.history.execute(new SetStyleCommand(this.style, patch, 'Diagram line style'));
|
|
84
|
+
this.onStyleChange();
|
|
85
|
+
} else if (appearanceChanged) {
|
|
86
|
+
this.view.history.execute(new SetStyleCommand(ownSettings, appearancePatch, 'Diagram appearance'));
|
|
87
|
+
this.onSettingsChange();
|
|
88
|
+
}
|
|
45
89
|
}
|
|
46
90
|
}
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -18,11 +18,12 @@ export class DiagramView extends DiagramEditorState {
|
|
|
18
18
|
* @param {Map<string, Template>} [options.templates]
|
|
19
19
|
* @param {Map<string, Edge>} [options.edges]
|
|
20
20
|
* @param {Object} [options.edgeStyle] - Diagram-level line style defaults
|
|
21
|
+
* @param {Object} [options.viewSettings] - Diagram appearance: background, watermark, title band
|
|
21
22
|
* @param {Object} [options.historyOptions] - Passed to CommandHistory (e.g. onChange)
|
|
22
23
|
* @param {string} [options.documentation]
|
|
23
24
|
* @param {Arrangement} [options.arrangement]
|
|
24
25
|
*/
|
|
25
|
-
constructor({ id, title, templates = new Map(), edges = new Map(), edgeStyle = {}, historyOptions = {}, documentation = undefined, arrangement = null }) {
|
|
26
|
+
constructor({ id, title, templates = new Map(), edges = new Map(), edgeStyle = {}, viewSettings = {}, historyOptions = {}, documentation = undefined, arrangement = null }) {
|
|
26
27
|
super({ historyOptions, arrangement });
|
|
27
28
|
this.historyOptions = historyOptions;
|
|
28
29
|
this.id = id;
|
|
@@ -31,6 +32,7 @@ export class DiagramView extends DiagramEditorState {
|
|
|
31
32
|
this.templates = templates;
|
|
32
33
|
this.edges = edges;
|
|
33
34
|
this.edgeStyle = edgeStyle;
|
|
35
|
+
this.viewSettings = viewSettings;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/** The shapes are what stack. */
|
|
@@ -52,6 +54,7 @@ export class DiagramView extends DiagramEditorState {
|
|
|
52
54
|
})),
|
|
53
55
|
edges: [...this.edges.values()].map(e => (typeof e.toJSON === 'function' ? e.toJSON() : JSON.parse(JSON.stringify(e)))),
|
|
54
56
|
edgeStyle: JSON.parse(JSON.stringify(this.edgeStyle ?? {})),
|
|
57
|
+
...(this.viewSettings && Object.keys(this.viewSettings).length ? { viewSettings: JSON.parse(JSON.stringify(this.viewSettings)) } : {}),
|
|
55
58
|
...(this.arrangement && { arrangement: this.arrangement.toJSON() }),
|
|
56
59
|
};
|
|
57
60
|
}
|
|
@@ -75,6 +78,7 @@ export class DiagramView extends DiagramEditorState {
|
|
|
75
78
|
templates,
|
|
76
79
|
edges,
|
|
77
80
|
edgeStyle: json.edgeStyle ?? {},
|
|
81
|
+
viewSettings: json.viewSettings ? JSON.parse(JSON.stringify(json.viewSettings)) : {},
|
|
78
82
|
documentation: json.documentation,
|
|
79
83
|
historyOptions,
|
|
80
84
|
arrangement,
|
|
@@ -89,9 +93,10 @@ export class DiagramView extends DiagramEditorState {
|
|
|
89
93
|
templates: new Map([...this.templates].map(([tid, t]) => [tid, new t.constructor(tid, t.name, t.serialize())])),
|
|
90
94
|
edges: new Map([...this.edges].map(([eid, e]) => [eid, e.clone()])),
|
|
91
95
|
edgeStyle: { ...this.edgeStyle },
|
|
96
|
+
viewSettings: { ...(this.viewSettings ?? {}) },
|
|
92
97
|
documentation: this.documentation,
|
|
93
98
|
historyOptions,
|
|
94
|
-
arrangement: this.arrangement
|
|
99
|
+
arrangement: this.arrangement?.copy() ?? null, // template ids are kept
|
|
95
100
|
});
|
|
96
101
|
}
|
|
97
102
|
}
|
|
@@ -149,4 +149,19 @@ export class GraphDiagram3D extends Diagram3D {
|
|
|
149
149
|
}
|
|
150
150
|
return pos;
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Asynchronous 3D force layout simulation, offloading to GPU when backend provided.
|
|
155
|
+
* @param {Array<{id: string, x?: number, y?: number, z?: number}>} nodes
|
|
156
|
+
* @param {Array<{source: string, target: string}>} edges
|
|
157
|
+
* @param {object} [options]
|
|
158
|
+
* @param {import('../../compute/ComputeBackend.js').ComputeBackend} [options.backend]
|
|
159
|
+
* @returns {Promise<Map<string, {x: number, y: number, z: number, vx: number, vy: number, vz: number}>>}
|
|
160
|
+
*/
|
|
161
|
+
static async layoutForce3DAsync(nodes, edges, options = {}) {
|
|
162
|
+
if (options.backend) {
|
|
163
|
+
return options.backend.layoutForce3DAsync(nodes, edges, options);
|
|
164
|
+
}
|
|
165
|
+
return GraphDiagram3D.layoutForce3D(nodes, edges, options);
|
|
166
|
+
}
|
|
152
167
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphSource — what a graph walker reads, whatever the graph is.
|
|
3
|
+
*
|
|
4
|
+
* `NavigatorModel` (step-by-step exploration, `findPaths`, `traceImpact`) asks a graph four questions:
|
|
5
|
+
*
|
|
6
|
+
* - `getElement(id)` — the element, `{ id, name, type, ... }`, or null;
|
|
7
|
+
* - `relationshipsFor(id)` — `{ incoming, outgoing }`, each relationship `{ id, source, target, kind }`;
|
|
8
|
+
* - `viewsContaining(id)` — the views that draw it (none, for a graph that has no views);
|
|
9
|
+
* - `layerOf(element)` — the band it belongs to, for grouping and colour.
|
|
10
|
+
*
|
|
11
|
+
* Why an interface. Traversal is the same for any graph; only these reads differ. So there is one walker,
|
|
12
|
+
* and each kind of graph answers the four questions: `ArchimateModel` answers them already (its layers
|
|
13
|
+
* from `treeLayerOf`), and `DocumentGraph` answers them for a graph document — `{ nodes, edges }`, the
|
|
14
|
+
* shape sqlengine stores graphs in and decisionengine scores — so a portfolio, a dependency graph or any
|
|
15
|
+
* stored graph is explored the way an ArchiMate model is, without a second walker.
|
|
16
|
+
*
|
|
17
|
+
* A subclass implements the four reads; this base says what is missing rather than failing obscurely.
|
|
18
|
+
*/
|
|
19
|
+
export class GraphSource {
|
|
20
|
+
getElement() { throw new Error(`${this.constructor.name} must implement getElement(id)`); }
|
|
21
|
+
relationshipsFor() { throw new Error(`${this.constructor.name} must implement relationshipsFor(id)`); }
|
|
22
|
+
viewsContaining() { return []; }
|
|
23
|
+
layerOf(element) { return element?.type ?? 'other'; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* DocumentGraph — a graph document (`{ nodes: [{ id, labels, props }], edges: [{ id?, from, to, type, props }] }`)
|
|
28
|
+
* as a GraphSource. A node's type is its first label and its name `props.name` (else its id); an edge's
|
|
29
|
+
* kind is its `type`, its id its own or one made from its ends. Its layer is its type, so a portfolio's
|
|
30
|
+
* capabilities, epics and objectives band apart. Indexed once, read many.
|
|
31
|
+
*/
|
|
32
|
+
export class DocumentGraph extends GraphSource {
|
|
33
|
+
/** True for something shaped like a graph document. */
|
|
34
|
+
static isDocument(doc) {
|
|
35
|
+
return Boolean(doc) && Array.isArray(doc.nodes) && (doc.edges === undefined || Array.isArray(doc.edges));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor(doc) {
|
|
39
|
+
super();
|
|
40
|
+
if (!DocumentGraph.isDocument(doc)) throw new Error('A graph document has a nodes list (and an edges list)');
|
|
41
|
+
this.elements = new Map();
|
|
42
|
+
for (const node of doc.nodes) {
|
|
43
|
+
if (node?.id == null) continue;
|
|
44
|
+
this.elements.set(node.id, Object.freeze({
|
|
45
|
+
id: node.id, name: node.props?.name ?? String(node.id), type: node.labels?.[0] ?? 'Node',
|
|
46
|
+
labels: node.labels ?? [], props: node.props ?? {},
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
this.incoming = new Map();
|
|
50
|
+
this.outgoing = new Map();
|
|
51
|
+
(doc.edges ?? []).forEach((edge, i) => {
|
|
52
|
+
if (!this.elements.has(edge?.from) || !this.elements.has(edge?.to)) return;
|
|
53
|
+
const relationship = Object.freeze({
|
|
54
|
+
id: edge.id ?? `${edge.from}-${edge.type}-${edge.to}-${i}`, source: edge.from, target: edge.to,
|
|
55
|
+
kind: edge.type, name: edge.props?.name ?? edge.type, props: edge.props ?? {},
|
|
56
|
+
});
|
|
57
|
+
push(this.outgoing, edge.from, relationship);
|
|
58
|
+
push(this.incoming, edge.to, relationship);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getElement(id) { return this.elements.get(id) ?? null; }
|
|
63
|
+
|
|
64
|
+
relationshipsFor(id) {
|
|
65
|
+
return { incoming: this.incoming.get(id) ?? [], outgoing: this.outgoing.get(id) ?? [] };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function push(map, key, value) {
|
|
70
|
+
if (!map.has(key)) map.set(key, []);
|
|
71
|
+
map.get(key).push(value);
|
|
72
|
+
}
|