@peter.prib/renderengine 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +480 -302
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49217 -37484
  4. package/dist/renderengine.umd.js +24899 -13093
  5. package/documentation/REFERENCE.md +68 -1
  6. package/package.json +13 -3
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +269 -3
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +7 -2
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +142 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/ask/AskBar.js +388 -0
  34. package/src/charting/ask/AskView.js +400 -0
  35. package/src/charting/charts/AlluvialDiagram.js +311 -0
  36. package/src/charting/charts/AreaChart.js +69 -15
  37. package/src/charting/charts/BarChart.js +38 -3
  38. package/src/charting/charts/BarChart3D.js +6 -4
  39. package/src/charting/charts/BarChart3DOrbit.js +3 -1
  40. package/src/charting/charts/BarChartBase.js +26 -6
  41. package/src/charting/charts/BeeswarmChart.js +164 -0
  42. package/src/charting/charts/BoxPlotChart.js +25 -3
  43. package/src/charting/charts/BubbleChart.js +4 -1
  44. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  45. package/src/charting/charts/CandlestickChart.js +172 -4
  46. package/src/charting/charts/ChordDiagram.js +107 -4
  47. package/src/charting/charts/CirclePackChart.js +81 -0
  48. package/src/charting/charts/ComboChart.js +233 -0
  49. package/src/charting/charts/ContourChart.js +310 -0
  50. package/src/charting/charts/DensityChart.js +1 -1
  51. package/src/charting/charts/DonutChart3D.js +13 -0
  52. package/src/charting/charts/DumbbellChart.js +217 -0
  53. package/src/charting/charts/EcdfChart.js +150 -0
  54. package/src/charting/charts/FunnelChart.js +5 -2
  55. package/src/charting/charts/GeoMapChart.js +132 -11
  56. package/src/charting/charts/GroupedBarChart.js +44 -3
  57. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  58. package/src/charting/charts/HeatmapChart.js +6 -3
  59. package/src/charting/charts/HexbinChart.js +137 -0
  60. package/src/charting/charts/HierarchyChart.js +77 -3
  61. package/src/charting/charts/HistogramChart.js +2 -2
  62. package/src/charting/charts/HorizonChart.js +105 -0
  63. package/src/charting/charts/IcicleChart.js +47 -0
  64. package/src/charting/charts/KagiChart.js +167 -0
  65. package/src/charting/charts/LineChart.js +74 -18
  66. package/src/charting/charts/LollipopChart.js +104 -0
  67. package/src/charting/charts/MarimekkoChart.js +75 -0
  68. package/src/charting/charts/OhlcChart.js +79 -0
  69. package/src/charting/charts/OutlineChart.js +10 -8
  70. package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
  71. package/src/charting/charts/ParetoChart.js +92 -0
  72. package/src/charting/charts/PieChart.js +195 -17
  73. package/src/charting/charts/PieChart3D.js +137 -0
  74. package/src/charting/charts/PipeChart.js +5 -2
  75. package/src/charting/charts/PointAndFigureChart.js +198 -0
  76. package/src/charting/charts/PolarAreaChart.js +189 -0
  77. package/src/charting/charts/PopulationPyramidChart.js +4 -4
  78. package/src/charting/charts/RadarChart.js +12 -5
  79. package/src/charting/charts/RadialBarChart.js +6 -3
  80. package/src/charting/charts/RenkoChart.js +178 -0
  81. package/src/charting/charts/RidgelineChart.js +113 -0
  82. package/src/charting/charts/SankeyDiagram.js +129 -6
  83. package/src/charting/charts/ScatterChart.js +19 -3
  84. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  85. package/src/charting/charts/SlopegraphChart.js +205 -0
  86. package/src/charting/charts/SparklineTableChart.js +166 -0
  87. package/src/charting/charts/StackedBarChart.js +125 -11
  88. package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
  89. package/src/charting/charts/StreamGraphChart.js +18 -3
  90. package/src/charting/charts/SunburstChart.js +28 -8
  91. package/src/charting/charts/SurfaceChart3D.js +157 -0
  92. package/src/charting/charts/TreemapChart.js +31 -11
  93. package/src/charting/charts/ViolinChart.js +20 -2
  94. package/src/charting/charts/WaterfallChart.js +2 -2
  95. package/src/charting/charts/WordCloudChart.js +2 -2
  96. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  97. package/src/charting/datamodel/DataLineage.js +150 -0
  98. package/src/charting/datamodel/DataModel.js +83 -1
  99. package/src/charting/datamodel/ModelMapping.js +87 -0
  100. package/src/charting/datamodel/SqlDdl.js +110 -85
  101. package/src/charting/datamodel/index.js +3 -0
  102. package/src/charting/decision/DecisionWorkbench.js +295 -0
  103. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  104. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  105. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  106. package/src/charting/diagrams/SketchView.js +121 -0
  107. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  108. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  109. package/src/charting/modelTree/NavigatorModel.js +410 -0
  110. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  111. package/src/charting/pivot/PivotChart.js +310 -13
  112. package/src/charting/pivot/PivotGridChart.js +322 -34
  113. package/src/charting/pivot/PivotView.js +626 -21
  114. package/src/charting/report/TemplatedReport.js +547 -0
  115. package/src/charting/templates/BoxTemplate.js +28 -27
  116. package/src/charting/templates/PackageTemplate.js +26 -25
  117. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  118. package/src/charting/templates/Template.js +219 -212
  119. package/src/cli.js +146 -0
  120. package/src/index.js +3370 -2899
  121. package/src/workspace/Workspace.js +126 -7
  122. package/src/workspace/archimateServices.js +4 -0
  123. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  124. package/src/workspace/styles.js +36 -1
@@ -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, legend: showLegend = 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
- return { base, top: base + d.value };
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: y + height - (b.top / maxValue) * height }));
50
- const basePts = band.map((b, i) => ({ px: x + i * pointMargin, py: y + height - (b.base / maxValue) * height }));
51
- const areaPath = 'M' + topPts.map(p => `${p.px},${p.py}`).join(' L')
52
- + ' L' + [...basePts].reverse().map(p => `${p.px},${p.py}`).join(' L') + ' Z';
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('M' + topPts.map(p => `${p.px},${p.py}`).join(' L'), 'none', { color, width: 2 });
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
- const legendItems = series.map((s, i) => ({ name: s.name, color: palette.colorAt(i) }));
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
- return this._drawYAxis(rawMax, Math.min(0, ...data.map(d => d.value)));
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
- // Y axis already drawn by _computeMaxValue(); nothing else needed.
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 { width } = this._plotBox;
29
- const step = width / count;
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 `_drawYAxis` drew: from the zero line up to it, or
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 };