apexify.js 5.1.0 → 5.2.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/CHANGELOG.md +263 -38
- package/README.md +248 -1109
- package/dist/cjs/Canvas/ApexPainter.d.ts +182 -204
- package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/cjs/Canvas/ApexPainter.js +482 -1286
- package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
- package/dist/cjs/Canvas/extended/CanvasCreator.d.ts +33 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.js +223 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/ChartCreator.d.ts +26 -0
- package/dist/cjs/Canvas/extended/ChartCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/ChartCreator.js +50 -0
- package/dist/cjs/Canvas/extended/ChartCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/GIFCreator.d.ts +43 -0
- package/dist/cjs/Canvas/extended/GIFCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/GIFCreator.js +157 -0
- package/dist/cjs/Canvas/extended/GIFCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/ImageCreator.d.ts +83 -0
- package/dist/cjs/Canvas/extended/ImageCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/ImageCreator.js +479 -0
- package/dist/cjs/Canvas/extended/ImageCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/TextCreator.d.ts +35 -0
- package/dist/cjs/Canvas/extended/TextCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/TextCreator.js +98 -0
- package/dist/cjs/Canvas/extended/TextCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/VideoCreator.d.ts +370 -0
- package/dist/cjs/Canvas/extended/VideoCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/VideoCreator.js +478 -0
- package/dist/cjs/Canvas/extended/VideoCreator.js.map +1 -0
- package/dist/cjs/Canvas/utils/Background/bg.d.ts +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.js +43 -7
- package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/barchart.d.ts +230 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.js +1891 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.js +368 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/index.d.ts +45 -0
- package/dist/cjs/Canvas/utils/Charts/index.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/index.js +17 -0
- package/dist/cjs/Canvas/utils/Charts/index.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.d.ts +216 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.js +1761 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.d.ts +167 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.js +794 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.js.map +1 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/batchOperations.js +3 -4
- package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/general functions.js +62 -33
- package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageStitching.js +3 -6
- package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageMasking.js +5 -12
- package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +4 -4
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.js +44 -9
- package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.js +48 -5
- package/dist/cjs/Canvas/utils/Texts/textProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts +489 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.js +1835 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.js.map +1 -0
- package/dist/cjs/Canvas/utils/errorUtils.d.ts +15 -0
- package/dist/cjs/Canvas/utils/errorUtils.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/errorUtils.js +26 -0
- package/dist/cjs/Canvas/utils/errorUtils.js.map +1 -0
- package/dist/cjs/Canvas/utils/types.d.ts +17 -178
- package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/types.js.map +1 -1
- package/dist/cjs/Canvas/utils/utils.d.ts +4 -3
- package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/utils.js +40 -6
- package/dist/cjs/Canvas/utils/utils.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -8
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -45
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/Canvas/ApexPainter.d.ts +182 -204
- package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/esm/Canvas/ApexPainter.js +482 -1286
- package/dist/esm/Canvas/ApexPainter.js.map +1 -1
- package/dist/esm/Canvas/extended/CanvasCreator.d.ts +33 -0
- package/dist/esm/Canvas/extended/CanvasCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/CanvasCreator.js +223 -0
- package/dist/esm/Canvas/extended/CanvasCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/ChartCreator.d.ts +26 -0
- package/dist/esm/Canvas/extended/ChartCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/ChartCreator.js +50 -0
- package/dist/esm/Canvas/extended/ChartCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/GIFCreator.d.ts +43 -0
- package/dist/esm/Canvas/extended/GIFCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/GIFCreator.js +157 -0
- package/dist/esm/Canvas/extended/GIFCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/ImageCreator.d.ts +83 -0
- package/dist/esm/Canvas/extended/ImageCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/ImageCreator.js +479 -0
- package/dist/esm/Canvas/extended/ImageCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/TextCreator.d.ts +35 -0
- package/dist/esm/Canvas/extended/TextCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/TextCreator.js +98 -0
- package/dist/esm/Canvas/extended/TextCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/VideoCreator.d.ts +370 -0
- package/dist/esm/Canvas/extended/VideoCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/VideoCreator.js +478 -0
- package/dist/esm/Canvas/extended/VideoCreator.js.map +1 -0
- package/dist/esm/Canvas/utils/Background/bg.d.ts +1 -1
- package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Background/bg.js +43 -7
- package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/barchart.d.ts +230 -0
- package/dist/esm/Canvas/utils/Charts/barchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/barchart.js +1891 -0
- package/dist/esm/Canvas/utils/Charts/barchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.js +368 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/index.d.ts +45 -0
- package/dist/esm/Canvas/utils/Charts/index.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/index.js +17 -0
- package/dist/esm/Canvas/utils/Charts/index.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/linechart.d.ts +216 -0
- package/dist/esm/Canvas/utils/Charts/linechart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/linechart.js +1761 -0
- package/dist/esm/Canvas/utils/Charts/linechart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/piechart.d.ts +167 -0
- package/dist/esm/Canvas/utils/Charts/piechart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/piechart.js +794 -0
- package/dist/esm/Canvas/utils/Charts/piechart.js.map +1 -0
- package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/batchOperations.js +3 -4
- package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -1
- package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/general functions.js +62 -33
- package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/imageStitching.js +3 -6
- package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageMasking.js +5 -12
- package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +4 -4
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.js +44 -9
- package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.d.ts +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.js +48 -5
- package/dist/esm/Canvas/utils/Texts/textProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts +489 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.js +1835 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.js.map +1 -0
- package/dist/esm/Canvas/utils/errorUtils.d.ts +15 -0
- package/dist/esm/Canvas/utils/errorUtils.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/errorUtils.js +26 -0
- package/dist/esm/Canvas/utils/errorUtils.js.map +1 -0
- package/dist/esm/Canvas/utils/types.d.ts +17 -178
- package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/types.js.map +1 -1
- package/dist/esm/Canvas/utils/utils.d.ts +4 -3
- package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/utils.js +40 -6
- package/dist/esm/Canvas/utils/utils.js.map +1 -1
- package/dist/esm/index.d.ts +1 -8
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +14 -45
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +118 -82
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts +0 -13
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +0 -1
- package/dist/cjs/Canvas/utils/Charts/charts.js +0 -466
- package/dist/cjs/Canvas/utils/Charts/charts.js.map +0 -1
- package/dist/esm/Canvas/utils/Charts/charts.d.ts +0 -13
- package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +0 -1
- package/dist/esm/Canvas/utils/Charts/charts.js +0 -466
- package/dist/esm/Canvas/utils/Charts/charts.js.map +0 -1
- package/lib/Canvas/ApexPainter.ts +0 -5414
- package/lib/Canvas/utils/Background/bg.ts +0 -285
- package/lib/Canvas/utils/Charts/charts.ts +0 -548
- package/lib/Canvas/utils/Custom/advancedLines.ts +0 -387
- package/lib/Canvas/utils/Custom/customLines.ts +0 -206
- package/lib/Canvas/utils/General/batchOperations.ts +0 -103
- package/lib/Canvas/utils/General/conversion.ts +0 -34
- package/lib/Canvas/utils/General/general functions.ts +0 -726
- package/lib/Canvas/utils/General/imageCompression.ts +0 -316
- package/lib/Canvas/utils/General/imageStitching.ts +0 -252
- package/lib/Canvas/utils/Image/imageEffects.ts +0 -175
- package/lib/Canvas/utils/Image/imageFilters.ts +0 -356
- package/lib/Canvas/utils/Image/imageMasking.ts +0 -335
- package/lib/Canvas/utils/Image/imageProperties.ts +0 -587
- package/lib/Canvas/utils/Image/professionalImageFilters.ts +0 -391
- package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +0 -229
- package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +0 -455
- package/lib/Canvas/utils/Shapes/shapes.ts +0 -528
- package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +0 -716
- package/lib/Canvas/utils/Texts/textPathRenderer.ts +0 -320
- package/lib/Canvas/utils/Texts/textProperties.ts +0 -231
- package/lib/Canvas/utils/types.ts +0 -983
- package/lib/Canvas/utils/utils.ts +0 -135
- package/lib/index.ts +0 -81
- package/lib/utils.ts +0 -5
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPieChart = createPieChart;
|
|
4
|
+
const canvas_1 = require("@napi-rs/canvas");
|
|
5
|
+
const imageProperties_1 = require("../Image/imageProperties");
|
|
6
|
+
/**
|
|
7
|
+
* Helper function to render enhanced text with custom fonts, gradients, shadows, strokes
|
|
8
|
+
*/
|
|
9
|
+
async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textGradient) {
|
|
10
|
+
ctx.save();
|
|
11
|
+
// Preserve text alignment settings
|
|
12
|
+
const savedTextAlign = ctx.textAlign;
|
|
13
|
+
const savedTextBaseline = ctx.textBaseline;
|
|
14
|
+
const effectiveFontSize = fontSize || style?.fontSize || 16;
|
|
15
|
+
const fontFamily = style?.fontFamily || style?.fontName || 'Arial';
|
|
16
|
+
let fontString = '';
|
|
17
|
+
if (style?.bold)
|
|
18
|
+
fontString += 'bold ';
|
|
19
|
+
if (style?.italic)
|
|
20
|
+
fontString += 'italic ';
|
|
21
|
+
fontString += `${effectiveFontSize}px "${fontFamily}"`;
|
|
22
|
+
ctx.font = fontString;
|
|
23
|
+
// Restore text alignment to ensure correct positioning
|
|
24
|
+
ctx.textAlign = savedTextAlign;
|
|
25
|
+
ctx.textBaseline = savedTextBaseline;
|
|
26
|
+
// Register custom font if provided
|
|
27
|
+
if (style?.fontPath && style?.fontName) {
|
|
28
|
+
try {
|
|
29
|
+
const { GlobalFonts } = await import('@napi-rs/canvas');
|
|
30
|
+
const path = await import('path');
|
|
31
|
+
const fullPath = path.join(process.cwd(), style.fontPath);
|
|
32
|
+
GlobalFonts.registerFromPath(fullPath, style.fontName);
|
|
33
|
+
ctx.font = fontString.replace(`"${fontFamily}"`, `"${style.fontName}"`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.warn(`Failed to register font: ${style.fontPath}`, error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Apply shadow
|
|
40
|
+
if (style?.shadow) {
|
|
41
|
+
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.5)';
|
|
42
|
+
ctx.shadowOffsetX = style.shadow.offsetX || 2;
|
|
43
|
+
ctx.shadowOffsetY = style.shadow.offsetY || 2;
|
|
44
|
+
ctx.shadowBlur = style.shadow.blur || 4;
|
|
45
|
+
if (style.shadow.opacity !== undefined) {
|
|
46
|
+
ctx.globalAlpha = style.shadow.opacity;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Set fill style (gradient or color)
|
|
50
|
+
if (textGradient) {
|
|
51
|
+
const metrics = ctx.measureText(text);
|
|
52
|
+
ctx.fillStyle = (0, imageProperties_1.createGradientFill)(ctx, textGradient, {
|
|
53
|
+
x, y, w: metrics.width, h: effectiveFontSize
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else if (color) {
|
|
57
|
+
ctx.fillStyle = color;
|
|
58
|
+
}
|
|
59
|
+
// Draw text
|
|
60
|
+
ctx.fillText(text, x, y);
|
|
61
|
+
// Apply stroke
|
|
62
|
+
if (style?.stroke) {
|
|
63
|
+
ctx.strokeStyle = style.stroke.color || '#000000';
|
|
64
|
+
ctx.lineWidth = style.stroke.width || 1;
|
|
65
|
+
if (style.stroke.gradient) {
|
|
66
|
+
const metrics = ctx.measureText(text);
|
|
67
|
+
ctx.strokeStyle = (0, imageProperties_1.createGradientFill)(ctx, style.stroke.gradient, {
|
|
68
|
+
x, y, w: metrics.width, h: effectiveFontSize
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
ctx.strokeText(text, x, y);
|
|
72
|
+
}
|
|
73
|
+
// Reset shadow and alpha
|
|
74
|
+
ctx.shadowColor = 'transparent';
|
|
75
|
+
ctx.shadowOffsetX = 0;
|
|
76
|
+
ctx.shadowOffsetY = 0;
|
|
77
|
+
ctx.shadowBlur = 0;
|
|
78
|
+
ctx.globalAlpha = 1;
|
|
79
|
+
ctx.restore();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Helper function to fill a shape with gradient or color
|
|
83
|
+
*/
|
|
84
|
+
function fillWithGradientOrColor(ctx, gradient, color, defaultColor = '#000000', rect) {
|
|
85
|
+
if (gradient && rect) {
|
|
86
|
+
ctx.fillStyle = (0, imageProperties_1.createGradientFill)(ctx, gradient, rect);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
ctx.fillStyle = color || defaultColor;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Wraps text to fit within a maximum width
|
|
94
|
+
*/
|
|
95
|
+
function wrapText(ctx, text, maxWidth) {
|
|
96
|
+
const words = text.split(' ');
|
|
97
|
+
const lines = [];
|
|
98
|
+
let currentLine = words[0];
|
|
99
|
+
for (let i = 1; i < words.length; i++) {
|
|
100
|
+
const word = words[i];
|
|
101
|
+
const width = ctx.measureText(currentLine + ' ' + word).width;
|
|
102
|
+
if (width < maxWidth) {
|
|
103
|
+
currentLine += ' ' + word;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
lines.push(currentLine);
|
|
107
|
+
currentLine = word;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
lines.push(currentLine);
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calculates dimensions for standard legend with text wrapping
|
|
115
|
+
*/
|
|
116
|
+
function calculateStandardLegendDimensions(entries, fontSize, spacing, padding, maxWidth) {
|
|
117
|
+
// Create a temporary canvas to measure text
|
|
118
|
+
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
119
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
120
|
+
tempCtx.font = `${fontSize}px Arial`;
|
|
121
|
+
// Box size scales with font size for better proportions
|
|
122
|
+
const boxSize = Math.max(18, fontSize * 1.2);
|
|
123
|
+
const textSpacing = 12;
|
|
124
|
+
const effectiveMaxWidth = maxWidth ? maxWidth - padding * 2 - boxSize - textSpacing : undefined;
|
|
125
|
+
let maxEntryWidth = 0;
|
|
126
|
+
const entryHeights = [];
|
|
127
|
+
entries.forEach(entry => {
|
|
128
|
+
let textWidth;
|
|
129
|
+
let textHeight;
|
|
130
|
+
if (effectiveMaxWidth) {
|
|
131
|
+
const wrappedLines = wrapText(tempCtx, entry.label, effectiveMaxWidth);
|
|
132
|
+
textWidth = Math.max(...wrappedLines.map(line => tempCtx.measureText(line).width));
|
|
133
|
+
textHeight = wrappedLines.length * fontSize * 1.2; // Line height multiplier
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
textWidth = tempCtx.measureText(entry.label).width;
|
|
137
|
+
textHeight = fontSize;
|
|
138
|
+
}
|
|
139
|
+
const entryWidth = boxSize + textSpacing + textWidth;
|
|
140
|
+
maxEntryWidth = Math.max(maxEntryWidth, entryWidth);
|
|
141
|
+
entryHeights.push(Math.max(boxSize, textHeight));
|
|
142
|
+
});
|
|
143
|
+
const width = maxWidth ? maxWidth : maxEntryWidth + padding * 2;
|
|
144
|
+
const height = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? spacing : 0), 0) + padding * 2;
|
|
145
|
+
return { width, height, entryHeights };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Draws standard legend (Type 1)
|
|
149
|
+
*/
|
|
150
|
+
async function drawStandardLegend(ctx, entries, config, canvasWidth, canvasHeight, padding, chartArea, legendSpacing, titleHeight) {
|
|
151
|
+
if (!config.show || !entries || entries.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
ctx.save();
|
|
154
|
+
const fontSize = config.fontSize ?? 16; // Increased default from 12 to 16
|
|
155
|
+
const spacing = config.spacing ?? 18; // Increased default spacing
|
|
156
|
+
const paddingBox = config.padding ?? 10; // Increased default padding
|
|
157
|
+
const backgroundColor = config.backgroundColor ?? 'rgba(255, 255, 255, 0.9)';
|
|
158
|
+
const borderColor = config.borderColor ?? '#000000';
|
|
159
|
+
const textColor = config.textColor ?? '#000000';
|
|
160
|
+
const maxWidth = config.maxWidth;
|
|
161
|
+
const wrapTextEnabled = config.wrapText !== false;
|
|
162
|
+
ctx.font = `${fontSize}px Arial`;
|
|
163
|
+
// Calculate dimensions
|
|
164
|
+
const { width, height, entryHeights } = calculateStandardLegendDimensions(entries, fontSize, spacing, paddingBox, maxWidth);
|
|
165
|
+
// Determine position - position relative to chart area if provided, otherwise use canvas edges
|
|
166
|
+
let legendX, legendY;
|
|
167
|
+
const position = config.position ?? 'right';
|
|
168
|
+
const gap = legendSpacing ?? 20;
|
|
169
|
+
if (chartArea) {
|
|
170
|
+
// Position relative to chart area for better spacing
|
|
171
|
+
switch (position) {
|
|
172
|
+
case 'top':
|
|
173
|
+
legendX = (canvasWidth - width) / 2;
|
|
174
|
+
// Position legend below title if title exists
|
|
175
|
+
legendY = padding.top + (titleHeight ?? 0);
|
|
176
|
+
break;
|
|
177
|
+
case 'bottom':
|
|
178
|
+
legendX = (canvasWidth - width) / 2;
|
|
179
|
+
legendY = canvasHeight - padding.bottom - height;
|
|
180
|
+
break;
|
|
181
|
+
case 'left':
|
|
182
|
+
legendX = chartArea.left - width - gap;
|
|
183
|
+
legendY = (canvasHeight - height) / 2;
|
|
184
|
+
break;
|
|
185
|
+
case 'right':
|
|
186
|
+
legendX = chartArea.right + gap;
|
|
187
|
+
legendY = (canvasHeight - height) / 2;
|
|
188
|
+
break;
|
|
189
|
+
default:
|
|
190
|
+
legendX = chartArea.right + gap;
|
|
191
|
+
legendY = (canvasHeight - height) / 2;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// Fallback to original positioning
|
|
196
|
+
switch (position) {
|
|
197
|
+
case 'top':
|
|
198
|
+
legendX = (canvasWidth - width) / 2;
|
|
199
|
+
legendY = padding.top;
|
|
200
|
+
break;
|
|
201
|
+
case 'bottom':
|
|
202
|
+
legendX = (canvasWidth - width) / 2;
|
|
203
|
+
legendY = canvasHeight - padding.bottom - height;
|
|
204
|
+
break;
|
|
205
|
+
case 'left':
|
|
206
|
+
legendX = padding.left;
|
|
207
|
+
legendY = (canvasHeight - height) / 2;
|
|
208
|
+
break;
|
|
209
|
+
case 'right':
|
|
210
|
+
legendX = canvasWidth - padding.right - width;
|
|
211
|
+
legendY = (canvasHeight - height) / 2;
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
legendX = canvasWidth - padding.right - width;
|
|
215
|
+
legendY = (canvasHeight - height) / 2;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Draw legend background with gradient or color
|
|
219
|
+
if (config.backgroundGradient) {
|
|
220
|
+
fillWithGradientOrColor(ctx, config.backgroundGradient, backgroundColor, backgroundColor, {
|
|
221
|
+
x: legendX, y: legendY, w: width, h: height
|
|
222
|
+
});
|
|
223
|
+
ctx.fillRect(legendX, legendY, width, height);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
ctx.fillStyle = backgroundColor;
|
|
227
|
+
ctx.fillRect(legendX, legendY, width, height);
|
|
228
|
+
}
|
|
229
|
+
// Draw legend border
|
|
230
|
+
ctx.strokeStyle = borderColor;
|
|
231
|
+
ctx.lineWidth = 1;
|
|
232
|
+
ctx.strokeRect(legendX, legendY, width, height);
|
|
233
|
+
// Draw legend entries
|
|
234
|
+
ctx.textAlign = 'left';
|
|
235
|
+
ctx.textBaseline = 'middle';
|
|
236
|
+
// Box size scales with font size for better proportions
|
|
237
|
+
const boxSize = Math.max(18, fontSize * 1.2);
|
|
238
|
+
const textSpacing = 12;
|
|
239
|
+
const effectiveMaxWidth = maxWidth ? maxWidth - paddingBox * 2 - boxSize - textSpacing : undefined;
|
|
240
|
+
let currentY = legendY + paddingBox;
|
|
241
|
+
for (let index = 0; index < entries.length; index++) {
|
|
242
|
+
const entry = entries[index];
|
|
243
|
+
const entryHeight = entryHeights[index];
|
|
244
|
+
const centerY = currentY + entryHeight / 2;
|
|
245
|
+
// Draw color box with gradient or color
|
|
246
|
+
if (entry.gradient) {
|
|
247
|
+
fillWithGradientOrColor(ctx, entry.gradient, entry.color, '#000000', {
|
|
248
|
+
x: legendX + paddingBox, y: centerY - boxSize / 2, w: boxSize, h: boxSize
|
|
249
|
+
});
|
|
250
|
+
ctx.fillRect(legendX + paddingBox, centerY - boxSize / 2, boxSize, boxSize);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
ctx.fillStyle = entry.color || '#000000';
|
|
254
|
+
ctx.fillRect(legendX + paddingBox, centerY - boxSize / 2, boxSize, boxSize);
|
|
255
|
+
}
|
|
256
|
+
// Draw box border
|
|
257
|
+
ctx.strokeStyle = borderColor;
|
|
258
|
+
ctx.lineWidth = 1;
|
|
259
|
+
ctx.strokeRect(legendX + paddingBox, centerY - boxSize / 2, boxSize, boxSize);
|
|
260
|
+
// Draw label with enhanced styling
|
|
261
|
+
const textX = legendX + paddingBox + boxSize + textSpacing;
|
|
262
|
+
const labelTextColor = config.textGradient ? undefined : (config.textColor || textColor);
|
|
263
|
+
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
264
|
+
const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
|
|
265
|
+
const lineHeight = fontSize * 1.2;
|
|
266
|
+
const startY = centerY - (wrappedLines.length - 1) * lineHeight / 2;
|
|
267
|
+
for (let lineIndex = 0; lineIndex < wrappedLines.length; lineIndex++) {
|
|
268
|
+
await renderEnhancedText(ctx, wrappedLines[lineIndex], textX, startY + lineIndex * lineHeight, config.textStyle, fontSize, labelTextColor, config.textGradient);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
await renderEnhancedText(ctx, entry.label, textX, centerY, config.textStyle, fontSize, labelTextColor, config.textGradient);
|
|
273
|
+
}
|
|
274
|
+
currentY += entryHeight + spacing;
|
|
275
|
+
}
|
|
276
|
+
ctx.restore();
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Draws connected legend (Type 2) - labels with lines connecting to pie slices
|
|
280
|
+
*/
|
|
281
|
+
function drawConnectedLegend(ctx, entries, sliceAngles, centerX, centerY, radius, innerRadius, config) {
|
|
282
|
+
if (!config.show || !entries || entries.length === 0)
|
|
283
|
+
return;
|
|
284
|
+
ctx.save();
|
|
285
|
+
const fontSize = config.fontSize ?? 12;
|
|
286
|
+
const padding = config.padding ?? 5;
|
|
287
|
+
const backgroundColor = config.backgroundColor ?? 'rgba(255, 255, 255, 0.9)';
|
|
288
|
+
const borderColor = config.borderColor ?? '#000000';
|
|
289
|
+
const textColor = config.textColor ?? '#000000';
|
|
290
|
+
const lineColor = config.lineColor ?? '#000000';
|
|
291
|
+
const lineWidth = config.lineWidth ?? 1;
|
|
292
|
+
const maxWidth = config.maxWidth ?? 150;
|
|
293
|
+
const wrapTextEnabled = config.wrapText !== false;
|
|
294
|
+
ctx.font = `${fontSize}px Arial`;
|
|
295
|
+
// Calculate midpoint angles for each slice
|
|
296
|
+
const midAngles = sliceAngles.map(slice => (slice.startAngle + slice.endAngle) / 2);
|
|
297
|
+
const labelPositions = [];
|
|
298
|
+
entries.forEach((entry, index) => {
|
|
299
|
+
if (index >= midAngles.length)
|
|
300
|
+
return;
|
|
301
|
+
const angle = midAngles[index];
|
|
302
|
+
const labelRadius = radius + 20; // Distance from pie edge to label
|
|
303
|
+
const labelX = centerX + Math.cos(angle) * labelRadius;
|
|
304
|
+
const labelY = centerY + Math.sin(angle) * labelRadius;
|
|
305
|
+
// Determine label position (left or right of pie)
|
|
306
|
+
const isLeftSide = Math.cos(angle) < 0;
|
|
307
|
+
// Calculate text dimensions with wrapping
|
|
308
|
+
const textMaxWidth = maxWidth - padding * 2;
|
|
309
|
+
let wrappedLines;
|
|
310
|
+
let textHeight;
|
|
311
|
+
let textWidth;
|
|
312
|
+
if (wrapTextEnabled) {
|
|
313
|
+
wrappedLines = wrapText(ctx, entry.label, textMaxWidth);
|
|
314
|
+
textWidth = Math.max(...wrappedLines.map(line => ctx.measureText(line).width));
|
|
315
|
+
textHeight = wrappedLines.length * fontSize * 1.2;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
wrappedLines = [entry.label];
|
|
319
|
+
textWidth = ctx.measureText(entry.label).width;
|
|
320
|
+
textHeight = fontSize;
|
|
321
|
+
}
|
|
322
|
+
const boxWidth = Math.min(maxWidth, textWidth + padding * 2);
|
|
323
|
+
const boxHeight = textHeight + padding * 2;
|
|
324
|
+
let labelBoxX = isLeftSide ? labelX - boxWidth : labelX;
|
|
325
|
+
let labelBoxY = labelY;
|
|
326
|
+
// Check for overlaps with previous labels and adjust position
|
|
327
|
+
const minSpacing = 5;
|
|
328
|
+
for (const prevPos of labelPositions) {
|
|
329
|
+
const overlapX = !(labelBoxX + boxWidth < prevPos.boxX || labelBoxX > prevPos.boxX + prevPos.boxWidth);
|
|
330
|
+
const overlapY = !(labelBoxY + boxHeight / 2 < prevPos.boxY - prevPos.boxHeight / 2 || labelBoxY - boxHeight / 2 > prevPos.boxY + prevPos.boxHeight / 2);
|
|
331
|
+
if (overlapX && overlapY) {
|
|
332
|
+
// Adjust vertically to avoid overlap
|
|
333
|
+
if (labelBoxY < prevPos.boxY) {
|
|
334
|
+
labelBoxY = prevPos.boxY - prevPos.boxHeight / 2 - boxHeight / 2 - minSpacing;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
labelBoxY = prevPos.boxY + prevPos.boxHeight / 2 + boxHeight / 2 + minSpacing;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
labelPositions.push({
|
|
342
|
+
angle,
|
|
343
|
+
x: labelX,
|
|
344
|
+
y: labelY,
|
|
345
|
+
boxX: labelBoxX,
|
|
346
|
+
boxY: labelBoxY,
|
|
347
|
+
boxWidth,
|
|
348
|
+
boxHeight,
|
|
349
|
+
isLeftSide
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
// Draw labels
|
|
353
|
+
entries.forEach((entry, index) => {
|
|
354
|
+
if (index >= labelPositions.length)
|
|
355
|
+
return;
|
|
356
|
+
const pos = labelPositions[index];
|
|
357
|
+
const angle = pos.angle;
|
|
358
|
+
// Calculate text dimensions with wrapping
|
|
359
|
+
const textMaxWidth = maxWidth - padding * 2;
|
|
360
|
+
let wrappedLines;
|
|
361
|
+
if (wrapTextEnabled) {
|
|
362
|
+
wrappedLines = wrapText(ctx, entry.label, textMaxWidth);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
wrappedLines = [entry.label];
|
|
366
|
+
}
|
|
367
|
+
// Draw connecting line
|
|
368
|
+
ctx.strokeStyle = lineColor;
|
|
369
|
+
ctx.lineWidth = lineWidth;
|
|
370
|
+
ctx.beginPath();
|
|
371
|
+
// Calculate line start point (on pie edge)
|
|
372
|
+
const lineStartX = centerX + Math.cos(angle) * radius;
|
|
373
|
+
const lineStartY = centerY + Math.sin(angle) * radius;
|
|
374
|
+
// Calculate line end point (at label box edge)
|
|
375
|
+
const lineEndX = pos.isLeftSide ? pos.boxX + pos.boxWidth : pos.boxX;
|
|
376
|
+
const lineEndY = pos.boxY;
|
|
377
|
+
ctx.moveTo(lineStartX, lineStartY);
|
|
378
|
+
ctx.lineTo(lineEndX, lineEndY);
|
|
379
|
+
ctx.stroke();
|
|
380
|
+
// Draw label box background
|
|
381
|
+
ctx.fillStyle = backgroundColor;
|
|
382
|
+
ctx.fillRect(pos.boxX, pos.boxY - pos.boxHeight / 2, pos.boxWidth, pos.boxHeight);
|
|
383
|
+
// Draw label box border
|
|
384
|
+
ctx.strokeStyle = borderColor;
|
|
385
|
+
ctx.lineWidth = 1;
|
|
386
|
+
ctx.strokeRect(pos.boxX, pos.boxY - pos.boxHeight / 2, pos.boxWidth, pos.boxHeight);
|
|
387
|
+
// Draw label text
|
|
388
|
+
ctx.fillStyle = textColor;
|
|
389
|
+
ctx.textAlign = pos.isLeftSide ? 'right' : 'left';
|
|
390
|
+
ctx.textBaseline = 'middle';
|
|
391
|
+
if (wrapTextEnabled && wrappedLines.length > 1) {
|
|
392
|
+
const lineHeight = fontSize * 1.2;
|
|
393
|
+
const startY = pos.boxY - (wrappedLines.length - 1) * lineHeight / 2;
|
|
394
|
+
const textX = pos.isLeftSide ? pos.boxX + pos.boxWidth - padding : pos.boxX + padding;
|
|
395
|
+
wrappedLines.forEach((line, lineIndex) => {
|
|
396
|
+
ctx.fillText(line, textX, startY + lineIndex * lineHeight);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const textX = pos.isLeftSide ? pos.boxX + pos.boxWidth - padding : pos.boxX + padding;
|
|
401
|
+
ctx.fillText(entry.label, textX, pos.boxY);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
ctx.restore();
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Creates a pie or donut chart
|
|
408
|
+
*/
|
|
409
|
+
async function createPieChart(data, options = {}) {
|
|
410
|
+
// Extract options with defaults
|
|
411
|
+
const width = options.dimensions?.width ?? 800;
|
|
412
|
+
const height = options.dimensions?.height ?? 600;
|
|
413
|
+
const padding = options.dimensions?.padding || {};
|
|
414
|
+
const paddingTop = padding.top ?? 60;
|
|
415
|
+
const paddingRight = padding.right ?? 80;
|
|
416
|
+
const paddingBottom = padding.bottom ?? 80;
|
|
417
|
+
const paddingLeft = padding.left ?? 80;
|
|
418
|
+
const backgroundColor = options.appearance?.backgroundColor ?? '#FFFFFF';
|
|
419
|
+
const backgroundGradient = options.appearance?.backgroundGradient;
|
|
420
|
+
const backgroundImage = options.appearance?.backgroundImage;
|
|
421
|
+
const chartType = options.type ?? 'pie';
|
|
422
|
+
const donutInnerRadiusRatio = options.donutInnerRadius ?? 0.6;
|
|
423
|
+
const chartTitle = options.labels?.title?.text;
|
|
424
|
+
const chartTitleFontSize = options.labels?.title?.fontSize ?? 24; // Increased default like Chart.js
|
|
425
|
+
const chartTitleColor = options.labels?.title?.color ?? '#000000';
|
|
426
|
+
// Calculate title height to account for it in layout
|
|
427
|
+
const titleHeight = chartTitle ? chartTitleFontSize + 30 : 0; // Font size + spacing
|
|
428
|
+
const showValues = options.labels?.showValues ?? true;
|
|
429
|
+
const showLabels = options.labels?.showLabels ?? false;
|
|
430
|
+
const valueFormatter = options.labels?.valueFormat;
|
|
431
|
+
const standardLegendConfig = options.legends?.standard;
|
|
432
|
+
const connectedLegendConfig = options.legends?.connected;
|
|
433
|
+
const globalSliceOpacity = options.slices?.opacity;
|
|
434
|
+
const globalSliceShadow = options.slices?.shadow;
|
|
435
|
+
const globalSliceStroke = options.slices?.stroke;
|
|
436
|
+
// Calculate total value
|
|
437
|
+
const total = data.reduce((sum, slice) => sum + slice.value, 0);
|
|
438
|
+
if (total === 0) {
|
|
439
|
+
throw new Error('Pie Chart Error: Total value of all slices must be greater than 0');
|
|
440
|
+
}
|
|
441
|
+
// Create temporary canvas to calculate legend dimensions
|
|
442
|
+
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
443
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
444
|
+
// Calculate legend dimensions BEFORE drawing to reserve proper space
|
|
445
|
+
let standardLegendWidth = 0;
|
|
446
|
+
let standardLegendHeight = 0;
|
|
447
|
+
let connectedLegendSpace = { left: 0, right: 0, top: 0, bottom: 0 };
|
|
448
|
+
const legendEntries = data.map((slice, index) => {
|
|
449
|
+
const defaultColors = ['#4A90E2', '#50C878', '#FF6B6B', '#FFA500', '#9B59B6', '#F39C12', '#1ABC9C', '#E74C3C'];
|
|
450
|
+
return {
|
|
451
|
+
color: slice.color || defaultColors[index % defaultColors.length],
|
|
452
|
+
label: slice.label
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
// Calculate standard legend dimensions
|
|
456
|
+
if (standardLegendConfig?.show && legendEntries.length > 0) {
|
|
457
|
+
const legendFontSize = standardLegendConfig.fontSize ?? 16; // Increased default
|
|
458
|
+
const legendSpacing = standardLegendConfig.spacing ?? 18; // Increased default
|
|
459
|
+
const legendPadding = standardLegendConfig.padding ?? 10; // Increased default
|
|
460
|
+
const legendMaxWidth = standardLegendConfig.maxWidth;
|
|
461
|
+
const legendWrapText = standardLegendConfig.wrapText !== false;
|
|
462
|
+
const { width: legWidth, height: legHeight } = calculateStandardLegendDimensions(legendEntries, legendFontSize, legendSpacing, legendPadding, legendMaxWidth);
|
|
463
|
+
standardLegendWidth = legWidth;
|
|
464
|
+
standardLegendHeight = legHeight;
|
|
465
|
+
}
|
|
466
|
+
// Calculate connected legend space needed (approximate based on max label width)
|
|
467
|
+
if (connectedLegendConfig?.show && legendEntries.length > 0) {
|
|
468
|
+
const connectedFontSize = connectedLegendConfig.fontSize ?? 12;
|
|
469
|
+
const connectedMaxWidth = connectedLegendConfig.maxWidth ?? 150;
|
|
470
|
+
const connectedPadding = connectedLegendConfig.padding ?? 5;
|
|
471
|
+
tempCtx.font = `${connectedFontSize}px Arial`;
|
|
472
|
+
const maxLabelWidth = Math.max(...legendEntries.map(e => tempCtx.measureText(e.label).width));
|
|
473
|
+
const boxWidth = Math.min(connectedMaxWidth, maxLabelWidth + connectedPadding * 2);
|
|
474
|
+
const boxHeight = connectedFontSize + connectedPadding * 2;
|
|
475
|
+
// Connected labels extend from pie edge, so we need space around the pie
|
|
476
|
+
// Estimate: labels can extend up to boxWidth from pie edge, plus some margin
|
|
477
|
+
connectedLegendSpace.left = boxWidth + 30;
|
|
478
|
+
connectedLegendSpace.right = boxWidth + 30;
|
|
479
|
+
connectedLegendSpace.top = boxHeight / 2 + 20;
|
|
480
|
+
connectedLegendSpace.bottom = boxHeight / 2 + 20;
|
|
481
|
+
}
|
|
482
|
+
// Calculate chart area - DO NOT shrink, add width/height for legends instead
|
|
483
|
+
const standardLegendPosition = standardLegendConfig?.position ?? 'right';
|
|
484
|
+
const legendGap = 20;
|
|
485
|
+
// Calculate how much extra space we need for legends
|
|
486
|
+
let extraWidth = 0;
|
|
487
|
+
let extraHeight = 0;
|
|
488
|
+
// Add extra width/height for standard legend instead of shrinking
|
|
489
|
+
if (standardLegendConfig?.show) {
|
|
490
|
+
if (standardLegendPosition === 'left') {
|
|
491
|
+
extraWidth = standardLegendWidth + legendGap;
|
|
492
|
+
}
|
|
493
|
+
else if (standardLegendPosition === 'right') {
|
|
494
|
+
extraWidth = standardLegendWidth + legendGap;
|
|
495
|
+
}
|
|
496
|
+
else if (standardLegendPosition === 'top') {
|
|
497
|
+
extraHeight = standardLegendHeight + legendGap;
|
|
498
|
+
}
|
|
499
|
+
else if (standardLegendPosition === 'bottom') {
|
|
500
|
+
extraHeight = standardLegendHeight + legendGap;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Add extra space for connected legend
|
|
504
|
+
extraWidth = Math.max(extraWidth, connectedLegendSpace.left, connectedLegendSpace.right);
|
|
505
|
+
extraHeight = Math.max(extraHeight, connectedLegendSpace.top, connectedLegendSpace.bottom);
|
|
506
|
+
// Adjust canvas dimensions to accommodate legends
|
|
507
|
+
const adjustedWidth = width + extraWidth;
|
|
508
|
+
const adjustedHeight = height + extraHeight;
|
|
509
|
+
// Calculate chart area - reposition chart based on legend position
|
|
510
|
+
let chartAreaLeft = paddingLeft;
|
|
511
|
+
let chartAreaRight = width - paddingRight;
|
|
512
|
+
let chartAreaTop = paddingTop + titleHeight; // Account for title
|
|
513
|
+
let chartAreaBottom = height - paddingBottom;
|
|
514
|
+
// Reposition chart based on legend position
|
|
515
|
+
if (standardLegendConfig?.show) {
|
|
516
|
+
if (standardLegendPosition === 'left') {
|
|
517
|
+
// Legend left → Chart shifts right
|
|
518
|
+
chartAreaLeft = paddingLeft + standardLegendWidth + legendGap;
|
|
519
|
+
chartAreaRight = adjustedWidth - paddingRight;
|
|
520
|
+
}
|
|
521
|
+
else if (standardLegendPosition === 'right') {
|
|
522
|
+
// Legend right → Chart shifts left (stays in original position, legend goes to right)
|
|
523
|
+
chartAreaLeft = paddingLeft;
|
|
524
|
+
chartAreaRight = width - paddingRight; // Keep original right edge
|
|
525
|
+
}
|
|
526
|
+
else if (standardLegendPosition === 'top') {
|
|
527
|
+
// Legend top → Chart shifts down
|
|
528
|
+
chartAreaTop = paddingTop + titleHeight + standardLegendHeight + legendGap;
|
|
529
|
+
chartAreaBottom = adjustedHeight - paddingBottom;
|
|
530
|
+
}
|
|
531
|
+
else if (standardLegendPosition === 'bottom') {
|
|
532
|
+
// Legend bottom → Chart shifts up
|
|
533
|
+
chartAreaTop = paddingTop + titleHeight;
|
|
534
|
+
chartAreaBottom = height - paddingBottom; // Keep original bottom edge
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Create canvas with adjusted dimensions
|
|
538
|
+
const canvas = (0, canvas_1.createCanvas)(adjustedWidth, adjustedHeight);
|
|
539
|
+
const ctx = canvas.getContext('2d');
|
|
540
|
+
// Fill background with gradient, color, or image
|
|
541
|
+
if (backgroundImage) {
|
|
542
|
+
try {
|
|
543
|
+
const bgImage = await (0, canvas_1.loadImage)(backgroundImage);
|
|
544
|
+
ctx.drawImage(bgImage, 0, 0, adjustedWidth, adjustedHeight);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
console.warn(`Failed to load background image: ${backgroundImage}`, error);
|
|
548
|
+
// Fallback to gradient or color
|
|
549
|
+
if (backgroundGradient) {
|
|
550
|
+
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
|
|
551
|
+
x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
|
|
552
|
+
});
|
|
553
|
+
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
ctx.fillStyle = backgroundColor;
|
|
557
|
+
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
else if (backgroundGradient) {
|
|
562
|
+
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
|
|
563
|
+
x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
|
|
564
|
+
});
|
|
565
|
+
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
ctx.fillStyle = backgroundColor;
|
|
569
|
+
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
570
|
+
}
|
|
571
|
+
// Draw chart title with enhanced styling
|
|
572
|
+
if (chartTitle) {
|
|
573
|
+
const titleStyle = options.labels?.title?.textStyle;
|
|
574
|
+
const titleGradient = options.labels?.title?.gradient;
|
|
575
|
+
const titleY = paddingTop + 10;
|
|
576
|
+
await renderEnhancedText(ctx, chartTitle, adjustedWidth / 2, titleY, titleStyle, chartTitleFontSize, chartTitleColor, titleGradient);
|
|
577
|
+
// Set alignment for title
|
|
578
|
+
ctx.save();
|
|
579
|
+
ctx.textAlign = 'center';
|
|
580
|
+
ctx.textBaseline = 'top';
|
|
581
|
+
ctx.restore();
|
|
582
|
+
}
|
|
583
|
+
// Calculate pie center and radius
|
|
584
|
+
const chartAreaWidth = chartAreaRight - chartAreaLeft;
|
|
585
|
+
const chartAreaHeight = chartAreaBottom - chartAreaTop;
|
|
586
|
+
// Chart.js approach: maxRadius = min(width, height) / 2, then use 100% of that
|
|
587
|
+
const maxRadius = Math.max(Math.min(chartAreaWidth, chartAreaHeight) / 2, 0);
|
|
588
|
+
const radius = maxRadius; // Use 100% of available space (Chart.js default)
|
|
589
|
+
const centerX = chartAreaLeft + chartAreaWidth / 2;
|
|
590
|
+
const centerY = chartAreaTop + chartAreaHeight / 2;
|
|
591
|
+
const innerRadius = chartType === 'donut' ? radius * donutInnerRadiusRatio : 0;
|
|
592
|
+
// Calculate slice angles
|
|
593
|
+
let currentAngle = -Math.PI / 2; // Start at top
|
|
594
|
+
const sliceAngles = [];
|
|
595
|
+
data.forEach(slice => {
|
|
596
|
+
const sliceAngle = (slice.value / total) * Math.PI * 2;
|
|
597
|
+
sliceAngles.push({
|
|
598
|
+
startAngle: currentAngle,
|
|
599
|
+
endAngle: currentAngle + sliceAngle
|
|
600
|
+
});
|
|
601
|
+
currentAngle += sliceAngle;
|
|
602
|
+
});
|
|
603
|
+
// Draw pie slices
|
|
604
|
+
const defaultColors = ['#4A90E2', '#50C878', '#FF6B6B', '#FFA500', '#9B59B6', '#F39C12', '#1ABC9C', '#E74C3C'];
|
|
605
|
+
for (let index = 0; index < data.length; index++) {
|
|
606
|
+
const slice = data[index];
|
|
607
|
+
const defaultColor = slice.color || defaultColors[index % defaultColors.length];
|
|
608
|
+
const angles = sliceAngles[index];
|
|
609
|
+
// Draw slice with gradient, opacity, shadow, and stroke support
|
|
610
|
+
ctx.save();
|
|
611
|
+
// Apply opacity
|
|
612
|
+
const effectiveOpacity = slice.opacity ?? globalSliceOpacity;
|
|
613
|
+
if (effectiveOpacity !== undefined) {
|
|
614
|
+
ctx.globalAlpha = effectiveOpacity;
|
|
615
|
+
}
|
|
616
|
+
// Apply shadow (slice shadow takes precedence over global)
|
|
617
|
+
const effectiveShadow = slice.shadow || globalSliceShadow;
|
|
618
|
+
if (effectiveShadow) {
|
|
619
|
+
ctx.shadowColor = effectiveShadow.color || 'rgba(0,0,0,0.3)';
|
|
620
|
+
ctx.shadowOffsetX = effectiveShadow.offsetX ?? 2;
|
|
621
|
+
ctx.shadowOffsetY = effectiveShadow.offsetY ?? 2;
|
|
622
|
+
ctx.shadowBlur = effectiveShadow.blur ?? 4;
|
|
623
|
+
}
|
|
624
|
+
ctx.beginPath();
|
|
625
|
+
if (chartType === 'donut') {
|
|
626
|
+
// Draw donut slice: outer arc -> line to inner -> inner arc (reverse) -> line back
|
|
627
|
+
ctx.arc(centerX, centerY, radius, angles.startAngle, angles.endAngle);
|
|
628
|
+
ctx.lineTo(centerX + Math.cos(angles.endAngle) * innerRadius, centerY + Math.sin(angles.endAngle) * innerRadius);
|
|
629
|
+
ctx.arc(centerX, centerY, innerRadius, angles.endAngle, angles.startAngle, true);
|
|
630
|
+
ctx.closePath();
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
// Draw pie slice: center -> outer arc -> back to center
|
|
634
|
+
ctx.moveTo(centerX, centerY);
|
|
635
|
+
ctx.arc(centerX, centerY, radius, angles.startAngle, angles.endAngle);
|
|
636
|
+
ctx.closePath();
|
|
637
|
+
}
|
|
638
|
+
// Fill with gradient or color
|
|
639
|
+
if (slice.gradient) {
|
|
640
|
+
// Calculate bounding box for gradient
|
|
641
|
+
const midAngle = (angles.startAngle + angles.endAngle) / 2;
|
|
642
|
+
const gradientRect = {
|
|
643
|
+
x: centerX - radius,
|
|
644
|
+
y: centerY - radius,
|
|
645
|
+
w: radius * 2,
|
|
646
|
+
h: radius * 2
|
|
647
|
+
};
|
|
648
|
+
fillWithGradientOrColor(ctx, slice.gradient, defaultColor, defaultColor, gradientRect);
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
ctx.fillStyle = defaultColor;
|
|
652
|
+
}
|
|
653
|
+
ctx.fill();
|
|
654
|
+
// Reset shadow before stroke
|
|
655
|
+
if (effectiveShadow) {
|
|
656
|
+
ctx.shadowColor = 'transparent';
|
|
657
|
+
ctx.shadowOffsetX = 0;
|
|
658
|
+
ctx.shadowOffsetY = 0;
|
|
659
|
+
ctx.shadowBlur = 0;
|
|
660
|
+
}
|
|
661
|
+
// Draw slice border/stroke (slice stroke takes precedence over global)
|
|
662
|
+
const effectiveStroke = slice.stroke || globalSliceStroke;
|
|
663
|
+
if (effectiveStroke && effectiveStroke.width && effectiveStroke.width > 0) {
|
|
664
|
+
ctx.beginPath();
|
|
665
|
+
// Outer arc
|
|
666
|
+
ctx.arc(centerX, centerY, radius, angles.startAngle, angles.endAngle);
|
|
667
|
+
if (chartType === 'donut') {
|
|
668
|
+
// Inner arc (reverse direction for donut)
|
|
669
|
+
ctx.arc(centerX, centerY, innerRadius, angles.endAngle, angles.startAngle, true);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
// Line back to center for pie
|
|
673
|
+
ctx.lineTo(centerX, centerY);
|
|
674
|
+
}
|
|
675
|
+
ctx.closePath();
|
|
676
|
+
if (effectiveStroke.gradient) {
|
|
677
|
+
const gradientRect = {
|
|
678
|
+
x: centerX - radius,
|
|
679
|
+
y: centerY - radius,
|
|
680
|
+
w: radius * 2,
|
|
681
|
+
h: radius * 2
|
|
682
|
+
};
|
|
683
|
+
ctx.strokeStyle = (0, imageProperties_1.createGradientFill)(ctx, effectiveStroke.gradient, gradientRect);
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
ctx.strokeStyle = effectiveStroke.color || '#FFFFFF';
|
|
687
|
+
}
|
|
688
|
+
ctx.lineWidth = effectiveStroke.width;
|
|
689
|
+
ctx.lineJoin = 'bevel';
|
|
690
|
+
ctx.stroke();
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
// Default border if no stroke specified (backward compatibility)
|
|
694
|
+
ctx.beginPath();
|
|
695
|
+
// Outer arc
|
|
696
|
+
ctx.arc(centerX, centerY, radius, angles.startAngle, angles.endAngle);
|
|
697
|
+
if (chartType === 'donut') {
|
|
698
|
+
// Inner arc (reverse direction for donut)
|
|
699
|
+
ctx.arc(centerX, centerY, innerRadius, angles.endAngle, angles.startAngle, true);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
// Line back to center for pie
|
|
703
|
+
ctx.lineTo(centerX, centerY);
|
|
704
|
+
}
|
|
705
|
+
ctx.closePath();
|
|
706
|
+
ctx.strokeStyle = '#FFFFFF'; // Chart.js default: white borders
|
|
707
|
+
ctx.lineWidth = 2; // Chart.js default: 2px
|
|
708
|
+
ctx.lineJoin = 'bevel';
|
|
709
|
+
ctx.stroke();
|
|
710
|
+
}
|
|
711
|
+
ctx.restore();
|
|
712
|
+
// Draw labels on slice if enabled
|
|
713
|
+
const percentage = (slice.value / total) * 100;
|
|
714
|
+
const sliceAngle = angles.endAngle - angles.startAngle;
|
|
715
|
+
const midAngle = (angles.startAngle + angles.endAngle) / 2;
|
|
716
|
+
// Build label text lines
|
|
717
|
+
const labelLines = [];
|
|
718
|
+
// Add slice label if enabled
|
|
719
|
+
if ((showLabels || slice.showLabel) && (slice.showLabel !== false)) {
|
|
720
|
+
labelLines.push(slice.label);
|
|
721
|
+
}
|
|
722
|
+
// Add value label if enabled (default format: "value percentage%" or custom format)
|
|
723
|
+
if (showValues && (slice.showValue !== false)) {
|
|
724
|
+
const valueText = slice.valueLabel || (valueFormatter ? valueFormatter(slice.value, percentage) : `${slice.value} ${percentage.toFixed(1)}%`);
|
|
725
|
+
labelLines.push(valueText);
|
|
726
|
+
}
|
|
727
|
+
// Draw labels - for small slices, position outside the slice edge
|
|
728
|
+
if (labelLines.length > 0) {
|
|
729
|
+
ctx.save();
|
|
730
|
+
// Determine if slice is small (less than 5% or very narrow angle)
|
|
731
|
+
const isSmallSlice = percentage < 5 || sliceAngle < 0.15; // ~8.6 degrees
|
|
732
|
+
let labelRadius;
|
|
733
|
+
let fontSize;
|
|
734
|
+
if (isSmallSlice) {
|
|
735
|
+
// For small slices: position labels OUTSIDE the slice edge (like wheel of fortune)
|
|
736
|
+
// Position text just outside the outer edge of the pie
|
|
737
|
+
labelRadius = radius + 15; // Outside the pie edge with some spacing
|
|
738
|
+
fontSize = 12; // Smaller font for external labels
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
// For normal slices: position labels inside the slice
|
|
742
|
+
labelRadius = chartType === 'donut' ? (radius + innerRadius) / 2 : radius * 0.7;
|
|
743
|
+
fontSize = 14;
|
|
744
|
+
}
|
|
745
|
+
const labelX = centerX + Math.cos(midAngle) * labelRadius;
|
|
746
|
+
const labelY = centerY + Math.sin(midAngle) * labelRadius;
|
|
747
|
+
const lineHeight = fontSize;
|
|
748
|
+
// Rotate context to make text vertical (perpendicular to radius)
|
|
749
|
+
// Text is rotated vertically along the slice's angle direction
|
|
750
|
+
ctx.translate(labelX, labelY);
|
|
751
|
+
ctx.rotate(midAngle + Math.PI / 2); // Rotate 90 degrees to make text vertical
|
|
752
|
+
// Get label styling options
|
|
753
|
+
const sliceLabelStyle = options.labels?.sliceLabels?.textStyle;
|
|
754
|
+
const sliceLabelColor = options.labels?.sliceLabels?.color || '#000000';
|
|
755
|
+
const sliceLabelGradient = options.labels?.sliceLabels?.gradient;
|
|
756
|
+
const valueLabelStyle = options.labels?.valueLabels?.textStyle;
|
|
757
|
+
const valueLabelColor = options.labels?.valueLabels?.color || '#000000';
|
|
758
|
+
const valueLabelGradient = options.labels?.valueLabels?.gradient;
|
|
759
|
+
ctx.textAlign = 'center';
|
|
760
|
+
ctx.textBaseline = 'middle';
|
|
761
|
+
// Draw multiple lines if both label and value are shown
|
|
762
|
+
if (labelLines.length === 1) {
|
|
763
|
+
const isValueLabel = !showLabels || slice.showLabel === false;
|
|
764
|
+
const style = isValueLabel ? valueLabelStyle : sliceLabelStyle;
|
|
765
|
+
const color = isValueLabel ? valueLabelColor : sliceLabelColor;
|
|
766
|
+
const gradient = isValueLabel ? valueLabelGradient : sliceLabelGradient;
|
|
767
|
+
await renderEnhancedText(ctx, labelLines[0], 0, 0, style, fontSize, color, gradient);
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
const startY = -(labelLines.length - 1) * lineHeight / 2;
|
|
771
|
+
for (let lineIndex = 0; lineIndex < labelLines.length; lineIndex++) {
|
|
772
|
+
const line = labelLines[lineIndex];
|
|
773
|
+
const isValueLabel = lineIndex > 0 || (!showLabels || slice.showLabel === false);
|
|
774
|
+
const style = isValueLabel ? valueLabelStyle : sliceLabelStyle;
|
|
775
|
+
const color = isValueLabel ? valueLabelColor : sliceLabelColor;
|
|
776
|
+
const gradient = isValueLabel ? valueLabelGradient : sliceLabelGradient;
|
|
777
|
+
await renderEnhancedText(ctx, line, 0, startY + lineIndex * lineHeight, style, fontSize, color, gradient);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
ctx.restore();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
// Draw connected legend (Type 2) - must be drawn after slices but before standard legend
|
|
784
|
+
if (connectedLegendConfig) {
|
|
785
|
+
drawConnectedLegend(ctx, legendEntries, sliceAngles, centerX, centerY, radius, innerRadius, connectedLegendConfig);
|
|
786
|
+
}
|
|
787
|
+
// Draw standard legend (Type 1)
|
|
788
|
+
if (standardLegendConfig) {
|
|
789
|
+
// Pass title height so legend can position correctly when at top
|
|
790
|
+
await drawStandardLegend(ctx, legendEntries, standardLegendConfig, adjustedWidth, adjustedHeight, { top: paddingTop, right: paddingRight, bottom: paddingBottom, left: paddingLeft }, { left: chartAreaLeft, right: chartAreaRight, top: chartAreaTop, bottom: chartAreaBottom }, legendGap, titleHeight);
|
|
791
|
+
}
|
|
792
|
+
return canvas.toBuffer('image/png');
|
|
793
|
+
}
|
|
794
|
+
//# sourceMappingURL=piechart.js.map
|