@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.
- package/README.md +480 -302
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49217 -37484
- package/dist/renderengine.umd.js +24899 -13093
- package/documentation/REFERENCE.md +68 -1
- package/package.json +13 -3
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +269 -3
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +7 -2
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +142 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/ask/AskBar.js +388 -0
- package/src/charting/ask/AskView.js +400 -0
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +69 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +6 -4
- package/src/charting/charts/BarChart3DOrbit.js +3 -1
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +25 -3
- package/src/charting/charts/BubbleChart.js +4 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +107 -4
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DensityChart.js +1 -1
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +5 -2
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +6 -3
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HistogramChart.js +2 -2
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/IcicleChart.js +47 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/LollipopChart.js +104 -0
- package/src/charting/charts/MarimekkoChart.js +75 -0
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/OutlineChart.js +10 -8
- package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
- package/src/charting/charts/ParetoChart.js +92 -0
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PipeChart.js +5 -2
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/PopulationPyramidChart.js +4 -4
- package/src/charting/charts/RadarChart.js +12 -5
- package/src/charting/charts/RadialBarChart.js +6 -3
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +129 -6
- package/src/charting/charts/ScatterChart.js +19 -3
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +125 -11
- package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
- package/src/charting/charts/StreamGraphChart.js +18 -3
- package/src/charting/charts/SunburstChart.js +28 -8
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +31 -11
- package/src/charting/charts/ViolinChart.js +20 -2
- package/src/charting/charts/WaterfallChart.js +2 -2
- package/src/charting/charts/WordCloudChart.js +2 -2
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +310 -13
- package/src/charting/pivot/PivotGridChart.js +322 -34
- package/src/charting/pivot/PivotView.js +626 -21
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2899
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/styles.js +36 -1
|
@@ -1,85 +1,110 @@
|
|
|
1
|
-
/** Words SQL keeps for itself that a name may not be left bare as. */
|
|
2
|
-
const RESERVED = new Set(['user', 'order', 'group', 'table', 'select', 'from', 'where', 'by', 'to', 'key', 'check', 'default', 'index', 'primary', 'foreign', 'references', 'unique', 'column', 'constraint', 'value', 'values', 'date', 'time', 'timestamp', 'level', 'position', 'role']);
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* SqlDdl — a model's physical level as SQL DDL (DMBOK2's data integration: the
|
|
6
|
-
* design handed to a database): a CREATE TABLE per table, its columns with
|
|
7
|
-
* their types (`defaultType` where a column has none) and NOT NULL where
|
|
8
|
-
* required, its primary key and UNIQUE columns; a FOREIGN KEY per column that
|
|
9
|
-
* references another (`references`: a table, or `table.column`, by id or name;
|
|
10
|
-
* a table alone: its primary key); and COMMENT ONs for definitions. Names are
|
|
11
|
-
* quoted where SQL would not take them bare. ANSI SQL, as PostgreSQL reads it.
|
|
12
|
-
*/
|
|
13
|
-
export class SqlDdl {
|
|
14
|
-
/**
|
|
15
|
-
* @param {Object} [options]
|
|
16
|
-
* @param {string} [options.defaultType] - A column's type when it names none
|
|
17
|
-
* @param {boolean} [options.comments] - Whether definitions become COMMENT ONs
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
...columns.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
1
|
+
/** Words SQL keeps for itself that a name may not be left bare as. */
|
|
2
|
+
const RESERVED = new Set(['user', 'order', 'group', 'table', 'select', 'from', 'where', 'by', 'to', 'key', 'check', 'default', 'index', 'primary', 'foreign', 'references', 'unique', 'column', 'constraint', 'value', 'values', 'date', 'time', 'timestamp', 'level', 'position', 'role']);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SqlDdl — a model's physical level as SQL DDL (DMBOK2's data integration: the
|
|
6
|
+
* design handed to a database): a CREATE TABLE per table, its columns with
|
|
7
|
+
* their types (`defaultType` where a column has none) and NOT NULL where
|
|
8
|
+
* required, its primary key and UNIQUE columns; a FOREIGN KEY per column that
|
|
9
|
+
* references another (`references`: a table, or `table.column`, by id or name;
|
|
10
|
+
* a table alone: its primary key); and COMMENT ONs for definitions. Names are
|
|
11
|
+
* quoted where SQL would not take them bare. ANSI SQL, as PostgreSQL reads it.
|
|
12
|
+
*/
|
|
13
|
+
export class SqlDdl {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} [options]
|
|
16
|
+
* @param {string} [options.defaultType] - A column's type when it names none
|
|
17
|
+
* @param {boolean} [options.comments] - Whether definitions become COMMENT ONs
|
|
18
|
+
* @param {boolean|Array<string>|Set<string>|Object} [options.external] - Trust foreign keys to tables outside the model
|
|
19
|
+
*/
|
|
20
|
+
constructor({ defaultType = 'VARCHAR(255)', comments = true, external = false } = {}) {
|
|
21
|
+
this.defaultType = defaultType;
|
|
22
|
+
this.comments = comments;
|
|
23
|
+
this.external = external;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A name as SQL takes it: bare when it can be, else double-quoted. */
|
|
27
|
+
static name(text) {
|
|
28
|
+
const s = String(text);
|
|
29
|
+
return /^[a-z_][a-z0-9_]*$/.test(s) && !RESERVED.has(s) ? s : `"${s.replace(/"/g, '""')}"`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A table's name as SQL takes it: a schema-qualified one (`sales.customer`)
|
|
34
|
+
* each part as name() has it, so it names the table in its schema rather
|
|
35
|
+
* than one table called "sales.customer".
|
|
36
|
+
*/
|
|
37
|
+
static tableName(text) {
|
|
38
|
+
return String(text).split('.').map(SqlDdl.name).join('.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A text as an SQL string literal. */
|
|
42
|
+
static literal(text) {
|
|
43
|
+
return `'${String(text).replace(/'/g, "''")}'`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The DDL for a model's physical level. @returns {string} */
|
|
47
|
+
write(model, { external = this.external } = {}) {
|
|
48
|
+
const schema = model.level('physical');
|
|
49
|
+
const tables = schema.entities;
|
|
50
|
+
const table = SqlDdl.tableName;
|
|
51
|
+
const out = [`-- ${model.name || 'Data model'}: the physical level, ${tables.length} table${tables.length === 1 ? '' : 's'}`, ''];
|
|
52
|
+
for (const t of tables) {
|
|
53
|
+
const columns = t.attributes ?? [];
|
|
54
|
+
const keys = columns.filter(c => c.isPK).map(c => SqlDdl.name(c.name));
|
|
55
|
+
const lines = [
|
|
56
|
+
...columns.map(c => ` ${SqlDdl.name(c.name)} ${c.type || this.defaultType}${c.required || c.isPK ? ' NOT NULL' : ''}`),
|
|
57
|
+
...(keys.length ? [` PRIMARY KEY (${keys.join(', ')})`] : []),
|
|
58
|
+
...columns.filter(c => c.isUnique && !c.isPK).map(c => ` UNIQUE (${SqlDdl.name(c.name)})`),
|
|
59
|
+
];
|
|
60
|
+
out.push(`CREATE TABLE ${table(t.name)} (`, lines.join(',\n'), ');', '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isExternalTrusted = (tableName) => {
|
|
64
|
+
if (external === true) return true;
|
|
65
|
+
if (external instanceof Set) return external.has(tableName);
|
|
66
|
+
if (Array.isArray(external)) return external.includes(tableName);
|
|
67
|
+
if (external && typeof external === 'object') return !!external[tableName];
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const t of tables) {
|
|
72
|
+
for (const c of (t.attributes ?? []).filter(a => a.isFK && a.references)) {
|
|
73
|
+
const ref = schema.resolveReference(c.references);
|
|
74
|
+
if (!ref) {
|
|
75
|
+
const raw = String(c.references).trim();
|
|
76
|
+
const dot = raw.lastIndexOf('.');
|
|
77
|
+
let targetTable = raw;
|
|
78
|
+
let targetCol = null;
|
|
79
|
+
if (dot > 0) {
|
|
80
|
+
targetTable = raw.slice(0, dot);
|
|
81
|
+
targetCol = raw.slice(dot + 1);
|
|
82
|
+
}
|
|
83
|
+
if (isExternalTrusted(targetTable)) {
|
|
84
|
+
const colRef = targetCol ? ` (${SqlDdl.name(targetCol)})` : '';
|
|
85
|
+
out.push(`ALTER TABLE ${table(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
|
|
86
|
+
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${table(targetTable)}${colRef};`);
|
|
87
|
+
} else {
|
|
88
|
+
out.push(`-- ${t.name}.${c.name} references ${c.references}, which is not a table here`);
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const theirs = ref.column ? [ref.column] : (ref.entity.attributes ?? []).filter(a => a.isPK).map(a => a.name);
|
|
93
|
+
if (!theirs.length) {
|
|
94
|
+
out.push(`-- ${t.name}.${c.name} references ${ref.entity.name}, which has no primary key`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
out.push(`ALTER TABLE ${table(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
|
|
98
|
+
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${table(ref.entity.name)} (${theirs.map(SqlDdl.name).join(', ')});`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (this.comments) {
|
|
102
|
+
const notes = tables.flatMap(t => [
|
|
103
|
+
...(t.description ? [`COMMENT ON TABLE ${table(t.name)} IS ${SqlDdl.literal(t.description)};`] : []),
|
|
104
|
+
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${table(t.name)}.${SqlDdl.name(c.name)} IS ${SqlDdl.literal(c.description)};`),
|
|
105
|
+
]);
|
|
106
|
+
if (notes.length) out.push('', ...notes);
|
|
107
|
+
}
|
|
108
|
+
return `${out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -19,3 +19,6 @@ export { DataModelFileFormat } from './DataModelFileFormat.js';
|
|
|
19
19
|
export { SqlDdl } from './SqlDdl.js';
|
|
20
20
|
export { SqlDdlFileFormat } from './SqlDdlFileFormat.js';
|
|
21
21
|
export { LevelEntitiesCompared, LevelRelationshipsCompared, MappingsCompared, ContextsCompared, PerspectivesCompared, DataTypesCompared } from './DataModelCompared.js';
|
|
22
|
+
export { ModelMapping } from './ModelMapping.js';
|
|
23
|
+
export { DataLineage } from './DataLineage.js';
|
|
24
|
+
export { ArchimateDataLink } from './ArchimateDataLink.js';
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// Copyright 2026 Jaroslav Peter Prib, Purveyor of Excellence
|
|
2
|
+
/**
|
|
3
|
+
* DecisionWorkbench — the Decision Workbench's views of a SADIS recommendation
|
|
4
|
+
* (decisionengine), as chart specs this library draws.
|
|
5
|
+
*
|
|
6
|
+
* It reads decisionengine's JSON, not decisionengine: a workbench document
|
|
7
|
+
*
|
|
8
|
+
* { kind: 'sadis-workbench', report, plan?, before? }
|
|
9
|
+
*
|
|
10
|
+
* holds a report's `toJSON()` (the ranked candidates, each with its score's
|
|
11
|
+
* parts), optionally a delivery plan's `toJSON()` and an earlier report to
|
|
12
|
+
* replay against. Each view is a `WorkbenchView` subclass naming the charts it
|
|
13
|
+
* shows (`{ title, chartType, data, options }`, `chartType` a CHART_CATALOG key)
|
|
14
|
+
* and notes for what a chart cannot show; a view needing what the document
|
|
15
|
+
* lacks (a plan, perspectives, an earlier report) is not offered.
|
|
16
|
+
*
|
|
17
|
+
* DOM-free: a workspace page puts each chart in a ChartPart.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const KIND = 'sadis-workbench';
|
|
21
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
22
|
+
const pct = (share) => Math.round(share * 1000) / 10;
|
|
23
|
+
const round = (n) => Math.round(n * 1000) / 1000;
|
|
24
|
+
|
|
25
|
+
/** One view of the workbench: what it is called, whether the document can fill it, its charts and notes. */
|
|
26
|
+
export class WorkbenchView {
|
|
27
|
+
static id = 'view';
|
|
28
|
+
static title = 'View';
|
|
29
|
+
|
|
30
|
+
constructor(workbench) { this.workbench = workbench; }
|
|
31
|
+
|
|
32
|
+
get id() { return this.constructor.id; }
|
|
33
|
+
get title() { return this.constructor.title; }
|
|
34
|
+
|
|
35
|
+
/** Whether the document holds what this view needs. */
|
|
36
|
+
available() { return true; }
|
|
37
|
+
|
|
38
|
+
/** @returns {Array<{title: string, chartType: string, data: *, options?: object}>} */
|
|
39
|
+
charts() { return []; }
|
|
40
|
+
|
|
41
|
+
/** Plain lines for what the charts cannot show. */
|
|
42
|
+
notes() { return []; }
|
|
43
|
+
|
|
44
|
+
toJSON() {
|
|
45
|
+
return { id: this.id, title: this.title, charts: this.charts(), notes: this.notes() };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What to build next, how the scores are made up, how fast value arrives. */
|
|
50
|
+
export class ExecutiveView extends WorkbenchView {
|
|
51
|
+
static id = 'executive';
|
|
52
|
+
static title = 'Executive';
|
|
53
|
+
|
|
54
|
+
charts() {
|
|
55
|
+
const { entries, plan } = this.workbench;
|
|
56
|
+
const top = entries[0];
|
|
57
|
+
const charts = [{ title: `Build next: ${top.name}`, chartType: 'kpi-tile', data: { value: round(top.score) } }];
|
|
58
|
+
if (plan) {
|
|
59
|
+
const half = plan.value.find((v) => v.share >= 0.5);
|
|
60
|
+
const last = plan.value[plan.value.length - 1];
|
|
61
|
+
charts.push(half
|
|
62
|
+
? { title: `Half the recommended value delivered by period ${half.period} (%)`, chartType: 'kpi-tile', data: { value: pct(half.share) } }
|
|
63
|
+
: { title: `Value delivered by period ${last?.period ?? 0} (%)`, chartType: 'kpi-tile', data: { value: pct(last?.share ?? 0) } });
|
|
64
|
+
}
|
|
65
|
+
charts.push({
|
|
66
|
+
title: 'Score, part by part',
|
|
67
|
+
chartType: 'stackedBar',
|
|
68
|
+
data: ['basin', 'unlock', 'influence'].map((part) => ({
|
|
69
|
+
name: { basin: 'rating', unlock: 'unlock', influence: 'influence' }[part],
|
|
70
|
+
values: entries.map((e) => ({ label: e.name, value: round(e.flow[part]) })),
|
|
71
|
+
})),
|
|
72
|
+
});
|
|
73
|
+
if (plan) {
|
|
74
|
+
charts.push({
|
|
75
|
+
title: 'Value delivered by the end of each period (%)',
|
|
76
|
+
chartType: 'line',
|
|
77
|
+
data: plan.value.map((v) => ({ label: `P${v.period}`, value: pct(v.share) })),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return charts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
notes() {
|
|
84
|
+
const top = this.workbench.entries[0];
|
|
85
|
+
return [`#1 ${top.name} (${top.type}): score ${round(top.score)}`];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Where value hides: the rating each candidate shows against what it unlocks and reaches. */
|
|
90
|
+
export class ArchitectureView extends WorkbenchView {
|
|
91
|
+
static id = 'architecture';
|
|
92
|
+
static title = 'Architecture';
|
|
93
|
+
|
|
94
|
+
charts() {
|
|
95
|
+
const { entries } = this.workbench;
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
title: 'Visible rating against basin position (unlock + influence): foundations sit high and left',
|
|
99
|
+
chartType: 'scatter',
|
|
100
|
+
data: entries.map((e) => ({ label: e.name, x: round(e.flow.basin), y: round(e.flow.unlock + e.flow.influence), type: e.type })),
|
|
101
|
+
options: { colorCol: 'type' },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
title: 'Share of other candidates each one frees (%)',
|
|
105
|
+
chartType: 'lollipop',
|
|
106
|
+
data: [...entries].sort((a, b) => b.basin.unlock - a.basin.unlock).map((e) => ({ label: e.name, value: pct(e.basin.unlock) })),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
notes() {
|
|
112
|
+
return this.workbench.entries
|
|
113
|
+
.filter((e) => e.basin.unlocked.length)
|
|
114
|
+
.map((e) => `${e.name} frees ${e.basin.unlocked.join(', ')}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The delivery plan: when each candidate is worked on, and how full each period is. */
|
|
119
|
+
export class DeliveryView extends WorkbenchView {
|
|
120
|
+
static id = 'delivery';
|
|
121
|
+
static title = 'Delivery';
|
|
122
|
+
|
|
123
|
+
available() { return Boolean(this.workbench.plan); }
|
|
124
|
+
|
|
125
|
+
/** Period `p` (from 1) as the date it starts: the workbench's calendar. */
|
|
126
|
+
periodStart(p) {
|
|
127
|
+
const { start, periodDays } = this.workbench.calendar;
|
|
128
|
+
return new Date(start + (p - 1) * periodDays * DAY).toISOString().slice(0, 10);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Each planned candidate's first period worked and the period it is finished in. */
|
|
132
|
+
spans() {
|
|
133
|
+
const { plan } = this.workbench;
|
|
134
|
+
const start = new Map();
|
|
135
|
+
const end = new Map();
|
|
136
|
+
for (const p of plan.periods) {
|
|
137
|
+
for (const id of p.working ?? p.items) if (!start.has(id)) start.set(id, p.period);
|
|
138
|
+
for (const id of p.items) end.set(id, p.period);
|
|
139
|
+
}
|
|
140
|
+
return [...end].map(([id, finished]) => ({ id, start: start.get(id) ?? finished, end: finished }));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
charts() {
|
|
144
|
+
const { plan } = this.workbench;
|
|
145
|
+
const name = (id) => this.workbench.entry(id)?.name ?? id;
|
|
146
|
+
const { periodDays } = this.workbench.calendar;
|
|
147
|
+
const charts = [{
|
|
148
|
+
title: `Delivery plan (${plan.order} order${plan.search?.swaps ? `, searched: ${plan.search.swaps} swaps` : ''}): ${periodDays}-day periods from ${this.periodStart(1)}`,
|
|
149
|
+
chartType: 'gantt',
|
|
150
|
+
data: this.spans().map((s) => ({ label: name(s.id), start: this.periodStart(s.start), end: this.periodStart(s.end + 1), category: this.workbench.entry(s.id)?.type })),
|
|
151
|
+
options: { dateFormat: 'iso', showToday: false },
|
|
152
|
+
}];
|
|
153
|
+
const teams = plan.periods[0]?.teams ? Object.keys(plan.periods[0].teams) : null;
|
|
154
|
+
charts.push({
|
|
155
|
+
title: 'Capacity used per period',
|
|
156
|
+
chartType: 'stackedBar',
|
|
157
|
+
data: teams
|
|
158
|
+
? teams.map((team) => ({ name: team, values: plan.periods.map((p) => ({ label: `P${p.period}`, value: p.teams[team].used })) }))
|
|
159
|
+
: [
|
|
160
|
+
{ name: 'used', values: plan.periods.map((p) => ({ label: `P${p.period}`, value: p.used })) },
|
|
161
|
+
{ name: 'free', values: plan.periods.map((p) => ({ label: `P${p.period}`, value: p.capacity - p.used })) },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
return charts;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
notes() {
|
|
168
|
+
const name = (id) => this.workbench.entry(id)?.name ?? id;
|
|
169
|
+
return this.workbench.plan.excluded.map((x) => `Not planned: ${name(x.id)} (${x.why})`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Where the stakeholders' perspectives agree and where they pull apart. */
|
|
174
|
+
export class PortfolioView extends WorkbenchView {
|
|
175
|
+
static id = 'portfolio';
|
|
176
|
+
static title = 'Portfolio';
|
|
177
|
+
|
|
178
|
+
available() { return Boolean(this.workbench.report.perspectives); }
|
|
179
|
+
|
|
180
|
+
charts() {
|
|
181
|
+
const { entries } = this.workbench;
|
|
182
|
+
const n = entries.length;
|
|
183
|
+
const contested = entries.filter((e) => e.spread > 0).sort((a, b) => b.spread - a.spread || a.rank - b.rank);
|
|
184
|
+
return [
|
|
185
|
+
{
|
|
186
|
+
title: 'Rank by perspective (darker: ranked higher)',
|
|
187
|
+
chartType: 'heatmap',
|
|
188
|
+
data: entries.flatMap((e) => e.perspectives.map((p) => ({ x: p.id, y: e.name, value: n - p.rank + 1 }))),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
title: 'Best and worst rank across perspectives, most contested first',
|
|
192
|
+
chartType: 'dumbbell',
|
|
193
|
+
data: contested.map((e) => {
|
|
194
|
+
const ranks = e.perspectives.filter((p) => p.weight > 0).map((p) => p.rank);
|
|
195
|
+
return { label: e.name, start: Math.min(...ranks), end: Math.max(...ranks) };
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
notes() {
|
|
202
|
+
return Object.entries(this.workbench.report.perspectives).map(([id, weight]) => `${id}: weight ${weight}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** What changed since the earlier report: ranks then and now, what entered and what left. */
|
|
207
|
+
export class ReplayView extends WorkbenchView {
|
|
208
|
+
static id = 'replay';
|
|
209
|
+
static title = 'Replay';
|
|
210
|
+
|
|
211
|
+
available() { return Boolean(this.workbench.before); }
|
|
212
|
+
|
|
213
|
+
/** Each candidate's rank before and now; those only now, and those only before. */
|
|
214
|
+
change() {
|
|
215
|
+
const was = new Map(this.workbench.before.entries.map((e) => [e.id, e]));
|
|
216
|
+
const now = this.workbench.entries;
|
|
217
|
+
return {
|
|
218
|
+
moved: now.filter((e) => was.has(e.id)).map((e) => ({ id: e.id, name: e.name, from: was.get(e.id).rank, to: e.rank })),
|
|
219
|
+
entered: now.filter((e) => !was.has(e.id)),
|
|
220
|
+
left: this.workbench.before.entries.filter((e) => !this.workbench.entry(e.id)),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
charts() {
|
|
225
|
+
const { moved } = this.change();
|
|
226
|
+
const when = (report) => report.asOf?.businessTime?.slice(0, 10);
|
|
227
|
+
const then = when(this.workbench.before) ?? 'then';
|
|
228
|
+
const now = when(this.workbench.report) ?? 'now';
|
|
229
|
+
return [{
|
|
230
|
+
title: `Rank ${then} → ${now} (1 = first)`,
|
|
231
|
+
chartType: 'slopegraph',
|
|
232
|
+
data: moved.map((m) => ({ label: m.name, start: m.from, end: m.to })),
|
|
233
|
+
}];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
notes() {
|
|
237
|
+
const { moved, entered, left } = this.change();
|
|
238
|
+
const superseded = new Map((this.workbench.report.superseded ?? []).map((s) => [s.id, s.by]));
|
|
239
|
+
return [
|
|
240
|
+
...entered.map((e) => `${e.name} entered (#${e.rank})`),
|
|
241
|
+
...left.map((e) => `${e.name} left${superseded.has(e.id) ? ` (superseded by ${superseded.get(e.id).join(', ')})` : ''}`),
|
|
242
|
+
...moved.filter((m) => m.from !== m.to).sort((a, b) => Math.abs(b.from - b.to) - Math.abs(a.from - a.to))
|
|
243
|
+
.map((m) => `${m.name} ${m.to < m.from ? 'up' : 'down'} ${Math.abs(m.from - m.to)} (#${m.from} → #${m.to})`),
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const WORKBENCH_VIEWS = Object.freeze([ExecutiveView, ArchitectureView, DeliveryView, PortfolioView, ReplayView]);
|
|
249
|
+
|
|
250
|
+
export class DecisionWorkbench {
|
|
251
|
+
static KIND = KIND;
|
|
252
|
+
|
|
253
|
+
/** A workbench from a workbench document, checked; refused with why when it is not one. */
|
|
254
|
+
static from(doc, calendar) {
|
|
255
|
+
if (!doc || typeof doc !== 'object') throw new Error('Not a decision workbench document');
|
|
256
|
+
if (doc.kind !== KIND) throw new Error(`Not a decision workbench document: kind is ${JSON.stringify(doc.kind)}, not "${KIND}"`);
|
|
257
|
+
const isReport = (r) => r && Array.isArray(r.entries) && r.entries.every((e) => e && e.id && e.flow && e.basin && Number.isFinite(e.rank));
|
|
258
|
+
if (!isReport(doc.report)) throw new Error('A decision workbench document needs a report: decisionengine\'s report.toJSON(), with ranked entries');
|
|
259
|
+
if (!doc.report.entries.length) throw new Error('The report ranks no candidates');
|
|
260
|
+
if (doc.plan !== undefined && !(doc.plan && Array.isArray(doc.plan.periods) && Array.isArray(doc.plan.value))) {
|
|
261
|
+
throw new Error('A plan, when given, is decisionengine\'s plan.toJSON(): periods and value');
|
|
262
|
+
}
|
|
263
|
+
if (doc.before !== undefined && !isReport(doc.before)) throw new Error('An earlier report, when given, is a report.toJSON() too');
|
|
264
|
+
return new DecisionWorkbench(doc, calendar);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {{report: object, plan?: object, before?: object}} doc
|
|
269
|
+
* @param {{start?: string|number, periodDays?: number}} [calendar] - when period 1 starts (default: the report's
|
|
270
|
+
* as-of business time, else today) and how long a period is (default 14 days), for the delivery plan's dates
|
|
271
|
+
*/
|
|
272
|
+
constructor({ report, plan = null, before = null }, { start, periodDays = 14 } = {}) {
|
|
273
|
+
const from = start ?? report.asOf?.businessTime ?? Date.now();
|
|
274
|
+
const ms = typeof from === 'number' ? from : Date.parse(from);
|
|
275
|
+
if (!Number.isFinite(ms)) throw new Error(`The calendar's start is ${JSON.stringify(start)}, not a date`);
|
|
276
|
+
if (!(Number.isFinite(periodDays) && periodDays > 0)) throw new Error('periodDays must be a number of days above 0');
|
|
277
|
+
this.calendar = Object.freeze({ start: ms, periodDays });
|
|
278
|
+
this.report = report;
|
|
279
|
+
this.plan = plan;
|
|
280
|
+
this.before = before;
|
|
281
|
+
this.entries = [...report.entries].sort((a, b) => a.rank - b.rank);
|
|
282
|
+
this.byId = new Map(this.entries.map((e) => [e.id, e]));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
entry(id) { return this.byId.get(id); }
|
|
286
|
+
|
|
287
|
+
/** The views this document can fill, in order. */
|
|
288
|
+
views() {
|
|
289
|
+
return WORKBENCH_VIEWS.map((View) => new View(this)).filter((view) => view.available());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
view(id) {
|
|
293
|
+
return this.views().find((v) => v.id === id) ?? null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -20,13 +20,13 @@ export class ArchimateDiagram extends Diagram {
|
|
|
20
20
|
const { elements, relationships } = this.data;
|
|
21
21
|
const options = this.options;
|
|
22
22
|
const {
|
|
23
|
-
layerColors, relationshipStyles, defaultIcons, iconChoiceIndex = 0, elementWidth = 120, elementHeight = 55,
|
|
23
|
+
layerColors, elementColors, relationshipStyles, defaultIcons, iconChoiceIndex = 0, elementWidth = 120, elementHeight = 55,
|
|
24
24
|
archimateVersion = '4', useDistinctShapes = false, iconSet = 'glyph',
|
|
25
25
|
validate = true, invalidRelationships = 'highlight', onValidationWarning, annotations = [], viewpoint = null,
|
|
26
26
|
viewpointMode = 'fade', customViewpoints = [], ...rest
|
|
27
27
|
} = options;
|
|
28
28
|
const nodeOpts = {
|
|
29
|
-
layerColors, defaultIcons, iconChoiceIndex, elementWidth, elementHeight,
|
|
29
|
+
layerColors, elementColors, defaultIcons, iconChoiceIndex, elementWidth, elementHeight,
|
|
30
30
|
archimateVersion, useDistinctShapes, iconSet,
|
|
31
31
|
};
|
|
32
32
|
const nodes = elements.map(element => ArchimateDiagram.buildNode(element, nodeOpts));
|
|
@@ -79,6 +79,12 @@ export class ArchimateDiagram extends Diagram {
|
|
|
79
79
|
annotation.shape = 'note';
|
|
80
80
|
annotation.labelPosition = annotation.labelPosition || 'inside';
|
|
81
81
|
annotation.resizable = annotation.resizable !== false;
|
|
82
|
+
if (annotation.reference || annotation.referencedModel || annotation.viewRef || annotation.isViewReference) {
|
|
83
|
+
annotation.isViewReference = true;
|
|
84
|
+
const refId = annotation.reference || annotation.referencedModel || annotation.viewRef;
|
|
85
|
+
annotation.referencedModel = refId;
|
|
86
|
+
annotation.tooltip = annotation.tooltip || `View: ${annotation.label || refId} (double-click to open)`;
|
|
87
|
+
}
|
|
82
88
|
visibleNodes.push(annotation);
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -117,7 +123,7 @@ export class ArchimateDiagram extends Diagram {
|
|
|
117
123
|
// Diagram-wide ArchiMate settings survive a rerender so live icon-choice
|
|
118
124
|
// changes can be applied later (see RenderEngine._archimateSetIconChoiceIndex).
|
|
119
125
|
this.engine._archimateOptions = {
|
|
120
|
-
layerColors, defaultIcons, iconChoiceIndex, relationshipStyles, elementWidth, elementHeight,
|
|
126
|
+
layerColors, elementColors, defaultIcons, iconChoiceIndex, relationshipStyles, elementWidth, elementHeight,
|
|
121
127
|
archimateVersion, useDistinctShapes, iconSet, viewpoint,
|
|
122
128
|
viewpointMode, customViewpoints,
|
|
123
129
|
};
|
|
@@ -195,15 +201,18 @@ export class ArchimateDiagram extends Diagram {
|
|
|
195
201
|
}
|
|
196
202
|
|
|
197
203
|
static buildNode(element, {
|
|
198
|
-
layerColors, defaultIcons, iconChoiceIndex = 0, elementWidth = 120, elementHeight = 55,
|
|
204
|
+
layerColors, elementColors, defaultIcons, iconChoiceIndex = 0, elementWidth = 120, elementHeight = 55,
|
|
199
205
|
archimateVersion = '4', useDistinctShapes = false, iconSet = 'glyph',
|
|
200
206
|
} = {}) {
|
|
201
207
|
const resolved = archimateVersion === '4' ? ArchimateNotation.migrateElementToV4(element) : element;
|
|
202
208
|
ArchimateSpecializationRegistry.applyToNode(resolved);
|
|
203
209
|
const layer = ArchimateDiagram.elementLayer(resolved, archimateVersion);
|
|
204
210
|
const type = ArchimateDiagram.resolveElementType(resolved.type, archimateVersion);
|
|
205
|
-
const
|
|
206
|
-
|
|
211
|
+
const isOrJunction = resolved.type === 'or-junction'
|
|
212
|
+
|| ((resolved.type === 'junction' || resolved.type === 'and-junction') && (resolved.junctionType === 'or' || resolved.attributes?.type === 'or' || resolved.archi?.attributes?.type === 'or'));
|
|
213
|
+
const isJunction = resolved.type === 'junction' || resolved.type === 'and-junction' || resolved.type === 'or-junction';
|
|
214
|
+
const useDistinctShape = isJunction ? true : (resolved.useDistinctShape ?? useDistinctShapes);
|
|
215
|
+
const notationShape = useDistinctShape ? (isJunction ? (isOrJunction ? 'junction-or' : 'junction') : type?.notationShape) : undefined;
|
|
207
216
|
const glyphBadges = (resolved.iconSet ?? iconSet) !== 'emoji';
|
|
208
217
|
|
|
209
218
|
// CRITICAL: Use diagramInstanceId (not elementId) as the node's unique ID
|
|
@@ -213,14 +222,21 @@ export class ArchimateDiagram extends Diagram {
|
|
|
213
222
|
// icon, and a plain white fill unless it sets one (a note keeps its own default).
|
|
214
223
|
const viewBox = resolved.viewObject && resolved.shape !== 'note';
|
|
215
224
|
|
|
225
|
+
const defaultW = isJunction ? 18 : elementWidth;
|
|
226
|
+
const defaultH = isJunction ? 18 : elementHeight;
|
|
227
|
+
const typeColor = elementColors?.[type?.name] ?? elementColors?.[resolved.type];
|
|
228
|
+
const defaultColor = typeColor || (isJunction
|
|
229
|
+
? (isOrJunction ? '#ffffff' : '#000000')
|
|
230
|
+
: (viewBox ? '#ffffff' : ArchimateDiagram.layerColor(layer, layerColors, type?.ownColor)));
|
|
231
|
+
|
|
216
232
|
return {
|
|
217
233
|
...resolved,
|
|
218
234
|
id: nodeId, // Override to use diagramInstanceId for uniqueness
|
|
219
235
|
layer,
|
|
220
236
|
shape: resolved.shape || notationShape || 'rect',
|
|
221
|
-
color: resolved.color ||
|
|
222
|
-
width: resolved.width ??
|
|
223
|
-
height: resolved.height ??
|
|
237
|
+
color: resolved.color || defaultColor,
|
|
238
|
+
width: resolved.width ?? defaultW,
|
|
239
|
+
height: resolved.height ?? defaultH,
|
|
224
240
|
strokeColor: resolved.strokeColor || '#000000',
|
|
225
241
|
strokeWidth: resolved.strokeWidth ?? 1.2,
|
|
226
242
|
// The ArchiMate notation draws a Grouping with a dashed outline.
|