@peter.prib/renderengine 0.14.0 → 0.15.1

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 (113) hide show
  1. package/README.md +45 -19
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49641 -39747
  4. package/dist/renderengine.umd.js +25366 -15650
  5. package/documentation/REFERENCE.md +206 -0
  6. package/examples/decision-workbench.html +26 -0
  7. package/package.json +7 -1
  8. package/src/CanvasRenderer.js +225 -200
  9. package/src/SVGRenderer.js +476 -454
  10. package/src/archimate.js +22 -2
  11. package/src/archimateArchiFormat.js +1735 -1701
  12. package/src/archimateArchive.js +36 -7
  13. package/src/archimateCSV.js +93 -37
  14. package/src/archimateDrawio.js +2 -0
  15. package/src/archimateExchangeFormat.js +3 -1
  16. package/src/chartCatalog.js +219 -0
  17. package/src/charting/Annotations.js +169 -0
  18. package/src/charting/ArchimateDiagramEditor.js +54 -10
  19. package/src/charting/Axis.js +217 -5
  20. package/src/charting/Chart.js +99 -9
  21. package/src/charting/Chart3D.js +3 -0
  22. package/src/charting/ChartBox.js +8 -1
  23. package/src/charting/ColorPalette.js +42 -4
  24. package/src/charting/DiagramEditor.js +174 -39
  25. package/src/charting/DiagramEditorCommands.js +28 -0
  26. package/src/charting/Edge.js +7 -0
  27. package/src/charting/Hierarchy.js +114 -0
  28. package/src/charting/InteractionManager.js +4 -0
  29. package/src/charting/ModelFileFormat.js +5 -1
  30. package/src/charting/SettingsManager.js +35 -0
  31. package/src/charting/ShapePaths.js +82 -10
  32. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  33. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  34. package/src/charting/charts/AlluvialDiagram.js +311 -0
  35. package/src/charting/charts/AreaChart.js +52 -15
  36. package/src/charting/charts/BarChart.js +38 -3
  37. package/src/charting/charts/BarChart3D.js +5 -3
  38. package/src/charting/charts/BarChartBase.js +26 -6
  39. package/src/charting/charts/BeeswarmChart.js +164 -0
  40. package/src/charting/charts/BoxPlotChart.js +23 -1
  41. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  42. package/src/charting/charts/CandlestickChart.js +172 -4
  43. package/src/charting/charts/ChordDiagram.js +105 -2
  44. package/src/charting/charts/CirclePackChart.js +81 -0
  45. package/src/charting/charts/ComboChart.js +233 -0
  46. package/src/charting/charts/ContourChart.js +310 -0
  47. package/src/charting/charts/DonutChart3D.js +13 -0
  48. package/src/charting/charts/DumbbellChart.js +217 -0
  49. package/src/charting/charts/EcdfChart.js +150 -0
  50. package/src/charting/charts/FunnelChart.js +4 -1
  51. package/src/charting/charts/GeoMapChart.js +132 -11
  52. package/src/charting/charts/GroupedBarChart.js +44 -3
  53. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  54. package/src/charting/charts/HeatmapChart.js +2 -2
  55. package/src/charting/charts/HexbinChart.js +137 -0
  56. package/src/charting/charts/HierarchyChart.js +77 -3
  57. package/src/charting/charts/HorizonChart.js +105 -0
  58. package/src/charting/charts/KagiChart.js +167 -0
  59. package/src/charting/charts/LineChart.js +74 -18
  60. package/src/charting/charts/OhlcChart.js +79 -0
  61. package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
  62. package/src/charting/charts/PieChart.js +195 -17
  63. package/src/charting/charts/PieChart3D.js +137 -0
  64. package/src/charting/charts/PointAndFigureChart.js +198 -0
  65. package/src/charting/charts/PolarAreaChart.js +189 -0
  66. package/src/charting/charts/RadarChart.js +7 -1
  67. package/src/charting/charts/RadialBarChart.js +5 -2
  68. package/src/charting/charts/RenkoChart.js +178 -0
  69. package/src/charting/charts/RidgelineChart.js +113 -0
  70. package/src/charting/charts/SankeyDiagram.js +127 -4
  71. package/src/charting/charts/ScatterChart.js +13 -0
  72. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  73. package/src/charting/charts/SlopegraphChart.js +205 -0
  74. package/src/charting/charts/SparklineTableChart.js +166 -0
  75. package/src/charting/charts/StackedBarChart.js +48 -5
  76. package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
  77. package/src/charting/charts/StreamGraphChart.js +10 -3
  78. package/src/charting/charts/SunburstChart.js +27 -7
  79. package/src/charting/charts/SurfaceChart3D.js +157 -0
  80. package/src/charting/charts/TreemapChart.js +28 -8
  81. package/src/charting/charts/ViolinChart.js +18 -0
  82. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  83. package/src/charting/datamodel/DataLineage.js +150 -0
  84. package/src/charting/datamodel/DataModel.js +83 -1
  85. package/src/charting/datamodel/ModelMapping.js +87 -0
  86. package/src/charting/datamodel/SqlDdl.js +110 -85
  87. package/src/charting/datamodel/index.js +3 -0
  88. package/src/charting/decision/DecisionWorkbench.js +295 -0
  89. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  90. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  91. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  92. package/src/charting/diagrams/SketchView.js +121 -0
  93. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  94. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  95. package/src/charting/modelTree/NavigatorModel.js +410 -0
  96. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  97. package/src/charting/pivot/PivotChart.js +109 -4
  98. package/src/charting/pivot/PivotGridChart.js +321 -33
  99. package/src/charting/pivot/PivotView.js +82 -0
  100. package/src/charting/report/TemplatedReport.js +547 -0
  101. package/src/charting/templates/BoxTemplate.js +28 -27
  102. package/src/charting/templates/PackageTemplate.js +26 -25
  103. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  104. package/src/charting/templates/Template.js +219 -212
  105. package/src/cli.js +146 -0
  106. package/src/index.js +3370 -2995
  107. package/src/workspace/Workspace.js +126 -7
  108. package/src/workspace/archimateServices.js +4 -0
  109. package/src/workspace/index.js +3 -0
  110. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  111. package/src/workspace/parts/DecisionWorkbenchPart.js +58 -0
  112. package/src/workspace/parts/ViewsPart.js +196 -0
  113. package/src/workspace/styles.js +36 -1
@@ -0,0 +1,233 @@
1
+ import { Axis } from '../Axis.js';
2
+ import { Chart } from '../Chart.js';
3
+ import { ChartBox, ChartLabels } from '../ChartBox.js';
4
+ import { Legend } from '../Legend.js';
5
+ import { ColorPalette } from '../ColorPalette.js';
6
+
7
+ /**
8
+ * ComboChart — multi-series chart combining different series shapes (bars,
9
+ * lines, and areas) sharing a common category axis with optional dual/secondary
10
+ * Y-axes (`yAxis: 'left'|'right'`).
11
+ *
12
+ * Data shape:
13
+ * Array<{
14
+ * name: string,
15
+ * type?: 'bar'|'line'|'area',
16
+ * yAxis?: 'left'|'right',
17
+ * color?: string,
18
+ * values: Array<{label: string, value: number}> | number[]
19
+ * }>
20
+ * OR
21
+ * {
22
+ * labels?: string[],
23
+ * categories?: string[],
24
+ * series: Array<{
25
+ * name: string,
26
+ * type?: 'bar'|'line'|'area',
27
+ * yAxis?: 'left'|'right',
28
+ * values: number[] | Array<{label: string, value: number}>
29
+ * }>
30
+ * }
31
+ */
32
+ export class ComboChart extends Chart {
33
+ static chartType = 'combo';
34
+
35
+ static normalize(data, options = {}) {
36
+ if (!data) return { categories: [], series: [] };
37
+
38
+ let rawSeries = [];
39
+ let categories = options.categories ?? options.labels ?? [];
40
+
41
+ if (Array.isArray(data)) {
42
+ rawSeries = data;
43
+ } else if (typeof data === 'object') {
44
+ rawSeries = Array.isArray(data.series) ? data.series : [];
45
+ if (!categories.length) {
46
+ categories = data.labels ?? data.categories ?? [];
47
+ }
48
+ }
49
+
50
+ if (!rawSeries.length) return { categories: [], series: [] };
51
+
52
+ if (!categories.length) {
53
+ for (const s of rawSeries) {
54
+ if (Array.isArray(s.values) && s.values[0] && typeof s.values[0] === 'object' && 'label' in s.values[0]) {
55
+ categories = [...new Set(s.values.map(v => v.label))];
56
+ break;
57
+ }
58
+ }
59
+ }
60
+ if (!categories.length && Array.isArray(rawSeries[0]?.values)) {
61
+ categories = Array.from({ length: rawSeries[0].values.length }, (_, i) => `Cat ${i + 1}`);
62
+ }
63
+
64
+ const normalizedSeries = rawSeries.map((s, idx) => {
65
+ const name = s.name ?? `Series ${idx + 1}`;
66
+ const type = s.type ?? (idx === 0 ? 'bar' : 'line');
67
+ const yAxis = s.yAxis ?? (type === 'line' && idx > 0 && options.secondaryY ? 'right' : 'left');
68
+ const map = new Map();
69
+ if (Array.isArray(s.values)) {
70
+ s.values.forEach((v, i) => {
71
+ if (typeof v === 'object' && v !== null && 'label' in v) {
72
+ map.set(v.label, Number(v.value) || 0);
73
+ } else {
74
+ map.set(categories[i] ?? `#${i + 1}`, Number(v) || 0);
75
+ }
76
+ });
77
+ }
78
+ return {
79
+ name,
80
+ type,
81
+ yAxis,
82
+ color: s.color,
83
+ step: s.step,
84
+ values: categories.map(cat => ({ label: cat, value: map.get(cat) ?? 0 })),
85
+ };
86
+ });
87
+
88
+ return { categories, series: normalizedSeries };
89
+ }
90
+
91
+ _render() {
92
+ const { categories, series } = ComboChart.normalize(this.data, this.options);
93
+ if (!categories.length || !series.length) return;
94
+
95
+ const options = this.options || {};
96
+ const leftSeries = series.filter(s => s.yAxis !== 'right');
97
+ const rightSeries = series.filter(s => s.yAxis === 'right');
98
+ const hasRight = rightSeries.length > 0;
99
+
100
+ const rightMargin = (hasRight ? 50 : 20) + (options.secondaryYAxisLabel ? 30 : 0);
101
+ const box = ChartBox.fromRenderer(this.renderer, { right: rightMargin });
102
+ const {
103
+ x = box.x, y = box.y, width: optWidth, height = box.height,
104
+ title, xAxisLabel, yAxisLabel, secondaryYAxisLabel, colors, legend: showLegend = true,
105
+ } = options;
106
+
107
+ const palette = new ColorPalette(colors);
108
+ const legendItems = series.map((s, i) => ({
109
+ label: s.name,
110
+ color: s.color ?? palette.colorAt(i),
111
+ marker: s.type === 'bar' ? 'square' : 'circle',
112
+ hidden: this._dataFilter?.has(i),
113
+ }));
114
+
115
+ const width = optWidth ?? (showLegend
116
+ ? Legend.plotWidthBeside({ x, width: box.width }, this.renderer.width, { items: legendItems })
117
+ : box.width);
118
+ const plotBox = { x, y, width, height };
119
+
120
+ new ChartLabels({ title, xAxisLabel, yAxisLabel, secondaryYAxisLabel }).draw(this.renderer, plotBox);
121
+
122
+ // Compute Left and Right domains
123
+ const leftVals = leftSeries.flatMap(s => s.values.map(v => v.value));
124
+ const rawLeftMax = leftVals.length ? Math.max(...leftVals, 1) : 1;
125
+ const rawLeftMin = leftVals.length ? Math.min(0, ...leftVals) : 0;
126
+ const leftDomain = new Axis().drawYRange(this.renderer, plotBox, rawLeftMin, rawLeftMax);
127
+
128
+ let rightDomain = { min: 0, max: 1 };
129
+ if (hasRight) {
130
+ const rightVals = rightSeries.flatMap(s => s.values.map(v => v.value));
131
+ const rawRightMax = rightVals.length ? Math.max(...rightVals, 1) : 1;
132
+ const rawRightMin = rightVals.length ? Math.min(0, ...rightVals) : 0;
133
+ rightDomain = new Axis().drawSecondaryYRange(this.renderer, plotBox, rawRightMin, rawRightMax);
134
+ }
135
+
136
+ // Draw base axis
137
+ this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
138
+
139
+ const visibleSeries = series.filter((s, i) => !this._dataFilter?.has(i));
140
+ const barSeries = visibleSeries.filter(s => s.type === 'bar');
141
+ const barSeriesCount = barSeries.length;
142
+ const categoryCount = categories.length;
143
+ const clusterWidth = width / categoryCount;
144
+ const groupGap = clusterWidth * 0.2;
145
+ const availableWidth = clusterWidth - groupGap;
146
+ const barWidth = barSeriesCount > 0 ? (availableWidth / barSeriesCount) * 0.9 : 0;
147
+ const barMargin = barSeriesCount > 0 ? (availableWidth / barSeriesCount) * 0.1 : 0;
148
+
149
+ const toLeftY = v => y + height - ((v - leftDomain.min) / ((leftDomain.max - leftDomain.min) || 1)) * height;
150
+ const toRightY = v => y + height - ((v - rightDomain.min) / ((rightDomain.max - rightDomain.min) || 1)) * height;
151
+ const toY = (v, yAxis) => yAxis === 'right' ? toRightY(v) : toLeftY(v);
152
+
153
+ // 1. Draw Bars first
154
+ categories.forEach((cat, catIdx) => {
155
+ const clusterX = x + catIdx * clusterWidth + groupGap / 2;
156
+ barSeries.forEach((s, barIdx) => {
157
+ const sIdx = series.indexOf(s);
158
+ const color = s.color ?? palette.colorAt(sIdx);
159
+ const val = s.values[catIdx]?.value ?? 0;
160
+ const zeroY = toY(0, s.yAxis);
161
+ const valY = toY(val, s.yAxis);
162
+ const barTop = Math.min(zeroY, valY);
163
+ const barH = Math.max(1, Math.abs(valY - zeroY));
164
+ const bx = clusterX + barIdx * (barWidth + barMargin);
165
+
166
+ this._datum(this.renderer.rect(bx, barTop, barWidth, barH, color, null, {
167
+ tooltip: `${s.name} (${cat}): ${Axis.formatValue(val)}`,
168
+ }), { label: cat, series: s.name, value: val });
169
+
170
+ if (options.showValues) {
171
+ this.renderer.text(Axis.formatValue(val), bx + barWidth / 2, barTop - 5, {
172
+ textAnchor: this.renderer.centerTextAnchor, fontSize: '9px', fill: '#333',
173
+ });
174
+ }
175
+ });
176
+
177
+ // X category labels
178
+ const step = Axis.categoryLabelStep(categories, width);
179
+ if (catIdx % step === 0) {
180
+ this.renderer.text(cat, clusterX + availableWidth / 2, y + height + 20, {
181
+ textAnchor: this.renderer.centerTextAnchor, fontSize: '10px', fill: '#333',
182
+ });
183
+ }
184
+ });
185
+
186
+ // 2. Draw Area and Line series over the bars
187
+ const nonBarSeries = visibleSeries.filter(s => s.type !== 'bar');
188
+ nonBarSeries.forEach((s) => {
189
+ const sIdx = series.indexOf(s);
190
+ const color = s.color ?? palette.colorAt(sIdx);
191
+ const pts = categories.map((cat, catIdx) => {
192
+ const px = x + catIdx * clusterWidth + clusterWidth / 2;
193
+ const py = toY(s.values[catIdx]?.value ?? 0, s.yAxis);
194
+ return { px, py, val: s.values[catIdx]?.value ?? 0, cat };
195
+ });
196
+
197
+ if (s.type === 'area') {
198
+ const zeroY = toY(0, s.yAxis);
199
+ const areaD = 'M' + pts.map(p => `${p.px},${p.py}`).join(' L')
200
+ + ` L${pts[pts.length - 1].px},${zeroY} L${pts[0].px},${zeroY} Z`;
201
+ const { r, g, b: bl } = palette.hexToRgb(color);
202
+ this.renderer.path(areaD, `rgba(${r},${g},${bl},0.28)`, null, { tooltip: s.name });
203
+ }
204
+
205
+ let pathD = '';
206
+ pts.forEach((p, i) => {
207
+ if (i === 0) pathD += `M${p.px},${p.py}`;
208
+ else if (s.step === 'before') pathD += ` V${p.py} H${p.px}`;
209
+ else if (s.step) pathD += ` H${p.px} V${p.py}`;
210
+ else pathD += ` L${p.px},${p.py}`;
211
+ });
212
+ this.renderer.path(pathD, 'none', { color, width: 2.5 });
213
+
214
+ // Vertex dots
215
+ pts.forEach(p => {
216
+ this._datum(this.renderer.circle(p.px, p.py, 3.5, color, { color: '#ffffff', width: 1.5 }, {
217
+ tooltip: `${s.name} (${p.cat}): ${Axis.formatValue(p.val)}`,
218
+ }), { label: p.cat, series: s.name, value: p.val });
219
+
220
+ if (options.showValues) {
221
+ this.renderer.text(Axis.formatValue(p.val), p.px, p.py - 8, {
222
+ textAnchor: this.renderer.centerTextAnchor, fontSize: '9px', fill: color, fontWeight: 'bold',
223
+ });
224
+ }
225
+ });
226
+ });
227
+
228
+ // 3. Legend
229
+ if (showLegend) {
230
+ Legend.beside({ x, y, width }, this.renderer.width, { items: legendItems }).draw(this.renderer);
231
+ }
232
+ }
233
+ }
@@ -0,0 +1,310 @@
1
+ import { Chart } from '../Chart.js';
2
+ import { ChartBox, ChartLabels } from '../ChartBox.js';
3
+ import { ColorPalette } from '../ColorPalette.js';
4
+ import { Axis } from '../Axis.js';
5
+
6
+ /**
7
+ * ContourChart — 2D isoband and isoline contour plot on regular scalar grids
8
+ * using marching squares.
9
+ *
10
+ * Data forms:
11
+ * - 2D array of numbers: `number[][]` (rows are Y, columns are X)
12
+ * - Object: `{ grid: number[][], xValues?: number[], yValues?: number[] }`
13
+ * - Flat array: `Array<{x: number, y: number, value: number}>`
14
+ *
15
+ * Options:
16
+ * - levels: number (count of contour levels, default 8) or number[] of explicit thresholds
17
+ * - colorScale: string[] (gradient stops, default 9-stop spectral)
18
+ * - filled: boolean (default true — draw filled isoband cells)
19
+ * - showLines: boolean (default true — draw marching-squares isolines)
20
+ * - showLabels: boolean (default true — draw numeric value on contour lines)
21
+ * - lineColor: string (default 'rgba(0,0,0,0.35)')
22
+ * - lineWidth: number (default 1)
23
+ * - legend: boolean (default true — draw color bar legend)
24
+ * - title, xAxisLabel, yAxisLabel: string
25
+ */
26
+ export class ContourChart extends Chart {
27
+ static chartType = 'contour';
28
+
29
+ static DEFAULT_COLOR_SCALE = [
30
+ '#313695', '#4575b4', '#74add1', '#abd9e9',
31
+ '#e0f3f8', '#fee090', '#fdae61', '#f46d43', '#d73027',
32
+ ];
33
+
34
+ /** Normalize various input shapes to `{ grid: number[][], xValues: number[], yValues: number[] }`. */
35
+ static normalizeData(raw) {
36
+ if (!raw) return { grid: [[]], xValues: [], yValues: [] };
37
+
38
+ if (Array.isArray(raw) && Array.isArray(raw[0])) {
39
+ const nrows = raw.length;
40
+ const ncols = raw[0].length;
41
+ const xValues = Array.from({ length: ncols }, (_, i) => i);
42
+ const yValues = Array.from({ length: nrows }, (_, i) => i);
43
+ return { grid: raw, xValues, yValues };
44
+ }
45
+
46
+ if (raw.grid && Array.isArray(raw.grid)) {
47
+ const nrows = raw.grid.length;
48
+ const ncols = raw.grid[0]?.length || 0;
49
+ const xValues = raw.xValues || Array.from({ length: ncols }, (_, i) => i);
50
+ const yValues = raw.yValues || Array.from({ length: nrows }, (_, i) => i);
51
+ return { grid: raw.grid, xValues, yValues };
52
+ }
53
+
54
+ if (Array.isArray(raw) && raw.length > 0 && typeof raw[0] === 'object' && 'x' in raw[0] && 'y' in raw[0]) {
55
+ const xSet = Array.from(new Set(raw.map(d => d.x))).sort((a, b) => a - b);
56
+ const ySet = Array.from(new Set(raw.map(d => d.y))).sort((a, b) => a - b);
57
+ const xIndex = new Map(xSet.map((x, i) => [x, i]));
58
+ const yIndex = new Map(ySet.map((y, i) => [y, i]));
59
+
60
+ const grid = Array.from({ length: ySet.length }, () => new Array(xSet.length).fill(0));
61
+ for (const d of raw) {
62
+ const r = yIndex.get(d.y);
63
+ const c = xIndex.get(d.x);
64
+ if (r !== undefined && c !== undefined) {
65
+ grid[r][c] = d.value ?? d.z ?? 0;
66
+ }
67
+ }
68
+ return { grid, xValues: xSet, yValues: ySet };
69
+ }
70
+
71
+ return { grid: [[]], xValues: [], yValues: [] };
72
+ }
73
+
74
+ /** Compute marching squares line segments for a scalar grid at threshold T. */
75
+ static marchingSquares(grid, xCoords, yCoords, threshold) {
76
+ const nrows = grid.length;
77
+ if (nrows < 2) return [];
78
+ const ncols = grid[0].length;
79
+ if (ncols < 2) return [];
80
+
81
+ const segments = [];
82
+
83
+ // Linear interpolation helper between two values and their positions
84
+ const interp = (v1, v2, p1, p2) => {
85
+ if (Math.abs(v2 - v1) < 1e-9) return (p1 + p2) / 2;
86
+ const t = (threshold - v1) / (v2 - v1);
87
+ return p1 + t * (p2 - p1);
88
+ };
89
+
90
+ for (let r = 0; r < nrows - 1; r++) {
91
+ const y0 = yCoords[r];
92
+ const y1 = yCoords[r + 1];
93
+
94
+ for (let c = 0; c < ncols - 1; c++) {
95
+ const x0 = xCoords[c];
96
+ const x1 = xCoords[c + 1];
97
+
98
+ const vTL = grid[r][c];
99
+ const vTR = grid[r][c + 1];
100
+ const vBR = grid[r + 1][c + 1];
101
+ const vBL = grid[r + 1][c];
102
+
103
+ let mask = 0;
104
+ if (vTL >= threshold) mask |= 1;
105
+ if (vTR >= threshold) mask |= 2;
106
+ if (vBR >= threshold) mask |= 4;
107
+ if (vBL >= threshold) mask |= 8;
108
+
109
+ if (mask === 0 || mask === 15) continue;
110
+
111
+ const top = [interp(vTL, vTR, x0, x1), y0];
112
+ const right = [x1, interp(vTR, vBR, y0, y1)];
113
+ const bottom = [interp(vBL, vBR, x0, x1), y1];
114
+ const left = [x0, interp(vTL, vBL, y0, y1)];
115
+
116
+ switch (mask) {
117
+ case 1: // TL
118
+ case 14: // TR, BR, BL
119
+ segments.push([top, left]);
120
+ break;
121
+ case 2: // TR
122
+ case 13: // TL, BR, BL
123
+ segments.push([top, right]);
124
+ break;
125
+ case 3: // TL, TR
126
+ case 12: // BR, BL
127
+ segments.push([left, right]);
128
+ break;
129
+ case 4: // BR
130
+ case 11: // TL, TR, BL
131
+ segments.push([right, bottom]);
132
+ break;
133
+ case 5: { // TL, BR (saddle)
134
+ const center = (vTL + vTR + vBR + vBL) / 4;
135
+ if (center >= threshold) {
136
+ segments.push([left, top]);
137
+ segments.push([right, bottom]);
138
+ } else {
139
+ segments.push([left, bottom]);
140
+ segments.push([top, right]);
141
+ }
142
+ break;
143
+ }
144
+ case 6: // TR, BR
145
+ case 9: // TL, BL
146
+ segments.push([top, bottom]);
147
+ break;
148
+ case 7: // TL, TR, BR
149
+ case 8: // BL
150
+ segments.push([left, bottom]);
151
+ break;
152
+ case 10: { // TR, BL (saddle)
153
+ const center = (vTL + vTR + vBR + vBL) / 4;
154
+ if (center >= threshold) {
155
+ segments.push([top, right]);
156
+ segments.push([bottom, left]);
157
+ } else {
158
+ segments.push([top, left]);
159
+ segments.push([bottom, right]);
160
+ }
161
+ break;
162
+ }
163
+ }
164
+ }
165
+ }
166
+ return segments;
167
+ }
168
+
169
+ _render() {
170
+ const { grid, xValues, yValues } = ContourChart.normalizeData(this.data);
171
+ const options = this.options || {};
172
+ const box = ChartBox.fromRenderer(this.renderer);
173
+ const {
174
+ x = box.x, y = box.y, width = box.width, height = box.height,
175
+ levels = 8,
176
+ colorScale = ContourChart.DEFAULT_COLOR_SCALE,
177
+ filled = true,
178
+ showLines = true,
179
+ showLabels = true,
180
+ lineColor = 'rgba(0,0,0,0.3)',
181
+ lineWidth = 1,
182
+ legend = true,
183
+ title, xAxisLabel, yAxisLabel,
184
+ } = options;
185
+
186
+ const nrows = grid.length;
187
+ const ncols = grid[0]?.length || 0;
188
+ if (nrows < 2 || ncols < 2) return;
189
+
190
+ let vMin = Infinity;
191
+ let vMax = -Infinity;
192
+ for (let r = 0; r < nrows; r++) {
193
+ for (let c = 0; c < ncols; c++) {
194
+ const v = grid[r][c];
195
+ if (v < vMin) vMin = v;
196
+ if (v > vMax) vMax = v;
197
+ }
198
+ }
199
+ if (vMin === vMax) {
200
+ vMin -= 1;
201
+ vMax += 1;
202
+ }
203
+
204
+ const plotW = legend ? width - 60 : width;
205
+ const plotH = height;
206
+
207
+ new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, { x, y, width: plotW, height: plotH });
208
+
209
+ // Calculate explicit threshold levels
210
+ const thresholds = Array.isArray(levels)
211
+ ? levels.slice().sort((a, b) => a - b)
212
+ : Array.from({ length: levels }, (_, i) => vMin + ((i + 1) / (levels + 1)) * (vMax - vMin));
213
+
214
+ const xMin = xValues[0];
215
+ const xMax = xValues[ncols - 1];
216
+ const yMin = yValues[0];
217
+ const yMax = yValues[nrows - 1];
218
+
219
+ const xRange = (xMax - xMin) || 1;
220
+ const yRange = (yMax - yMin) || 1;
221
+
222
+ const toScreenX = (xv) => x + ((xv - xMin) / xRange) * plotW;
223
+ const toScreenY = (yv) => y + plotH - ((yv - yMin) / yRange) * plotH;
224
+
225
+ const screenXCoords = xValues.map(toScreenX);
226
+ const screenYCoords = yValues.map(toScreenY);
227
+
228
+ // 1. Filled isoband cells
229
+ if (filled) {
230
+ for (let r = 0; r < nrows - 1; r++) {
231
+ const yTop = Math.min(screenYCoords[r], screenYCoords[r + 1]);
232
+ const cellH = Math.abs(screenYCoords[r + 1] - screenYCoords[r]);
233
+
234
+ for (let c = 0; c < ncols - 1; c++) {
235
+ const xLeft = Math.min(screenXCoords[c], screenXCoords[c + 1]);
236
+ const cellW = Math.abs(screenXCoords[c + 1] - screenXCoords[c]);
237
+
238
+ // Average cell corner value to determine cell color
239
+ const avgVal = (grid[r][c] + grid[r][c + 1] + grid[r + 1][c] + grid[r + 1][c + 1]) / 4;
240
+ const color = ColorPalette.scaleColor(colorScale, vMin, vMax, avgVal);
241
+
242
+ this.renderer.rect(xLeft, yTop, cellW + 0.5, cellH + 0.5, color, null, {
243
+ tooltip: `Value: ${Axis.formatValue(avgVal)}`,
244
+ });
245
+ }
246
+ }
247
+ }
248
+
249
+ // 2. Isolines & labels via marching squares
250
+ if (showLines) {
251
+ thresholds.forEach((threshold, tIdx) => {
252
+ const segments = ContourChart.marchingSquares(grid, screenXCoords, screenYCoords, threshold);
253
+ if (!segments.length) return;
254
+
255
+ let pathData = '';
256
+ segments.forEach(([p1, p2]) => {
257
+ pathData += `M${p1[0].toFixed(1)},${p1[1].toFixed(1)} L${p2[0].toFixed(1)},${p2[1].toFixed(1)} `;
258
+ });
259
+
260
+ if (pathData) {
261
+ this.renderer.path(pathData.trim(), 'none', { color: lineColor, width: lineWidth });
262
+ }
263
+
264
+ // Add contour label along middle segment if showLabels is enabled
265
+ if (showLabels && segments.length > 2) {
266
+ const midSeg = segments[Math.floor(segments.length / 2)];
267
+ const labelX = (midSeg[0][0] + midSeg[1][0]) / 2;
268
+ const labelY = (midSeg[0][1] + midSeg[1][1]) / 2;
269
+ const formatted = Axis.formatValue(threshold);
270
+
271
+ this.renderer.text(formatted, labelX, labelY, {
272
+ textAnchor: 'middle',
273
+ verticalAlign: 'middle',
274
+ fontSize: '9px',
275
+ fill: '#222',
276
+ stroke: '#fff',
277
+ strokeWidth: 2,
278
+ });
279
+ }
280
+ });
281
+ }
282
+
283
+ // 3. Axes
284
+ const axis = new Axis();
285
+ axis.drawXRange(this.renderer, { x, y, width: plotW, height: plotH }, xMin, xMax);
286
+ axis.drawYRange(this.renderer, { x, y, width: plotW, height: plotH }, yMin, yMax);
287
+
288
+ // 4. Color bar legend
289
+ if (legend) {
290
+ const barX = x + plotW + 20;
291
+ const barW = 14;
292
+ const barY = y;
293
+ const barH = plotH;
294
+ const numStops = 20;
295
+ const sliceH = barH / numStops;
296
+
297
+ for (let i = 0; i < numStops; i++) {
298
+ const val = vMax - (i / (numStops - 1)) * (vMax - vMin);
299
+ const color = ColorPalette.scaleColor(colorScale, vMin, vMax, val);
300
+ this.renderer.rect(barX, barY + i * sliceH, barW, sliceH + 0.5, color, null);
301
+ }
302
+ this.renderer.rect(barX, barY, barW, barH, 'none', { color: '#888', width: 1 });
303
+
304
+ // Max, Mid, Min ticks
305
+ this.renderer.text(Axis.formatValue(vMax), barX + barW + 4, barY + 4, { textAnchor: 'start', fontSize: '9px', fill: '#444' });
306
+ this.renderer.text(Axis.formatValue((vMin + vMax) / 2), barX + barW + 4, barY + barH / 2, { textAnchor: 'start', verticalAlign: 'middle', fontSize: '9px', fill: '#444' });
307
+ this.renderer.text(Axis.formatValue(vMin), barX + barW + 4, barY + barH - 2, { textAnchor: 'start', fontSize: '9px', fill: '#444' });
308
+ }
309
+ }
310
+ }
@@ -0,0 +1,13 @@
1
+ import { PieChart3D } from './PieChart3D.js';
2
+
3
+ /**
4
+ * DonutChart3D — extruded 3D tilted doughnut chart with inner hole cylinder wall,
5
+ * outer cylinder skirt, radial cut walls, exploded slices, and hover pull-out.
6
+ */
7
+ export class DonutChart3D extends PieChart3D {
8
+ static chartType = 'donut3D';
9
+
10
+ constructor(renderer, data, options = {}) {
11
+ super(renderer, data, { innerRadiusRatio: 0.5, ...options });
12
+ }
13
+ }