@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
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
2
|
+
import { Chart } from '../Chart.js';
|
|
3
|
+
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
4
|
+
import { ColorPalette } from '../ColorPalette.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AlluvialDiagram — multi-stage categorical flow diagram across 3+ ordered
|
|
8
|
+
* dimensions (extending Sankey ribbons to multi-column categorical pathways).
|
|
9
|
+
*
|
|
10
|
+
* Data shape:
|
|
11
|
+
* { dimensions: string[], rows: Array<object>, valueField?: string }
|
|
12
|
+
* or Array<{steps: string[], value: number}>
|
|
13
|
+
*/
|
|
14
|
+
export class AlluvialDiagram extends Chart {
|
|
15
|
+
static chartType = 'alluvial';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalize heterogeneous alluvial datasets into a unified stage-and-link model.
|
|
19
|
+
* Supports:
|
|
20
|
+
* 1. Edge list: Array<{ source: string, target: string, value?: number }>
|
|
21
|
+
* 2. Pathway steps: Array<{ steps: string[], value?: number }>
|
|
22
|
+
* 3. Categorical rows: { dimensions: string[], rows: object[], valueField?: string }
|
|
23
|
+
* 4. Flat row objects: Array<object> with options.dimensions
|
|
24
|
+
*
|
|
25
|
+
* @returns {{
|
|
26
|
+
* dimensions: string[],
|
|
27
|
+
* stages: Array<Array<{ name: string, val: number, stage: number }>>,
|
|
28
|
+
* links: Array<{ stage: number, source: string, target: string, value: number }>
|
|
29
|
+
* }}
|
|
30
|
+
*/
|
|
31
|
+
static normalize(data, options = {}) {
|
|
32
|
+
if (!data) return { dimensions: [], stages: [], links: [] };
|
|
33
|
+
|
|
34
|
+
// Format 1: Edge list Array<{ source, target, value }>
|
|
35
|
+
if (Array.isArray(data) && data.length > 0 && data[0].source != null && data[0].target != null) {
|
|
36
|
+
return AlluvialDiagram._normalizeEdgeList(data, options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Format 2: Pathway steps Array<{ steps: string[], value: number }>
|
|
40
|
+
if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0].steps)) {
|
|
41
|
+
return AlluvialDiagram._normalizePathways(data, options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Format 3: Explicit { dimensions, rows, valueField }
|
|
45
|
+
if (data && data.dimensions && Array.isArray(data.rows)) {
|
|
46
|
+
const valKey = data.valueField ?? 'value';
|
|
47
|
+
const pathways = data.rows.map(r => ({
|
|
48
|
+
steps: data.dimensions.map(d => String(r[d] ?? 'Other')),
|
|
49
|
+
value: Number(r[valKey]) || 1,
|
|
50
|
+
}));
|
|
51
|
+
return AlluvialDiagram._normalizePathways(pathways, { ...options, dimensions: data.dimensions });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Format 4: Array of row objects [ { col1: 'A', col2: 'B', value: 10 } ]
|
|
55
|
+
if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') {
|
|
56
|
+
const valKey = options.valueField ?? 'value';
|
|
57
|
+
const dims = options.dimensions ?? Object.keys(data[0]).filter(k => k !== valKey && k !== 'count' && k !== 'weight');
|
|
58
|
+
if (dims.length >= 2) {
|
|
59
|
+
const pathways = data.map(r => ({
|
|
60
|
+
steps: dims.map(d => String(r[d] ?? 'Other')),
|
|
61
|
+
value: Number(r[valKey]) || 1,
|
|
62
|
+
}));
|
|
63
|
+
return AlluvialDiagram._normalizePathways(pathways, { ...options, dimensions: dims });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { dimensions: [], stages: [], links: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static _normalizePathways(pathways, options = {}) {
|
|
71
|
+
const stageCount = pathways[0].steps.length;
|
|
72
|
+
const dimensions = options.dimensions ?? options.stages ?? pathways[0].steps.map((_, i) => `Stage ${i + 1}`);
|
|
73
|
+
|
|
74
|
+
const stages = dimensions.map((dim, sIdx) => {
|
|
75
|
+
const valMap = new Map();
|
|
76
|
+
for (const p of pathways) {
|
|
77
|
+
const cat = p.steps[sIdx];
|
|
78
|
+
valMap.set(cat, (valMap.get(cat) ?? 0) + (Number(p.value) || 1));
|
|
79
|
+
}
|
|
80
|
+
return Array.from(valMap.entries()).map(([name, val]) => ({ name, val, stage: sIdx }));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const links = [];
|
|
84
|
+
for (let s = 0; s < stageCount - 1; s++) {
|
|
85
|
+
const pairMap = new Map();
|
|
86
|
+
for (const p of pathways) {
|
|
87
|
+
const src = p.steps[s];
|
|
88
|
+
const tgt = p.steps[s + 1];
|
|
89
|
+
const key = `${src}:::${tgt}`;
|
|
90
|
+
pairMap.set(key, (pairMap.get(key) ?? 0) + (Number(p.value) || 1));
|
|
91
|
+
}
|
|
92
|
+
for (const [key, val] of pairMap.entries()) {
|
|
93
|
+
const [source, target] = key.split(':::');
|
|
94
|
+
links.push({ stage: s, source, target, value: val });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { dimensions, stages, links };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static _normalizeEdgeList(edges, options = {}) {
|
|
102
|
+
const nodeSet = new Set();
|
|
103
|
+
const incoming = new Map();
|
|
104
|
+
const outgoing = new Map();
|
|
105
|
+
|
|
106
|
+
for (const e of edges) {
|
|
107
|
+
nodeSet.add(e.source);
|
|
108
|
+
nodeSet.add(e.target);
|
|
109
|
+
if (!incoming.has(e.target)) incoming.set(e.target, []);
|
|
110
|
+
if (!outgoing.has(e.source)) outgoing.set(e.source, []);
|
|
111
|
+
if (!incoming.has(e.source)) incoming.set(e.source, []);
|
|
112
|
+
if (!outgoing.has(e.target)) outgoing.set(e.target, []);
|
|
113
|
+
|
|
114
|
+
const val = Number(e.value) || 1;
|
|
115
|
+
outgoing.get(e.source).push({ target: e.target, value: val });
|
|
116
|
+
incoming.get(e.target).push({ source: e.source, value: val });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Topological stage assignment
|
|
120
|
+
const stageMap = new Map();
|
|
121
|
+
const queue = [];
|
|
122
|
+
for (const name of nodeSet) {
|
|
123
|
+
if (incoming.get(name).length === 0) {
|
|
124
|
+
stageMap.set(name, 0);
|
|
125
|
+
queue.push(name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (queue.length === 0 && nodeSet.size > 0) {
|
|
129
|
+
const first = nodeSet.values().next().value;
|
|
130
|
+
stageMap.set(first, 0);
|
|
131
|
+
queue.push(first);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
while (queue.length > 0) {
|
|
135
|
+
const curr = queue.shift();
|
|
136
|
+
const s = stageMap.get(curr);
|
|
137
|
+
for (const e of outgoing.get(curr)) {
|
|
138
|
+
const existing = stageMap.get(e.target);
|
|
139
|
+
if (existing == null || existing < s + 1) {
|
|
140
|
+
stageMap.set(e.target, s + 1);
|
|
141
|
+
queue.push(e.target);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const name of nodeSet) {
|
|
147
|
+
if (!stageMap.has(name)) stageMap.set(name, 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const maxStage = Math.max(...stageMap.values(), 1);
|
|
151
|
+
const stageCount = maxStage + 1;
|
|
152
|
+
const dimensions = options.dimensions ?? options.stages ?? Array.from({ length: stageCount }, (_, i) => `Stage ${i + 1}`);
|
|
153
|
+
|
|
154
|
+
// Segment multi-stage flows so ribbons cross each adjacent stage cleanly
|
|
155
|
+
const links = [];
|
|
156
|
+
for (const e of edges) {
|
|
157
|
+
const val = Number(e.value) || 1;
|
|
158
|
+
const s1 = stageMap.get(e.source);
|
|
159
|
+
const s2 = stageMap.get(e.target);
|
|
160
|
+
if (s1 == null || s2 == null || s1 >= s2) continue;
|
|
161
|
+
|
|
162
|
+
let prev = e.source;
|
|
163
|
+
for (let s = s1; s < s2; s++) {
|
|
164
|
+
const next = (s === s2 - 1) ? e.target : `${e.source} → ${e.target}`;
|
|
165
|
+
links.push({
|
|
166
|
+
stage: s,
|
|
167
|
+
source: prev,
|
|
168
|
+
target: next,
|
|
169
|
+
value: val,
|
|
170
|
+
isSynthetic: s !== s1 && s !== s2 - 1,
|
|
171
|
+
});
|
|
172
|
+
prev = next;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Build stages array with node flow totals
|
|
177
|
+
const stages = Array.from({ length: stageCount }, (_, sIdx) => {
|
|
178
|
+
const nodeValMap = new Map();
|
|
179
|
+
for (const l of links) {
|
|
180
|
+
if (l.stage === sIdx) {
|
|
181
|
+
nodeValMap.set(l.source, (nodeValMap.get(l.source) ?? 0) + l.value);
|
|
182
|
+
}
|
|
183
|
+
if (l.stage === sIdx - 1) {
|
|
184
|
+
nodeValMap.set(l.target, (nodeValMap.get(l.target) ?? 0) + l.value);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return Array.from(nodeValMap.entries()).map(([name, val]) => ({
|
|
188
|
+
name, val, stage: sIdx,
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { dimensions, stages, links };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_render() {
|
|
196
|
+
const { dimensions, stages, links } = AlluvialDiagram.normalize(this.data, this.options);
|
|
197
|
+
const stageCount = stages.length;
|
|
198
|
+
if (stageCount < 2 || !links.length) return;
|
|
199
|
+
|
|
200
|
+
const options = this.options;
|
|
201
|
+
const box = ChartBox.fromRenderer(this.renderer);
|
|
202
|
+
const {
|
|
203
|
+
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
204
|
+
title, colors, nodeWidth = 14,
|
|
205
|
+
} = options;
|
|
206
|
+
const plotBox = { x, y, width, height };
|
|
207
|
+
|
|
208
|
+
new ChartLabels({ title }).draw(this.renderer, plotBox);
|
|
209
|
+
|
|
210
|
+
const palette = new ColorPalette(colors);
|
|
211
|
+
const topPad = 32;
|
|
212
|
+
const leftMargin = Math.max(75, Math.min(130, width * 0.18));
|
|
213
|
+
const rightMargin = Math.max(75, Math.min(130, width * 0.18));
|
|
214
|
+
const flowX = x + leftMargin;
|
|
215
|
+
const flowW = Math.max(100, width - leftMargin - rightMargin);
|
|
216
|
+
const usableH = height - topPad - 16;
|
|
217
|
+
const usableY = y + topPad;
|
|
218
|
+
|
|
219
|
+
const stageStepX = (flowW - nodeWidth) / (stageCount - 1);
|
|
220
|
+
|
|
221
|
+
// Compute nodes geometry per stage
|
|
222
|
+
const stageNodes = stages.map((nodeList, sIdx) => {
|
|
223
|
+
nodeList.sort((a, b) => b.val - a.val);
|
|
224
|
+
const totalStageVal = nodeList.reduce((sum, n) => sum + n.val, 0) || 1;
|
|
225
|
+
|
|
226
|
+
const gap = 8;
|
|
227
|
+
const gapTotal = Math.max(0, nodeList.length - 1) * gap;
|
|
228
|
+
const availableH = Math.max(10, usableH - gapTotal);
|
|
229
|
+
let curY = usableY;
|
|
230
|
+
|
|
231
|
+
return nodeList.map((cat, cIdx) => {
|
|
232
|
+
const h = Math.max(6, (cat.val / totalStageVal) * availableH);
|
|
233
|
+
const node = {
|
|
234
|
+
stage: sIdx,
|
|
235
|
+
category: cat.name,
|
|
236
|
+
val: cat.val,
|
|
237
|
+
x: flowX + sIdx * stageStepX,
|
|
238
|
+
y: curY,
|
|
239
|
+
w: nodeWidth,
|
|
240
|
+
h,
|
|
241
|
+
color: palette.colorAt(cIdx),
|
|
242
|
+
};
|
|
243
|
+
curY += h + gap;
|
|
244
|
+
return node;
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Draw Stage Headers
|
|
249
|
+
dimensions.forEach((dim, sIdx) => {
|
|
250
|
+
const hx = flowX + sIdx * stageStepX + nodeWidth / 2;
|
|
251
|
+
this.renderer.text(dim, hx, y + 16, {
|
|
252
|
+
textAnchor: this.renderer.centerTextAnchor, fontSize: '11px', fontWeight: 'bold', fill: '#444',
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Ribbon flows between adjacent stages
|
|
257
|
+
for (let sIdx = 0; sIdx < stageCount - 1; sIdx++) {
|
|
258
|
+
const leftNodes = new Map(stageNodes[sIdx].map(n => [n.category, n]));
|
|
259
|
+
const rightNodes = new Map(stageNodes[sIdx + 1].map(n => [n.category, n]));
|
|
260
|
+
const leftOffset = new Map();
|
|
261
|
+
const rightOffset = new Map();
|
|
262
|
+
|
|
263
|
+
const stageLinks = links.filter(l => l.stage === sIdx);
|
|
264
|
+
for (const l of stageLinks) {
|
|
265
|
+
const n1 = leftNodes.get(l.source);
|
|
266
|
+
const n2 = rightNodes.get(l.target);
|
|
267
|
+
if (!n1 || !n2) continue;
|
|
268
|
+
|
|
269
|
+
const rH1 = (l.value / n1.val) * n1.h;
|
|
270
|
+
const rH2 = (l.value / n2.val) * n2.h;
|
|
271
|
+
const y1 = n1.y + (leftOffset.get(l.source) ?? 0);
|
|
272
|
+
const y2 = n2.y + (rightOffset.get(l.target) ?? 0);
|
|
273
|
+
leftOffset.set(l.source, (leftOffset.get(l.source) ?? 0) + rH1);
|
|
274
|
+
rightOffset.set(l.target, (rightOffset.get(l.target) ?? 0) + rH2);
|
|
275
|
+
|
|
276
|
+
const x1 = n1.x + n1.w;
|
|
277
|
+
const x2 = n2.x;
|
|
278
|
+
const cpx = (x1 + x2) / 2;
|
|
279
|
+
const ribbonD = `M${x1},${y1} `
|
|
280
|
+
+ `C${cpx},${y1} ${cpx},${y2} ${x2},${y2} `
|
|
281
|
+
+ `L${x2},${y2 + rH2} `
|
|
282
|
+
+ `C${cpx},${y2 + rH2} ${cpx},${y1 + rH1} ${x1},${y1 + rH1} Z`;
|
|
283
|
+
|
|
284
|
+
const { r, g, b: bl } = palette.hexToRgb(n1.color);
|
|
285
|
+
this._datum(this.renderer.path(ribbonD, `rgba(${r},${g},${bl},0.38)`, null, {
|
|
286
|
+
tooltip: `${l.source} → ${l.target}: ${Axis.formatValue(l.value)}`,
|
|
287
|
+
}), { label: `${l.source} → ${l.target}`, value: l.value });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Draw Stage Nodes over ribbons
|
|
292
|
+
stageNodes.forEach(nodes => {
|
|
293
|
+
nodes.forEach(n => {
|
|
294
|
+
this._datum(this.renderer.rect(n.x, n.y, n.w, n.h, n.color, { color: '#ffffff', width: 1.5 }, {
|
|
295
|
+
tooltip: `${n.category}: ${Axis.formatValue(n.val)}`, rx: 2, ry: 2,
|
|
296
|
+
}), { label: n.category, value: n.val });
|
|
297
|
+
|
|
298
|
+
// Category name label
|
|
299
|
+
const isFirst = n.stage === 0;
|
|
300
|
+
const isLast = n.stage === stageCount - 1;
|
|
301
|
+
const lx = isLast ? n.x + n.w + 8 : (isFirst ? n.x - 8 : n.x + n.w / 2);
|
|
302
|
+
const lAnchor = isLast ? 'start' : (isFirst ? 'end' : this.renderer.centerTextAnchor);
|
|
303
|
+
const ly = isFirst || isLast ? n.y + n.h / 2 : n.y - 5;
|
|
304
|
+
|
|
305
|
+
this.renderer.text(n.category, lx, ly, {
|
|
306
|
+
textAnchor: lAnchor, verticalAlign: 'middle', fontSize: '10px', fill: '#333',
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Chart } from '../Chart.js';
|
|
|
3
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
4
4
|
import { Legend } from '../Legend.js';
|
|
5
5
|
import { ColorPalette } from '../ColorPalette.js';
|
|
6
|
+
import { Annotations } from '../Annotations.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* AreaChart — stacked or overlaid area chart.
|
|
@@ -16,8 +17,11 @@ export class AreaChart extends Chart {
|
|
|
16
17
|
const box = ChartBox.fromRenderer(this.renderer);
|
|
17
18
|
const {
|
|
18
19
|
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
19
|
-
colors, title, xAxisLabel, yAxisLabel, stacked = false,
|
|
20
|
+
colors, title, xAxisLabel, yAxisLabel, stacked = false,
|
|
21
|
+
normalize = false, stackMode, legend: showLegend = false,
|
|
22
|
+
scale = 'linear',
|
|
20
23
|
} = options;
|
|
24
|
+
const isPercent = Boolean(stacked && (normalize || stackMode === 'percent'));
|
|
21
25
|
const plotBox = { x, y, width, height };
|
|
22
26
|
const isMultiline = Array.isArray(data[0]?.values);
|
|
23
27
|
const series = isMultiline ? data : [{ name: 'default', values: data }];
|
|
@@ -26,33 +30,87 @@ export class AreaChart extends Chart {
|
|
|
26
30
|
const palette = new ColorPalette(colors);
|
|
27
31
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
28
32
|
|
|
33
|
+
const legendOptions = {
|
|
34
|
+
items: series.map((s, idx) => ({
|
|
35
|
+
label: s.name,
|
|
36
|
+
color: palette.colorAt(idx),
|
|
37
|
+
hidden: this._dataFilter.has(idx),
|
|
38
|
+
dataIndex: idx,
|
|
39
|
+
})),
|
|
40
|
+
itemHeight: 20,
|
|
41
|
+
markerSize: 12,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const pointTotals = isPercent
|
|
45
|
+
? Array.from({ length: pointCount }, (_, i) => series.reduce((sum, s, sIdx) => sum + (this._dataFilter.has(sIdx) ? 0 : Math.max(0, s.values[i]?.value ?? 0)), 0))
|
|
46
|
+
: null;
|
|
47
|
+
|
|
29
48
|
// Precompute each series' [base, top] band per point — base is 0
|
|
30
49
|
// unless stacked, in which case it's the running cumulative of prior
|
|
31
50
|
// series (computed once here, not re-derived during render).
|
|
32
51
|
let running = new Array(pointCount).fill(0);
|
|
33
|
-
const bands = series.map(s => {
|
|
52
|
+
const bands = series.map((s, sIdx) => {
|
|
53
|
+
const isHidden = this._dataFilter.has(sIdx);
|
|
34
54
|
const band = s.values.map((d, i) => {
|
|
35
55
|
const base = stacked ? running[i] : 0;
|
|
36
|
-
|
|
56
|
+
const rawVal = isHidden ? 0 : d.value;
|
|
57
|
+
const val = isPercent
|
|
58
|
+
? (pointTotals[i] > 0 ? (Math.max(0, rawVal) / pointTotals[i]) * 100 : 0)
|
|
59
|
+
: rawVal;
|
|
60
|
+
return { base, top: base + val, pct: val };
|
|
37
61
|
});
|
|
38
|
-
if (stacked) running = band.map(b => b.top);
|
|
62
|
+
if (stacked && !isHidden) running = band.map(b => b.top);
|
|
39
63
|
return band;
|
|
40
64
|
});
|
|
41
|
-
const maxValue = Math.max(...bands.flat().map(b => b.top)) || 1;
|
|
65
|
+
const maxValue = isPercent ? 100 : (Math.max(...bands.flat().map(b => b.top)) || 1);
|
|
42
66
|
|
|
43
67
|
new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, plotBox);
|
|
68
|
+
const axisRes = new Axis().drawYRange(this.renderer, plotBox, 0, maxValue, { scale });
|
|
69
|
+
const toY = axisRes?.yOf ?? ((v) => y + height - (v / maxValue) * height);
|
|
70
|
+
const toX = (idx) => x + idx * pointMargin;
|
|
71
|
+
|
|
72
|
+
// Threshold bands and reference lines
|
|
73
|
+
Annotations.drawThresholdBands(this.renderer, plotBox, options.thresholdBands, { toY });
|
|
74
|
+
const refLines = options.referenceLines ?? options.referenceLine;
|
|
75
|
+
Annotations.drawReferenceLines(this.renderer, plotBox, refLines, { toY, toX });
|
|
76
|
+
|
|
44
77
|
this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
|
|
45
78
|
|
|
46
79
|
series.forEach((s, seriesIndex) => {
|
|
80
|
+
if (this._dataFilter.has(seriesIndex)) return;
|
|
47
81
|
const band = bands[seriesIndex];
|
|
48
82
|
const color = palette.colorAt(seriesIndex);
|
|
49
|
-
const topPts = band.map((b, i) => ({ px: x + i * pointMargin, py:
|
|
50
|
-
const basePts = band.map((b, i) => ({ px: x + i * pointMargin, py:
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
const topPts = band.map((b, i) => ({ px: x + i * pointMargin, py: toY(b.top) }));
|
|
84
|
+
const basePts = band.map((b, i) => ({ px: x + i * pointMargin, py: toY(b.base) }));
|
|
85
|
+
let areaPath = '';
|
|
86
|
+
let strokePath = '';
|
|
87
|
+
if (options.step) {
|
|
88
|
+
let topD = `M${topPts[0].px},${topPts[0].py}`;
|
|
89
|
+
for (let i = 1; i < topPts.length; i++) {
|
|
90
|
+
topD += ` H${topPts[i].px} V${topPts[i].py}`;
|
|
91
|
+
}
|
|
92
|
+
strokePath = topD;
|
|
93
|
+
let baseD = '';
|
|
94
|
+
for (let i = basePts.length - 1; i >= 0; i--) {
|
|
95
|
+
baseD += ` L${basePts[i].px},${basePts[i].py}`;
|
|
96
|
+
}
|
|
97
|
+
areaPath = topD + baseD + ' Z';
|
|
98
|
+
} else {
|
|
99
|
+
areaPath = 'M' + topPts.map(p => `${p.px},${p.py}`).join(' L')
|
|
100
|
+
+ ' L' + [...basePts].reverse().map(p => `${p.px},${p.py}`).join(' L') + ' Z';
|
|
101
|
+
strokePath = 'M' + topPts.map(p => `${p.px},${p.py}`).join(' L');
|
|
102
|
+
}
|
|
53
103
|
const { r, g, b: bl } = palette.hexToRgb(color);
|
|
54
104
|
this.renderer.path(areaPath, `rgba(${r},${g},${bl},0.35)`, null, { tooltip: s.name });
|
|
55
|
-
this.renderer.path(
|
|
105
|
+
this.renderer.path(strokePath, 'none', { color, width: 2 });
|
|
106
|
+
if (typeof this.renderer.circle === 'function') {
|
|
107
|
+
topPts.forEach((p, i) => {
|
|
108
|
+
const d = s.values[i];
|
|
109
|
+
this._datum(this.renderer.circle(p.px, p.py, 3, color, { color: '#fff', width: 1 }, {
|
|
110
|
+
tooltip: `${isMultiline ? s.name + ': ' : ''}${d.label}: ${d.value}`,
|
|
111
|
+
}), { label: d.label, series: isMultiline ? s.name : null });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
56
114
|
|
|
57
115
|
if (seriesIndex === 0) {
|
|
58
116
|
const step = Axis.categoryLabelStep(s.values.map(d => d.label), width, { fontSize: 10 });
|
|
@@ -64,11 +122,7 @@ export class AreaChart extends Chart {
|
|
|
64
122
|
});
|
|
65
123
|
|
|
66
124
|
if (isMultiline && showLegend) {
|
|
67
|
-
|
|
68
|
-
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
69
|
-
items: legendItems,
|
|
70
|
-
itemHeight: 20, markerSize: 12,
|
|
71
|
-
}).draw(this.renderer);
|
|
125
|
+
Legend.beside({ x, y, width }, this.renderer.width, legendOptions).draw(this.renderer);
|
|
72
126
|
}
|
|
73
127
|
}
|
|
74
128
|
}
|
|
@@ -2,6 +2,7 @@ import { Axis } from '../Axis.js';
|
|
|
2
2
|
import { Legend } from '../Legend.js';
|
|
3
3
|
import { DataFilter } from '../DataFilter.js';
|
|
4
4
|
import { BarChartBase } from './BarChartBase.js';
|
|
5
|
+
import { Annotations } from '../Annotations.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* BarChart — vertical bar chart with optional legend and hidden items.
|
|
@@ -17,22 +18,56 @@ export class BarChart extends BarChartBase {
|
|
|
17
18
|
_computeMaxValue() {
|
|
18
19
|
const data = this.data;
|
|
19
20
|
const rawMax = Math.max(...data.map(d => d.value)) || 1;
|
|
20
|
-
|
|
21
|
+
const rawMin = Math.min(0, ...data.map(d => d.value));
|
|
22
|
+
return this._isHorizontal()
|
|
23
|
+
? this._drawXAxis(rawMax, rawMin)
|
|
24
|
+
: this._drawYAxis(rawMax, rawMin);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
_beforeBars(maxValue) {
|
|
24
|
-
|
|
28
|
+
const { x, y, width, height } = this._plotBox;
|
|
29
|
+
const options = this.options;
|
|
30
|
+
const toY = (v) => y + height - (v / maxValue) * height;
|
|
31
|
+
const toX = (v) => x + (v / maxValue) * width;
|
|
32
|
+
Annotations.drawThresholdBands(this.renderer, this._plotBox, options.thresholdBands, { toY });
|
|
33
|
+
const refLines = options.referenceLines ?? options.referenceLine;
|
|
34
|
+
Annotations.drawReferenceLines(this.renderer, this._plotBox, refLines, this._isHorizontal() ? { toX } : { toY });
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
_renderBars(maxValue) {
|
|
28
38
|
const data = this.data;
|
|
29
39
|
const options = this.options;
|
|
30
40
|
const { x, y, width, height } = this._plotBox;
|
|
31
|
-
const labelStep = Axis.categoryLabelStep(data.map(d => d.label), width);
|
|
32
41
|
const { barColor = 'steelblue', colors } = options;
|
|
33
42
|
const { bandWidth, margin } = this._bandScale(data.length);
|
|
34
43
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
35
44
|
|
|
45
|
+
if (this._isHorizontal()) {
|
|
46
|
+
data.forEach((d, i) => {
|
|
47
|
+
if (this._dataFilter.has(i)) return;
|
|
48
|
+
const color = colors ? this._palette.colorAt(i) : barColor;
|
|
49
|
+
const bar = this._barSpan(d.value);
|
|
50
|
+
const barY = y + i * (bandWidth + margin);
|
|
51
|
+
|
|
52
|
+
this._datum(this.renderer.rect(bar.top, barY, bar.height, bandWidth, color, null, {
|
|
53
|
+
bar: true, dataIndex: i, tooltip: `${d.label}: ${d.value}`,
|
|
54
|
+
}), { label: d.label });
|
|
55
|
+
|
|
56
|
+
// Category label on left
|
|
57
|
+
this.renderer.text(d.label, x - 8, barY + bandWidth / 2, {
|
|
58
|
+
textAnchor: 'end', verticalAlign: 'middle', fontSize: '11px', fill: '#333',
|
|
59
|
+
});
|
|
60
|
+
// Value text past the end of the bar
|
|
61
|
+
const valX = d.value < 0 ? bar.end - 6 : bar.end + 6;
|
|
62
|
+
const valAnchor = d.value < 0 ? 'end' : 'start';
|
|
63
|
+
this.renderer.text(String(d.value), valX, barY + bandWidth / 2, {
|
|
64
|
+
textAnchor: valAnchor, verticalAlign: 'middle', fontSize: '10px', fill: '#333',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const labelStep = Axis.categoryLabelStep(data.map(d => d.label), width);
|
|
36
71
|
data.forEach((d, i) => {
|
|
37
72
|
if (this._dataFilter.has(i)) {
|
|
38
73
|
return; // Skip rendering the bar if it's hidden
|
|
@@ -34,20 +34,22 @@ export class BarChart3D extends Chart {
|
|
|
34
34
|
const barBaseY = y + height;
|
|
35
35
|
const color = palette.colorAt(i);
|
|
36
36
|
|
|
37
|
-
this.renderer.rect(
|
|
37
|
+
const frontEl = this._datum(this.renderer.rect(
|
|
38
38
|
barX, barBaseY - barHeight, barWidth, barHeight,
|
|
39
39
|
color,
|
|
40
40
|
{ color: '#333', width: 1 },
|
|
41
41
|
{ tooltip: `${d.label}: ${d.value}` },
|
|
42
|
-
);
|
|
42
|
+
), { label: d.label });
|
|
43
43
|
|
|
44
44
|
// Top face
|
|
45
45
|
const topPath = `M${barX},${barBaseY - barHeight} L${barX + barWidth},${barBaseY - barHeight} L${barX + barWidth + depth},${barBaseY - barHeight - depth} L${barX + depth},${barBaseY - barHeight - depth} Z`;
|
|
46
|
-
this.renderer.path(topPath, palette.lighten(color, 0.2), { color: '#333', width: 1 });
|
|
46
|
+
const topEl = this.renderer.path(topPath, palette.lighten(color, 0.2), { color: '#333', width: 1 }, { tooltip: `${d.label}: ${d.value}` });
|
|
47
47
|
|
|
48
48
|
// Right face
|
|
49
49
|
const rightPath = `M${barX + barWidth},${barBaseY - barHeight} L${barX + barWidth},${barBaseY} L${barX + barWidth + depth},${barBaseY + depth} L${barX + barWidth + depth},${barBaseY - barHeight - depth} Z`;
|
|
50
|
-
this.renderer.path(rightPath, palette.lighten(color, -0.15), { color: '#333', width: 1 });
|
|
50
|
+
const rightEl = this.renderer.path(rightPath, palette.lighten(color, -0.15), { color: '#333', width: 1 }, { tooltip: `${d.label}: ${d.value}` });
|
|
51
|
+
|
|
52
|
+
this._wireHoverTransform([frontEl, topEl, rightEl], { dx: 0, dy: -6 });
|
|
51
53
|
|
|
52
54
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
53
55
|
this.renderer.text(d.label, barX + barWidth / 2, barBaseY + 20, { textAnchor });
|
|
@@ -40,7 +40,9 @@ export class BarChart3DOrbit extends Chart3D {
|
|
|
40
40
|
}).sort((a, b) => b.depth - a.depth);
|
|
41
41
|
|
|
42
42
|
for (const bar of bars) {
|
|
43
|
-
this.drawBox(this.renderer, bar.x0, bar.x1, 0, bar.y1, bar.z0, bar.z1, bar.color, toScreen
|
|
43
|
+
this.drawBox(this.renderer, bar.x0, bar.x1, 0, bar.y1, bar.z0, bar.z1, bar.color, toScreen, {
|
|
44
|
+
label: bar.d.x, series: bar.d.z, x: bar.d.x, z: bar.d.z,
|
|
45
|
+
});
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
new Legend({
|
|
@@ -14,8 +14,10 @@ export class BarChartBase extends Chart {
|
|
|
14
14
|
_setup() {
|
|
15
15
|
const box = ChartBox.fromRenderer(this.renderer);
|
|
16
16
|
const options = this.options;
|
|
17
|
+
const isH = this._isHorizontal();
|
|
18
|
+
const leftExtra = isH && options.x == null ? 40 : 0;
|
|
17
19
|
const {
|
|
18
|
-
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
20
|
+
x = box.x + leftExtra, y = box.y, width = box.width - leftExtra, height = box.height,
|
|
19
21
|
colors, title, xAxisLabel, yAxisLabel,
|
|
20
22
|
} = options;
|
|
21
23
|
this._plotBox = { x, y, width, height };
|
|
@@ -23,10 +25,14 @@ export class BarChartBase extends Chart {
|
|
|
23
25
|
new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, this._plotBox);
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
_isHorizontal() {
|
|
29
|
+
return this.options.orientation === 'horizontal';
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
/** @returns {{bandWidth: number, step: number, margin: number}} */
|
|
27
33
|
_bandScale(count) {
|
|
28
|
-
const
|
|
29
|
-
const step =
|
|
34
|
+
const span = this._isHorizontal() ? this._plotBox.height : this._plotBox.width;
|
|
35
|
+
const step = span / count;
|
|
30
36
|
const bandWidth = step * 0.8;
|
|
31
37
|
const margin = step - bandWidth;
|
|
32
38
|
return { bandWidth, step, margin };
|
|
@@ -47,15 +53,29 @@ export class BarChartBase extends Chart {
|
|
|
47
53
|
return this._domain.max;
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/** Draw an X axis for horizontal bar orientation. */
|
|
57
|
+
_drawXAxis(rawMax, rawMin = 0) {
|
|
58
|
+
this._domain = new Axis().drawXRange(this.renderer, this._plotBox, rawMin, rawMax);
|
|
59
|
+
return this._domain.max;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
/**
|
|
51
|
-
* Where a bar of `value` goes on the axis
|
|
52
|
-
* down to it when negative.
|
|
63
|
+
* Where a bar of `value` goes on the axis drawn: from the zero line up/right to it, or
|
|
64
|
+
* down/left to it when negative.
|
|
53
65
|
* @returns {{top: number, height: number, end: number}} `end` is the bar's far end from zero
|
|
54
66
|
*/
|
|
55
67
|
_barSpan(value) {
|
|
68
|
+
if (this._isHorizontal()) {
|
|
69
|
+
const { x, width } = this._plotBox;
|
|
70
|
+
const { min, max } = this._domain;
|
|
71
|
+
const xOf = (v) => x + ((v - min) / ((max - min) || 1)) * width;
|
|
72
|
+
const zero = xOf(0);
|
|
73
|
+
const end = xOf(value);
|
|
74
|
+
return { top: Math.min(zero, end), height: Math.abs(end - zero), end };
|
|
75
|
+
}
|
|
56
76
|
const { y, height } = this._plotBox;
|
|
57
77
|
const { min, max } = this._domain;
|
|
58
|
-
const yOf = (v) => y + height - ((v - min) / (max - min)) * height;
|
|
78
|
+
const yOf = (v) => y + height - ((v - min) / ((max - min) || 1)) * height;
|
|
59
79
|
const zero = yOf(0);
|
|
60
80
|
const end = yOf(value);
|
|
61
81
|
return { top: Math.min(zero, end), height: Math.abs(end - zero), end };
|