@meonode/canvas 1.0.0 → 1.1.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 +142 -10
- package/dist/cjs/canvas/canvas.type.d.ts +101 -1
- package/dist/cjs/canvas/canvas.type.d.ts.map +1 -1
- package/dist/cjs/canvas/chart.canvas.util.d.ts +20 -0
- package/dist/cjs/canvas/chart.canvas.util.d.ts.map +1 -0
- package/dist/cjs/canvas/chart.canvas.util.js +582 -0
- package/dist/cjs/canvas/chart.canvas.util.js.map +1 -0
- package/dist/cjs/canvas/text.canvas.util.d.ts +18 -0
- package/dist/cjs/canvas/text.canvas.util.d.ts.map +1 -1
- package/dist/cjs/canvas/text.canvas.util.js +19 -0
- package/dist/cjs/canvas/text.canvas.util.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/canvas/canvas.type.d.ts +101 -1
- package/dist/esm/canvas/canvas.type.d.ts.map +1 -1
- package/dist/esm/canvas/chart.canvas.util.d.ts +20 -0
- package/dist/esm/canvas/chart.canvas.util.d.ts.map +1 -0
- package/dist/esm/canvas/chart.canvas.util.js +578 -0
- package/dist/esm/canvas/text.canvas.util.d.ts +18 -0
- package/dist/esm/canvas/text.canvas.util.d.ts.map +1 -1
- package/dist/esm/canvas/text.canvas.util.js +19 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var layout_canvas_util = require('./layout.canvas.util.js');
|
|
4
|
+
var common_const = require('../constant/common.const.js');
|
|
5
|
+
var text_canvas_util = require('./text.canvas.util.js');
|
|
6
|
+
|
|
7
|
+
class ChartNode extends layout_canvas_util.BoxNode {
|
|
8
|
+
chartData;
|
|
9
|
+
chartType;
|
|
10
|
+
chartOptions;
|
|
11
|
+
constructor(props) {
|
|
12
|
+
// Set default intrinsic size if not provided
|
|
13
|
+
const defaultWidth = props.width ?? 400;
|
|
14
|
+
const defaultHeight = props.height ?? 300;
|
|
15
|
+
super({
|
|
16
|
+
...props,
|
|
17
|
+
width: defaultWidth,
|
|
18
|
+
height: defaultHeight,
|
|
19
|
+
name: 'Chart',
|
|
20
|
+
});
|
|
21
|
+
this.chartData = props.data;
|
|
22
|
+
this.chartType = props.type;
|
|
23
|
+
this.chartOptions = {
|
|
24
|
+
showLabels: true,
|
|
25
|
+
showLegend: true,
|
|
26
|
+
labelFontSize: 12,
|
|
27
|
+
legendPosition: 'bottom',
|
|
28
|
+
...props.options,
|
|
29
|
+
};
|
|
30
|
+
this.validateProps();
|
|
31
|
+
}
|
|
32
|
+
validateProps() {
|
|
33
|
+
if (this.chartType === 'bar' || this.chartType === 'line') {
|
|
34
|
+
const data = this.chartData;
|
|
35
|
+
if (!data.labels || !data.datasets) {
|
|
36
|
+
console.warn(`[ChartNode] Warning: Cartesian chart (${this.chartType}) is missing 'labels' or 'datasets' in its data prop.`);
|
|
37
|
+
}
|
|
38
|
+
data.datasets?.forEach((dataset, i) => {
|
|
39
|
+
if (dataset.data.length !== data.labels.length) {
|
|
40
|
+
console.warn(`[ChartNode] Warning: In dataset ${i} ("${dataset.label}"), the number of data points (${dataset.data.length}) does not match the number of labels (${data.labels.length}).`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else if (this.chartType === 'pie' || this.chartType === 'doughnut') {
|
|
45
|
+
const data = this.chartData;
|
|
46
|
+
if (!Array.isArray(data)) {
|
|
47
|
+
console.warn(`[ChartNode] Warning: ${this.chartType} chart expects an array of PieChartDataPoint, but received a different type.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
_renderContent(ctx, x, y, width, height) {
|
|
52
|
+
// First render background/borders from parent
|
|
53
|
+
super._renderContent(ctx, x, y, width, height);
|
|
54
|
+
// Then render chart-specific content
|
|
55
|
+
const paddingLeft = this.node.getComputedPadding(common_const.Style.Edge.Left);
|
|
56
|
+
const paddingRight = this.node.getComputedPadding(common_const.Style.Edge.Right);
|
|
57
|
+
const paddingTop = this.node.getComputedPadding(common_const.Style.Edge.Top);
|
|
58
|
+
const paddingBottom = this.node.getComputedPadding(common_const.Style.Edge.Bottom);
|
|
59
|
+
const contentX = x + paddingLeft;
|
|
60
|
+
const contentY = y + paddingTop;
|
|
61
|
+
const contentWidth = width - paddingLeft - paddingRight;
|
|
62
|
+
const contentHeight = height - paddingTop - paddingBottom;
|
|
63
|
+
switch (this.chartType) {
|
|
64
|
+
case 'bar':
|
|
65
|
+
this.renderBarChart(ctx, contentX, contentY, contentWidth, contentHeight);
|
|
66
|
+
break;
|
|
67
|
+
case 'line':
|
|
68
|
+
this.renderLineChart(ctx, contentX, contentY, contentWidth, contentHeight);
|
|
69
|
+
break;
|
|
70
|
+
case 'pie':
|
|
71
|
+
this.renderPieChart(ctx, contentX, contentY, contentWidth, contentHeight);
|
|
72
|
+
break;
|
|
73
|
+
case 'doughnut':
|
|
74
|
+
this.renderDoughnutChart(ctx, contentX, contentY, contentWidth, contentHeight);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
getLegendLayout(ctx, totalWidth, totalHeight) {
|
|
79
|
+
if (!this.chartOptions?.showLegend) {
|
|
80
|
+
return { x: 0, y: 0, width: 0, height: 0, chartWidth: totalWidth, chartHeight: totalHeight, chartX: 0, chartY: 0 };
|
|
81
|
+
}
|
|
82
|
+
const legendItems = 'datasets' in this.chartData ? this.chartData.datasets : this.chartData;
|
|
83
|
+
if (legendItems.length === 0) {
|
|
84
|
+
return { x: 0, y: 0, width: 0, height: 0, chartWidth: totalWidth, chartHeight: totalHeight, chartX: 0, chartY: 0 };
|
|
85
|
+
}
|
|
86
|
+
const fontSize = this.chartOptions?.labelFontSize || 12;
|
|
87
|
+
ctx.font = `${fontSize}px ${this.props.fontFamily || 'sans-serif'}`;
|
|
88
|
+
const metrics = ctx.measureText('Mg');
|
|
89
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
90
|
+
const itemHeight = Math.ceil(textHeight + 8);
|
|
91
|
+
const position = this.chartOptions.legendPosition;
|
|
92
|
+
const boxSize = Math.min(15, itemHeight - 2);
|
|
93
|
+
const legendItemLabels = 'datasets' in this.chartData ? this.chartData.datasets.map(d => d.label) : this.chartData.map(p => `${p.label} (${p.value})`);
|
|
94
|
+
let calculatedLegendHeight;
|
|
95
|
+
let calculatedLegendWidth;
|
|
96
|
+
if (position === 'top' || position === 'bottom') {
|
|
97
|
+
let currentX = 0;
|
|
98
|
+
let numRows = 1;
|
|
99
|
+
const itemPadding = 20;
|
|
100
|
+
legendItemLabels.forEach(label => {
|
|
101
|
+
const labelWidth = ctx.measureText(label).width;
|
|
102
|
+
const itemWidth = boxSize + 5 + labelWidth + itemPadding;
|
|
103
|
+
if (currentX > 0 && currentX + itemWidth > totalWidth) {
|
|
104
|
+
numRows++;
|
|
105
|
+
currentX = 0;
|
|
106
|
+
}
|
|
107
|
+
currentX += itemWidth;
|
|
108
|
+
});
|
|
109
|
+
calculatedLegendHeight = numRows * itemHeight + 10;
|
|
110
|
+
calculatedLegendWidth = totalWidth;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// 'left' or 'right'
|
|
114
|
+
const maxLabelWidth = Math.max(...legendItemLabels.map(label => ctx.measureText(label).width));
|
|
115
|
+
calculatedLegendWidth = maxLabelWidth + boxSize + 25; // padding + box + padding + text
|
|
116
|
+
calculatedLegendHeight = totalHeight;
|
|
117
|
+
}
|
|
118
|
+
let effectiveChartWidth = totalWidth;
|
|
119
|
+
let effectiveChartHeight = totalHeight;
|
|
120
|
+
let legendAreaX;
|
|
121
|
+
let legendAreaY;
|
|
122
|
+
let chartAreaX;
|
|
123
|
+
let chartAreaY;
|
|
124
|
+
let legendAreaWidth;
|
|
125
|
+
let legendAreaHeight;
|
|
126
|
+
if (position === 'top' || position === 'bottom') {
|
|
127
|
+
effectiveChartHeight -= calculatedLegendHeight;
|
|
128
|
+
legendAreaHeight = calculatedLegendHeight;
|
|
129
|
+
legendAreaWidth = totalWidth;
|
|
130
|
+
legendAreaX = 0;
|
|
131
|
+
chartAreaX = 0;
|
|
132
|
+
if (position === 'top') {
|
|
133
|
+
chartAreaY = calculatedLegendHeight;
|
|
134
|
+
legendAreaY = 0;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
// bottom
|
|
138
|
+
legendAreaY = effectiveChartHeight;
|
|
139
|
+
chartAreaY = 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// 'left' or 'right'
|
|
144
|
+
effectiveChartWidth -= calculatedLegendWidth;
|
|
145
|
+
legendAreaWidth = calculatedLegendWidth;
|
|
146
|
+
legendAreaHeight = totalHeight;
|
|
147
|
+
legendAreaY = 0;
|
|
148
|
+
chartAreaY = 0;
|
|
149
|
+
if (position === 'left') {
|
|
150
|
+
chartAreaX = calculatedLegendWidth;
|
|
151
|
+
legendAreaX = 0;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// right
|
|
155
|
+
legendAreaX = effectiveChartWidth;
|
|
156
|
+
chartAreaX = 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
x: legendAreaX,
|
|
161
|
+
y: legendAreaY,
|
|
162
|
+
width: legendAreaWidth,
|
|
163
|
+
height: legendAreaHeight,
|
|
164
|
+
chartWidth: effectiveChartWidth,
|
|
165
|
+
chartHeight: effectiveChartHeight,
|
|
166
|
+
chartX: chartAreaX,
|
|
167
|
+
chartY: chartAreaY,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
renderBarChart(ctx, x, y, width, height) {
|
|
171
|
+
if (this.chartType !== 'bar')
|
|
172
|
+
return;
|
|
173
|
+
const chartData = this.chartData;
|
|
174
|
+
const chartOptions = this.chartOptions;
|
|
175
|
+
const legendLayout = this.getLegendLayout(ctx, width, height);
|
|
176
|
+
const chartX = x + legendLayout.chartX;
|
|
177
|
+
const chartY = y + legendLayout.chartY;
|
|
178
|
+
const chartWidth = legendLayout.chartWidth;
|
|
179
|
+
const chartHeight = legendLayout.chartHeight;
|
|
180
|
+
const { labels, datasets } = chartData;
|
|
181
|
+
const maxValue = Math.max(...datasets.flatMap(d => d.data));
|
|
182
|
+
let labelHeight = 0;
|
|
183
|
+
if (chartOptions?.showLabels) {
|
|
184
|
+
const fontSize = chartOptions.labelFontSize || 12;
|
|
185
|
+
ctx.font = `${fontSize}px ${this.props.fontFamily || 'sans-serif'}`;
|
|
186
|
+
const metrics = ctx.measureText('Mg');
|
|
187
|
+
labelHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent + 10; // with padding
|
|
188
|
+
}
|
|
189
|
+
const finalChartHeight = chartHeight - labelHeight;
|
|
190
|
+
const groupWidth = chartWidth / labels.length;
|
|
191
|
+
const barSpacing = groupWidth * 0.2;
|
|
192
|
+
const barWidth = (groupWidth - barSpacing) / datasets.length;
|
|
193
|
+
// Render grid
|
|
194
|
+
if (chartOptions?.grid?.show) {
|
|
195
|
+
ctx.strokeStyle = chartOptions.grid.color || '#e0e0e0';
|
|
196
|
+
ctx.lineWidth = 1;
|
|
197
|
+
if (chartOptions.grid.style === 'dashed') {
|
|
198
|
+
ctx.setLineDash([5, 5]);
|
|
199
|
+
}
|
|
200
|
+
else if (chartOptions.grid.style === 'dotted') {
|
|
201
|
+
ctx.setLineDash([2, 2]);
|
|
202
|
+
}
|
|
203
|
+
for (let i = 0; i <= 5; i++) {
|
|
204
|
+
const gridY = chartY + (finalChartHeight / 5) * i;
|
|
205
|
+
ctx.beginPath();
|
|
206
|
+
ctx.moveTo(chartX, gridY);
|
|
207
|
+
ctx.lineTo(chartX + chartWidth, gridY);
|
|
208
|
+
ctx.stroke();
|
|
209
|
+
}
|
|
210
|
+
ctx.setLineDash([]);
|
|
211
|
+
}
|
|
212
|
+
// Render bars
|
|
213
|
+
labels.forEach((label, index) => {
|
|
214
|
+
const groupX = chartX + index * groupWidth + barSpacing / 2;
|
|
215
|
+
datasets.forEach((dataset, datasetIndex) => {
|
|
216
|
+
const barHeight = (dataset.data[index] / maxValue) * finalChartHeight;
|
|
217
|
+
const barX = groupX + datasetIndex * barWidth;
|
|
218
|
+
const barY = chartY + finalChartHeight - barHeight;
|
|
219
|
+
ctx.fillStyle = dataset.color || this.generateColor(datasetIndex);
|
|
220
|
+
ctx.fillRect(barX, barY, barWidth, barHeight);
|
|
221
|
+
});
|
|
222
|
+
// Render labels
|
|
223
|
+
if (chartOptions?.showLabels) {
|
|
224
|
+
const { renderLabelItem } = chartOptions;
|
|
225
|
+
if (renderLabelItem) {
|
|
226
|
+
const labelNode = renderLabelItem({ item: label, index });
|
|
227
|
+
if (labelNode) {
|
|
228
|
+
labelNode.processInitialChildren();
|
|
229
|
+
labelNode.node.calculateLayout(undefined, undefined, common_const.Style.Direction.LTR);
|
|
230
|
+
const layout = labelNode.node.getComputedLayout();
|
|
231
|
+
labelNode.render(ctx, groupX + (groupWidth - barSpacing) / 2 - layout.width / 2, chartY + finalChartHeight + labelHeight / 2 - layout.height / 2);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
text_canvas_util.TextNode.renderSimpleText(ctx, label, groupX + (groupWidth - barSpacing) / 2, chartY + finalChartHeight + labelHeight / 2, {
|
|
236
|
+
color: chartOptions.labelColor || chartOptions.axisColor,
|
|
237
|
+
fontSize: chartOptions.labelFontSize,
|
|
238
|
+
fontFamily: this.props.fontFamily,
|
|
239
|
+
textAlign: 'center',
|
|
240
|
+
textBaseline: 'middle',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// Render legend
|
|
246
|
+
if (chartOptions?.showLegend) {
|
|
247
|
+
this.renderLegend(ctx, x + legendLayout.x, y + legendLayout.y, legendLayout.width, legendLayout.height);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
renderLineChart(ctx, x, y, width, height) {
|
|
251
|
+
if (this.chartType !== 'line')
|
|
252
|
+
return;
|
|
253
|
+
const chartData = this.chartData;
|
|
254
|
+
const chartOptions = this.chartOptions;
|
|
255
|
+
const legendLayout = this.getLegendLayout(ctx, width, height);
|
|
256
|
+
const chartX = x + legendLayout.chartX;
|
|
257
|
+
const chartY = y + legendLayout.chartY;
|
|
258
|
+
const chartWidth = legendLayout.chartWidth;
|
|
259
|
+
const chartHeight = legendLayout.chartHeight;
|
|
260
|
+
const { labels, datasets } = chartData;
|
|
261
|
+
const maxValue = Math.max(...datasets.flatMap(d => d.data));
|
|
262
|
+
let labelHeight = 0;
|
|
263
|
+
if (chartOptions?.showLabels) {
|
|
264
|
+
const fontSize = chartOptions.labelFontSize || 12;
|
|
265
|
+
ctx.font = `${fontSize}px ${this.props.fontFamily || 'sans-serif'}`;
|
|
266
|
+
const metrics = ctx.measureText('Mg');
|
|
267
|
+
labelHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent + 10; // with padding
|
|
268
|
+
}
|
|
269
|
+
const finalChartHeight = chartHeight - labelHeight;
|
|
270
|
+
const pointSpacing = chartWidth / (labels.length > 1 ? labels.length - 1 : 1);
|
|
271
|
+
// Render grid
|
|
272
|
+
if (chartOptions?.grid?.show) {
|
|
273
|
+
ctx.strokeStyle = chartOptions.grid.color || '#e0e0e0';
|
|
274
|
+
ctx.lineWidth = 1;
|
|
275
|
+
if (chartOptions.grid.style === 'dashed') {
|
|
276
|
+
ctx.setLineDash([5, 5]);
|
|
277
|
+
}
|
|
278
|
+
else if (chartOptions.grid.style === 'dotted') {
|
|
279
|
+
ctx.setLineDash([2, 2]);
|
|
280
|
+
}
|
|
281
|
+
for (let i = 0; i <= 5; i++) {
|
|
282
|
+
const gridY = chartY + (finalChartHeight / 5) * i;
|
|
283
|
+
ctx.beginPath();
|
|
284
|
+
ctx.moveTo(chartX, gridY);
|
|
285
|
+
ctx.lineTo(chartX + chartWidth, gridY);
|
|
286
|
+
ctx.stroke();
|
|
287
|
+
}
|
|
288
|
+
ctx.setLineDash([]);
|
|
289
|
+
}
|
|
290
|
+
// Render lines and points
|
|
291
|
+
datasets.forEach((dataset, datasetIndex) => {
|
|
292
|
+
ctx.strokeStyle = dataset.color || this.generateColor(datasetIndex);
|
|
293
|
+
ctx.lineWidth = 2;
|
|
294
|
+
ctx.beginPath();
|
|
295
|
+
dataset.data.forEach((value, index) => {
|
|
296
|
+
const pointX = chartX + index * pointSpacing;
|
|
297
|
+
const pointY = chartY + finalChartHeight - (value / maxValue) * finalChartHeight;
|
|
298
|
+
if (index === 0) {
|
|
299
|
+
ctx.moveTo(pointX, pointY);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
ctx.lineTo(pointX, pointY);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
ctx.stroke();
|
|
306
|
+
// Render points
|
|
307
|
+
dataset.data.forEach((value, index) => {
|
|
308
|
+
const pointX = chartX + index * pointSpacing;
|
|
309
|
+
const pointY = chartY + finalChartHeight - (value / maxValue) * finalChartHeight;
|
|
310
|
+
ctx.fillStyle = dataset.color || this.generateColor(datasetIndex);
|
|
311
|
+
ctx.beginPath();
|
|
312
|
+
ctx.arc(pointX, pointY, 4, 0, Math.PI * 2);
|
|
313
|
+
ctx.fill();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
// Render labels
|
|
317
|
+
if (chartOptions?.showLabels) {
|
|
318
|
+
const { renderLabelItem } = chartOptions;
|
|
319
|
+
labels.forEach((label, index) => {
|
|
320
|
+
const pointX = chartX + index * pointSpacing;
|
|
321
|
+
if (renderLabelItem) {
|
|
322
|
+
const labelNode = renderLabelItem({ item: label, index });
|
|
323
|
+
if (labelNode) {
|
|
324
|
+
labelNode.processInitialChildren();
|
|
325
|
+
labelNode.node.calculateLayout(undefined, undefined, common_const.Style.Direction.LTR);
|
|
326
|
+
const layout = labelNode.node.getComputedLayout();
|
|
327
|
+
labelNode.render(ctx, pointX - layout.width / 2, chartY + finalChartHeight + labelHeight / 2 - layout.height / 2);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
text_canvas_util.TextNode.renderSimpleText(ctx, label, pointX, chartY + finalChartHeight + labelHeight / 2, {
|
|
332
|
+
color: chartOptions.labelColor || chartOptions.axisColor,
|
|
333
|
+
fontSize: chartOptions.labelFontSize,
|
|
334
|
+
fontFamily: this.props.fontFamily,
|
|
335
|
+
textAlign: 'center',
|
|
336
|
+
textBaseline: 'middle',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (chartOptions?.showLegend) {
|
|
342
|
+
this.renderLegend(ctx, x + legendLayout.x, y + legendLayout.y, legendLayout.width, legendLayout.height);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
renderPieChart(ctx, x, y, width, height) {
|
|
346
|
+
if (this.chartType !== 'pie')
|
|
347
|
+
return;
|
|
348
|
+
const data = this.chartData;
|
|
349
|
+
const chartOptions = this.chartOptions;
|
|
350
|
+
const legendLayout = this.getLegendLayout(ctx, width, height);
|
|
351
|
+
const chartX = x + legendLayout.chartX;
|
|
352
|
+
const chartY = y + legendLayout.chartY;
|
|
353
|
+
const chartWidth = legendLayout.chartWidth;
|
|
354
|
+
const chartHeight = legendLayout.chartHeight;
|
|
355
|
+
const centerX = chartX + chartWidth / 2;
|
|
356
|
+
const centerY = chartY + chartHeight / 2;
|
|
357
|
+
const radius = Math.min(chartWidth, chartHeight) / 2 - 10;
|
|
358
|
+
const total = data.reduce((sum, point) => sum + point.value, 0);
|
|
359
|
+
let currentAngle = -Math.PI / 2; // Start at top
|
|
360
|
+
data.forEach((point, index) => {
|
|
361
|
+
const sliceAngle = (point.value / total) * Math.PI * 2;
|
|
362
|
+
const startAngle = currentAngle;
|
|
363
|
+
const endAngle = currentAngle + sliceAngle;
|
|
364
|
+
ctx.fillStyle = point.color || this.generateColor(index);
|
|
365
|
+
ctx.beginPath();
|
|
366
|
+
ctx.moveTo(centerX, centerY);
|
|
367
|
+
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
|
|
368
|
+
ctx.closePath();
|
|
369
|
+
ctx.fill();
|
|
370
|
+
// Draw slice border
|
|
371
|
+
ctx.strokeStyle = '#fff';
|
|
372
|
+
ctx.lineWidth = 2;
|
|
373
|
+
ctx.stroke();
|
|
374
|
+
// Render labels
|
|
375
|
+
if (chartOptions?.showLabels) {
|
|
376
|
+
const { renderLabelItem } = chartOptions;
|
|
377
|
+
const labelAngle = startAngle + sliceAngle / 2;
|
|
378
|
+
const labelRadius = radius * 0.7;
|
|
379
|
+
const labelX = centerX + Math.cos(labelAngle) * labelRadius;
|
|
380
|
+
const labelY = centerY + Math.sin(labelAngle) * labelRadius;
|
|
381
|
+
if (renderLabelItem) {
|
|
382
|
+
const labelNode = renderLabelItem({ item: point, index });
|
|
383
|
+
if (labelNode) {
|
|
384
|
+
labelNode.processInitialChildren();
|
|
385
|
+
labelNode.node.calculateLayout(undefined, undefined, common_const.Style.Direction.LTR);
|
|
386
|
+
const layout = labelNode.node.getComputedLayout();
|
|
387
|
+
labelNode.render(ctx, labelX - layout.width / 2, labelY - layout.height / 2);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
text_canvas_util.TextNode.renderSimpleText(ctx, point.label, labelX, labelY, {
|
|
392
|
+
color: chartOptions.labelColor,
|
|
393
|
+
fontSize: chartOptions.labelFontSize,
|
|
394
|
+
fontFamily: this.props.fontFamily,
|
|
395
|
+
textAlign: 'center',
|
|
396
|
+
textBaseline: 'middle',
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
currentAngle = endAngle;
|
|
401
|
+
});
|
|
402
|
+
if (chartOptions?.showLegend) {
|
|
403
|
+
this.renderLegend(ctx, x + legendLayout.x, y + legendLayout.y, legendLayout.width, legendLayout.height);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
renderDoughnutChart(ctx, x, y, width, height) {
|
|
407
|
+
if (this.chartType !== 'doughnut')
|
|
408
|
+
return;
|
|
409
|
+
const data = this.chartData;
|
|
410
|
+
const chartOptions = this.chartOptions;
|
|
411
|
+
const legendLayout = this.getLegendLayout(ctx, width, height);
|
|
412
|
+
const chartX = x + legendLayout.chartX;
|
|
413
|
+
const chartY = y + legendLayout.chartY;
|
|
414
|
+
const chartWidth = legendLayout.chartWidth;
|
|
415
|
+
const chartHeight = legendLayout.chartHeight;
|
|
416
|
+
const centerX = chartX + chartWidth / 2;
|
|
417
|
+
const centerY = chartY + chartHeight / 2;
|
|
418
|
+
const outerRadius = Math.min(chartWidth, chartHeight) / 2 - 10;
|
|
419
|
+
const innerRadius = outerRadius * (chartOptions?.innerRadius ?? 0.6);
|
|
420
|
+
const total = data.reduce((sum, point) => sum + point.value, 0);
|
|
421
|
+
let currentAngle = -Math.PI / 2;
|
|
422
|
+
data.forEach((point, index) => {
|
|
423
|
+
const sliceAngle = (point.value / total) * Math.PI * 2;
|
|
424
|
+
const startAngle = currentAngle;
|
|
425
|
+
const endAngle = currentAngle + sliceAngle;
|
|
426
|
+
ctx.fillStyle = point.color || this.generateColor(index);
|
|
427
|
+
ctx.beginPath();
|
|
428
|
+
ctx.arc(centerX, centerY, outerRadius, startAngle, endAngle);
|
|
429
|
+
ctx.arc(centerX, centerY, innerRadius, endAngle, startAngle, true);
|
|
430
|
+
ctx.closePath();
|
|
431
|
+
ctx.fill();
|
|
432
|
+
ctx.strokeStyle = '#fff';
|
|
433
|
+
ctx.lineWidth = 2;
|
|
434
|
+
ctx.stroke();
|
|
435
|
+
// Render labels
|
|
436
|
+
if (chartOptions?.showLabels) {
|
|
437
|
+
const { renderLabelItem } = chartOptions;
|
|
438
|
+
const labelAngle = startAngle + sliceAngle / 2;
|
|
439
|
+
const labelRadius = innerRadius + (outerRadius - innerRadius) / 2;
|
|
440
|
+
const labelX = centerX + Math.cos(labelAngle) * labelRadius;
|
|
441
|
+
const labelY = centerY + Math.sin(labelAngle) * labelRadius;
|
|
442
|
+
if (renderLabelItem) {
|
|
443
|
+
const labelNode = renderLabelItem({ item: point, index });
|
|
444
|
+
if (labelNode) {
|
|
445
|
+
labelNode.processInitialChildren();
|
|
446
|
+
labelNode.node.calculateLayout(undefined, undefined, common_const.Style.Direction.LTR);
|
|
447
|
+
const layout = labelNode.node.getComputedLayout();
|
|
448
|
+
labelNode.render(ctx, labelX - layout.width / 2, labelY - layout.height / 2);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
text_canvas_util.TextNode.renderSimpleText(ctx, point.label, labelX, labelY, {
|
|
453
|
+
color: chartOptions.labelColor,
|
|
454
|
+
fontSize: chartOptions.labelFontSize,
|
|
455
|
+
fontFamily: this.props.fontFamily,
|
|
456
|
+
textAlign: 'center',
|
|
457
|
+
textBaseline: 'middle',
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
currentAngle = endAngle;
|
|
462
|
+
});
|
|
463
|
+
if (chartOptions?.showLegend) {
|
|
464
|
+
this.renderLegend(ctx, x + legendLayout.x, y + legendLayout.y, legendLayout.width, legendLayout.height);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
renderLegend(ctx, x, y, width, height) {
|
|
468
|
+
const { renderLegendItem } = this.chartOptions;
|
|
469
|
+
if (renderLegendItem) {
|
|
470
|
+
let legendNodes;
|
|
471
|
+
if (this.chartType === 'bar' || this.chartType === 'line') {
|
|
472
|
+
const items = this.chartData.datasets;
|
|
473
|
+
const render = renderLegendItem;
|
|
474
|
+
legendNodes = items.map((item, index) => {
|
|
475
|
+
const color = item.color || this.generateColor(index);
|
|
476
|
+
return render({ item, index, color });
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const items = this.chartData;
|
|
481
|
+
const render = renderLegendItem;
|
|
482
|
+
legendNodes = items.map((item, index) => {
|
|
483
|
+
const color = item.color || this.generateColor(index);
|
|
484
|
+
return render({ item, index, color });
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const finalNodes = legendNodes.filter((node) => !!node);
|
|
488
|
+
if (finalNodes.length > 0) {
|
|
489
|
+
const legendContainer = layout_canvas_util.Row({
|
|
490
|
+
children: finalNodes,
|
|
491
|
+
width,
|
|
492
|
+
height,
|
|
493
|
+
justifyContent: common_const.Style.Justify.Center,
|
|
494
|
+
alignItems: common_const.Style.Align.Center,
|
|
495
|
+
flexWrap: common_const.Style.Wrap.Wrap,
|
|
496
|
+
gap: 10,
|
|
497
|
+
});
|
|
498
|
+
legendContainer.processInitialChildren();
|
|
499
|
+
legendContainer.node.calculateLayout(width, height, common_const.Style.Direction.LTR);
|
|
500
|
+
legendContainer.render(ctx, x, y);
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// Fallback to default rendering if renderLegendItem is not provided
|
|
505
|
+
const legendItems = 'datasets' in this.chartData
|
|
506
|
+
? this.chartData.datasets.map(d => ({ label: d.label, value: d.data.reduce((a, b) => a + b, 0) }))
|
|
507
|
+
: this.chartData;
|
|
508
|
+
const fontSize = this.chartOptions?.labelFontSize || 12;
|
|
509
|
+
ctx.font = `${fontSize}px ${this.props.fontFamily || 'sans-serif'}`;
|
|
510
|
+
const metrics = ctx.measureText('Mg');
|
|
511
|
+
const textHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
|
|
512
|
+
const itemHeight = Math.ceil(textHeight + 8);
|
|
513
|
+
const boxSize = Math.min(15, itemHeight - 2);
|
|
514
|
+
const position = this.chartOptions.legendPosition;
|
|
515
|
+
if (position === 'top' || position === 'bottom') {
|
|
516
|
+
const itemPadding = 20; // horizontal padding between items
|
|
517
|
+
const rows = [];
|
|
518
|
+
let currentRow = { items: [], width: 0 };
|
|
519
|
+
legendItems.forEach((point, index) => {
|
|
520
|
+
const color = ('datasets' in this.chartData ? this.chartData.datasets[index].color : point.color) || this.generateColor(index);
|
|
521
|
+
const label = 'datasets' in this.chartData ? point.label : `${point.label} (${point.value})`;
|
|
522
|
+
const labelWidth = ctx.measureText(label).width;
|
|
523
|
+
const itemWidth = boxSize + 5 + labelWidth;
|
|
524
|
+
if (currentRow.items.length > 0 && currentRow.width + itemPadding + itemWidth > width) {
|
|
525
|
+
rows.push(currentRow);
|
|
526
|
+
currentRow = { items: [], width: 0 };
|
|
527
|
+
}
|
|
528
|
+
currentRow.items.push({ label, color, width: itemWidth });
|
|
529
|
+
currentRow.width += itemWidth + (currentRow.items.length > 1 ? itemPadding : 0);
|
|
530
|
+
});
|
|
531
|
+
rows.push(currentRow);
|
|
532
|
+
let currentY = y + 5;
|
|
533
|
+
rows.forEach(row => {
|
|
534
|
+
let currentX = x + (width - row.width) / 2;
|
|
535
|
+
row.items.forEach(item => {
|
|
536
|
+
const boxY = currentY + (itemHeight - boxSize) / 2;
|
|
537
|
+
ctx.fillStyle = item.color;
|
|
538
|
+
ctx.fillRect(currentX, boxY, boxSize, boxSize);
|
|
539
|
+
text_canvas_util.TextNode.renderSimpleText(ctx, item.label, currentX + boxSize + 5, currentY + itemHeight / 2, {
|
|
540
|
+
color: this.chartOptions?.labelColor,
|
|
541
|
+
fontSize,
|
|
542
|
+
fontFamily: this.props.fontFamily,
|
|
543
|
+
textAlign: 'left',
|
|
544
|
+
textBaseline: 'middle',
|
|
545
|
+
});
|
|
546
|
+
currentX += item.width + itemPadding;
|
|
547
|
+
});
|
|
548
|
+
currentY += itemHeight;
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
// 'left' or 'right'
|
|
553
|
+
const totalHeight = legendItems.length * itemHeight;
|
|
554
|
+
const startY = y + (height - totalHeight) / 2;
|
|
555
|
+
legendItems.forEach((point, index) => {
|
|
556
|
+
const itemX = x + 10;
|
|
557
|
+
const itemY = startY + index * itemHeight;
|
|
558
|
+
const boxY = itemY + (itemHeight - boxSize) / 2;
|
|
559
|
+
ctx.fillStyle =
|
|
560
|
+
('datasets' in this.chartData ? this.chartData.datasets[index].color : point.color) || this.generateColor(index);
|
|
561
|
+
ctx.fillRect(itemX, boxY, boxSize, boxSize);
|
|
562
|
+
const label = 'datasets' in this.chartData ? point.label : `${point.label} (${point.value})`;
|
|
563
|
+
text_canvas_util.TextNode.renderSimpleText(ctx, label, itemX + boxSize + 5, itemY + itemHeight / 2, {
|
|
564
|
+
color: this.chartOptions?.labelColor,
|
|
565
|
+
fontSize,
|
|
566
|
+
fontFamily: this.props.fontFamily,
|
|
567
|
+
textAlign: 'left',
|
|
568
|
+
textBaseline: 'middle',
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
generateColor(index) {
|
|
574
|
+
const colors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#C9CBCF'];
|
|
575
|
+
return colors[index % colors.length];
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const Chart = (props) => new ChartNode(props);
|
|
579
|
+
|
|
580
|
+
exports.Chart = Chart;
|
|
581
|
+
exports.ChartNode = ChartNode;
|
|
582
|
+
//# sourceMappingURL=chart.canvas.util.js.map
|