@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.
@@ -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