@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.
- package/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49641 -39747
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +206 -0
- package/examples/decision-workbench.html +26 -0
- package/package.json +7 -1
- 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 +219 -0
- 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 +3 -0
- 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 +114 -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/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- 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/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 +4 -1
- 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 +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- 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 +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- 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 +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- 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 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/index.js +3 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/parts/DecisionWorkbenchPart.js +58 -0
- package/src/workspace/parts/ViewsPart.js +196 -0
- 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
|
+
}
|