apexify.js 5.1.1 → 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 +240 -0
- package/README.md +248 -1105
- 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 +234 -198
- 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,1389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHorizontalBarChart = createHorizontalBarChart;
|
|
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
|
+
* Draws an arrow at the end of an axis
|
|
94
|
+
*/
|
|
95
|
+
function drawArrow(ctx, x, y, angle, size) {
|
|
96
|
+
ctx.save();
|
|
97
|
+
ctx.translate(x, y);
|
|
98
|
+
ctx.rotate(angle);
|
|
99
|
+
ctx.beginPath();
|
|
100
|
+
ctx.moveTo(0, 0);
|
|
101
|
+
ctx.lineTo(-size, -size / 2);
|
|
102
|
+
ctx.lineTo(-size, size / 2);
|
|
103
|
+
ctx.closePath();
|
|
104
|
+
ctx.fill();
|
|
105
|
+
ctx.restore();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Draws X-axis ticks and labels (horizontal axis - value axis)
|
|
109
|
+
*/
|
|
110
|
+
function drawXAxisTicks(ctx, originX, originY, axisEndX, minValue, maxValue, step, tickFontSize, customValues, valueSpacing) {
|
|
111
|
+
ctx.save();
|
|
112
|
+
ctx.fillStyle = '#000000';
|
|
113
|
+
ctx.font = `${tickFontSize}px Arial`;
|
|
114
|
+
ctx.textAlign = 'center';
|
|
115
|
+
ctx.textBaseline = 'top';
|
|
116
|
+
const chartWidth = axisEndX - originX;
|
|
117
|
+
if (customValues && customValues.length > 0) {
|
|
118
|
+
// Position labels based on their actual values, not pixel spacing
|
|
119
|
+
const actualMin = Math.min(...customValues);
|
|
120
|
+
const actualMax = Math.max(...customValues);
|
|
121
|
+
const range = actualMax - actualMin || 1; // Avoid division by zero
|
|
122
|
+
let lastLabelX = -Infinity;
|
|
123
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40; // Use valueSpacing as min spacing if provided
|
|
124
|
+
customValues.forEach((value) => {
|
|
125
|
+
const x = originX + ((value - actualMin) / range) * chartWidth;
|
|
126
|
+
const labelText = value.toString();
|
|
127
|
+
// Check if this label would overlap with the previous one
|
|
128
|
+
if (x - lastLabelX < minLabelSpacing && value > actualMin) {
|
|
129
|
+
// Skip this label to prevent overlap (but still draw tick mark)
|
|
130
|
+
ctx.beginPath();
|
|
131
|
+
ctx.moveTo(x, originY);
|
|
132
|
+
ctx.lineTo(x, originY + 5);
|
|
133
|
+
ctx.stroke();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
ctx.beginPath();
|
|
137
|
+
ctx.moveTo(x, originY);
|
|
138
|
+
ctx.lineTo(x, originY + 5);
|
|
139
|
+
ctx.stroke();
|
|
140
|
+
ctx.fillText(labelText, x, originY + 10);
|
|
141
|
+
lastLabelX = x; // Update last label center position
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Range-based positioning - always position based on values, use valueSpacing only for label density
|
|
146
|
+
const range = maxValue - minValue || 1; // Avoid division by zero
|
|
147
|
+
// Calculate all tick positions first
|
|
148
|
+
const tickValues = [];
|
|
149
|
+
for (let value = minValue; value <= maxValue; value += step) {
|
|
150
|
+
tickValues.push(value);
|
|
151
|
+
}
|
|
152
|
+
// Draw ticks, but skip labels if they're too close together
|
|
153
|
+
let lastLabelX = -Infinity;
|
|
154
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40; // Use valueSpacing as min spacing if provided
|
|
155
|
+
for (const value of tickValues) {
|
|
156
|
+
const x = originX + ((value - minValue) / range) * chartWidth;
|
|
157
|
+
const labelText = value.toString();
|
|
158
|
+
// Check if this label center is too close to the previous label center
|
|
159
|
+
if (x - lastLabelX < minLabelSpacing && value > minValue) {
|
|
160
|
+
// Skip this label to prevent overlap - but still draw the tick mark
|
|
161
|
+
ctx.beginPath();
|
|
162
|
+
ctx.moveTo(x, originY);
|
|
163
|
+
ctx.lineTo(x, originY + 5);
|
|
164
|
+
ctx.stroke();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Draw tick mark
|
|
168
|
+
ctx.beginPath();
|
|
169
|
+
ctx.moveTo(x, originY);
|
|
170
|
+
ctx.lineTo(x, originY + 5);
|
|
171
|
+
ctx.stroke();
|
|
172
|
+
// Draw label
|
|
173
|
+
ctx.fillText(labelText, x, originY + 10);
|
|
174
|
+
// Update last label position (center of the label)
|
|
175
|
+
lastLabelX = x;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
ctx.restore();
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Draws Y-axis ticks and labels (vertical axis - category axis)
|
|
182
|
+
*/
|
|
183
|
+
function drawYAxisTicks(ctx, originX, originY, axisEndY, minValue, maxValue, step, tickFontSize, customValues, valueSpacing) {
|
|
184
|
+
ctx.save();
|
|
185
|
+
ctx.fillStyle = '#000000';
|
|
186
|
+
ctx.font = `${tickFontSize}px Arial`;
|
|
187
|
+
ctx.textAlign = 'right';
|
|
188
|
+
ctx.textBaseline = 'middle';
|
|
189
|
+
const chartHeight = originY - axisEndY;
|
|
190
|
+
if (customValues && customValues.length > 0) {
|
|
191
|
+
const totalValues = customValues.length;
|
|
192
|
+
const divisor = totalValues > 1 ? totalValues - 1 : 1;
|
|
193
|
+
if (valueSpacing && valueSpacing > 0) {
|
|
194
|
+
let currentY = originY;
|
|
195
|
+
customValues.forEach((value, index) => {
|
|
196
|
+
if (index === 0) {
|
|
197
|
+
currentY = originY;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
currentY -= valueSpacing;
|
|
201
|
+
}
|
|
202
|
+
if (currentY >= axisEndY && currentY <= originY) {
|
|
203
|
+
ctx.beginPath();
|
|
204
|
+
ctx.moveTo(originX - 5, currentY);
|
|
205
|
+
ctx.lineTo(originX, currentY);
|
|
206
|
+
ctx.stroke();
|
|
207
|
+
ctx.fillText(value.toString(), originX - 10, currentY);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Position based on value range
|
|
213
|
+
const range = maxValue - minValue || 1;
|
|
214
|
+
customValues.forEach((value) => {
|
|
215
|
+
const y = originY - ((value - minValue) / range) * chartHeight;
|
|
216
|
+
ctx.beginPath();
|
|
217
|
+
ctx.moveTo(originX - 5, y);
|
|
218
|
+
ctx.lineTo(originX, y);
|
|
219
|
+
ctx.stroke();
|
|
220
|
+
ctx.fillText(value.toString(), originX - 10, y);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Range-based positioning
|
|
226
|
+
const range = maxValue - minValue || 1;
|
|
227
|
+
let lastLabelY = Infinity;
|
|
228
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30; // Vertical spacing
|
|
229
|
+
for (let value = minValue; value <= maxValue; value += step) {
|
|
230
|
+
const y = originY - ((value - minValue) / range) * chartHeight;
|
|
231
|
+
const labelText = value.toString();
|
|
232
|
+
// Check if this label would overlap with the previous one
|
|
233
|
+
if (lastLabelY - y < minLabelSpacing && value > minValue) {
|
|
234
|
+
// Skip this label to prevent overlap - but still draw the tick mark
|
|
235
|
+
ctx.beginPath();
|
|
236
|
+
ctx.moveTo(originX - 5, y);
|
|
237
|
+
ctx.lineTo(originX, y);
|
|
238
|
+
ctx.stroke();
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
ctx.beginPath();
|
|
242
|
+
ctx.moveTo(originX - 5, y);
|
|
243
|
+
ctx.lineTo(originX, y);
|
|
244
|
+
ctx.stroke();
|
|
245
|
+
ctx.fillText(labelText, originX - 10, y);
|
|
246
|
+
lastLabelY = y; // Update last label position
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
ctx.restore();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Draws grid lines for horizontal bar chart
|
|
253
|
+
*/
|
|
254
|
+
function drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep, yMin, yMax, yStep, yAxisCustomValues, xAxisCustomValues, gridColor = '#E0E0E0', gridWidth = 1) {
|
|
255
|
+
ctx.save();
|
|
256
|
+
ctx.strokeStyle = gridColor;
|
|
257
|
+
ctx.lineWidth = gridWidth;
|
|
258
|
+
ctx.setLineDash([2, 2]);
|
|
259
|
+
const chartWidth = axisEndX - originX;
|
|
260
|
+
const chartHeight = originY - axisEndY;
|
|
261
|
+
// Draw vertical grid lines (based on X-axis values)
|
|
262
|
+
if (xAxisCustomValues && xAxisCustomValues.length > 0) {
|
|
263
|
+
const actualMin = Math.min(...xAxisCustomValues);
|
|
264
|
+
const actualMax = Math.max(...xAxisCustomValues);
|
|
265
|
+
const xRange = actualMax - actualMin || 1;
|
|
266
|
+
xAxisCustomValues.forEach((value) => {
|
|
267
|
+
const x = originX + ((value - actualMin) / xRange) * chartWidth;
|
|
268
|
+
ctx.beginPath();
|
|
269
|
+
ctx.moveTo(x, axisEndY);
|
|
270
|
+
ctx.lineTo(x, originY);
|
|
271
|
+
ctx.stroke();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const xRange = xMax - xMin || 1;
|
|
276
|
+
for (let value = xMin; value <= xMax; value += xStep) {
|
|
277
|
+
const x = originX + ((value - xMin) / xRange) * chartWidth;
|
|
278
|
+
ctx.beginPath();
|
|
279
|
+
ctx.moveTo(x, axisEndY);
|
|
280
|
+
ctx.lineTo(x, originY);
|
|
281
|
+
ctx.stroke();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Draw horizontal grid lines (based on Y-axis values/range)
|
|
285
|
+
if (yAxisCustomValues && yAxisCustomValues.length > 0) {
|
|
286
|
+
const actualMin = Math.min(...yAxisCustomValues);
|
|
287
|
+
const actualMax = Math.max(...yAxisCustomValues);
|
|
288
|
+
const yRange = actualMax - actualMin || 1;
|
|
289
|
+
yAxisCustomValues.forEach((value) => {
|
|
290
|
+
const y = originY - ((value - actualMin) / yRange) * chartHeight;
|
|
291
|
+
ctx.beginPath();
|
|
292
|
+
ctx.moveTo(originX, y);
|
|
293
|
+
ctx.lineTo(axisEndX, y);
|
|
294
|
+
ctx.stroke();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const yRange = yMax - yMin || 1;
|
|
299
|
+
for (let value = yMin; value <= yMax; value += yStep) {
|
|
300
|
+
const y = originY - ((value - yMin) / yRange) * chartHeight;
|
|
301
|
+
ctx.beginPath();
|
|
302
|
+
ctx.moveTo(originX, y);
|
|
303
|
+
ctx.lineTo(axisEndX, y);
|
|
304
|
+
ctx.stroke();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
ctx.restore();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Wraps text to fit within a maximum width
|
|
311
|
+
*/
|
|
312
|
+
function wrapText(ctx, text, maxWidth) {
|
|
313
|
+
const words = text.split(' ');
|
|
314
|
+
const lines = [];
|
|
315
|
+
let currentLine = words[0];
|
|
316
|
+
for (let i = 1; i < words.length; i++) {
|
|
317
|
+
const word = words[i];
|
|
318
|
+
const width = ctx.measureText(currentLine + ' ' + word).width;
|
|
319
|
+
if (width < maxWidth) {
|
|
320
|
+
currentLine += ' ' + word;
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
lines.push(currentLine);
|
|
324
|
+
currentLine = word;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
lines.push(currentLine);
|
|
328
|
+
return lines;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Calculates legend dimensions without needing a canvas context
|
|
332
|
+
*/
|
|
333
|
+
function calculateLegendDimensions(legend, fontSize, maxWidth, wrapTextEnabled = true, paddingBox = 8) {
|
|
334
|
+
if (!legend || legend.length === 0)
|
|
335
|
+
return { width: 0, height: 0 };
|
|
336
|
+
const boxSize = 15;
|
|
337
|
+
const spacing = 10;
|
|
338
|
+
const padding = paddingBox;
|
|
339
|
+
// Create a temporary canvas to measure text
|
|
340
|
+
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
341
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
342
|
+
tempCtx.font = `${fontSize}px Arial`;
|
|
343
|
+
const textSpacing = 10;
|
|
344
|
+
const effectiveMaxWidth = maxWidth ? maxWidth - padding * 2 - boxSize - textSpacing : undefined;
|
|
345
|
+
let maxEntryWidth = 0;
|
|
346
|
+
const entryHeights = [];
|
|
347
|
+
legend.forEach(entry => {
|
|
348
|
+
let textWidth;
|
|
349
|
+
let textHeight;
|
|
350
|
+
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
351
|
+
const wrappedLines = wrapText(tempCtx, entry.label, effectiveMaxWidth);
|
|
352
|
+
textWidth = Math.max(...wrappedLines.map(line => tempCtx.measureText(line).width));
|
|
353
|
+
textHeight = wrappedLines.length * fontSize * 1.2;
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
textWidth = tempCtx.measureText(entry.label).width;
|
|
357
|
+
textHeight = fontSize;
|
|
358
|
+
}
|
|
359
|
+
const entryWidth = boxSize + textSpacing + textWidth;
|
|
360
|
+
maxEntryWidth = Math.max(maxEntryWidth, entryWidth);
|
|
361
|
+
entryHeights.push(Math.max(boxSize, textHeight));
|
|
362
|
+
});
|
|
363
|
+
const legendWidth = maxWidth ? maxWidth : maxEntryWidth + padding * 2;
|
|
364
|
+
const legendHeight = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? spacing : 0), 0) + padding * 2;
|
|
365
|
+
return { width: legendWidth, height: legendHeight };
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Draws legend/key showing colors and their meanings at a specific position
|
|
369
|
+
*/
|
|
370
|
+
async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, backgroundColor = '#FFFFFF', textColor, borderColor, paddingBox, maxWidth, wrapTextEnabled = true, backgroundGradient, textGradient, textStyle) {
|
|
371
|
+
if (!legend || legend.length === 0)
|
|
372
|
+
return;
|
|
373
|
+
ctx.save();
|
|
374
|
+
const boxSize = 15;
|
|
375
|
+
const spacing = 10;
|
|
376
|
+
const padding = paddingBox ?? 8;
|
|
377
|
+
ctx.font = `${fontSize}px Arial`;
|
|
378
|
+
// Determine colors
|
|
379
|
+
const isDarkBackground = backgroundColor === '#000000' || backgroundColor.toLowerCase() === 'black';
|
|
380
|
+
const effectiveTextColor = textColor ?? (isDarkBackground ? '#FFFFFF' : '#000000');
|
|
381
|
+
const effectiveBgColor = isDarkBackground ? 'rgba(0, 0, 0, 0.8)' : (backgroundColor.startsWith('rgba') || backgroundColor.startsWith('rgb') ? backgroundColor : 'rgba(255, 255, 255, 0.9)');
|
|
382
|
+
const effectiveBorderColor = borderColor ?? (isDarkBackground ? '#FFFFFF' : '#000000');
|
|
383
|
+
// Calculate dimensions with text wrapping support
|
|
384
|
+
const textSpacing = 10;
|
|
385
|
+
const effectiveMaxWidth = maxWidth ? maxWidth - padding * 2 - boxSize - textSpacing : undefined;
|
|
386
|
+
let maxEntryWidth = 0;
|
|
387
|
+
const entryHeights = [];
|
|
388
|
+
legend.forEach(entry => {
|
|
389
|
+
let textWidth;
|
|
390
|
+
let textHeight;
|
|
391
|
+
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
392
|
+
const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
|
|
393
|
+
textWidth = Math.max(...wrappedLines.map(line => ctx.measureText(line).width));
|
|
394
|
+
textHeight = wrappedLines.length * fontSize * 1.2;
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
textWidth = ctx.measureText(entry.label).width;
|
|
398
|
+
textHeight = fontSize;
|
|
399
|
+
}
|
|
400
|
+
const entryWidth = boxSize + textSpacing + textWidth;
|
|
401
|
+
maxEntryWidth = Math.max(maxEntryWidth, entryWidth);
|
|
402
|
+
entryHeights.push(Math.max(boxSize, textHeight));
|
|
403
|
+
});
|
|
404
|
+
const legendWidth = maxWidth ? maxWidth : maxEntryWidth + padding * 2;
|
|
405
|
+
const legendHeight = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? spacing : 0), 0) + padding * 2;
|
|
406
|
+
// Draw legend background (gradient or color)
|
|
407
|
+
ctx.beginPath();
|
|
408
|
+
ctx.rect(legendX, legendY, legendWidth, legendHeight);
|
|
409
|
+
fillWithGradientOrColor(ctx, backgroundGradient, effectiveBgColor, effectiveBgColor, { x: legendX, y: legendY, w: legendWidth, h: legendHeight });
|
|
410
|
+
ctx.fill();
|
|
411
|
+
ctx.strokeStyle = effectiveBorderColor;
|
|
412
|
+
ctx.lineWidth = 1;
|
|
413
|
+
ctx.strokeRect(legendX, legendY, legendWidth, legendHeight);
|
|
414
|
+
ctx.textAlign = 'left';
|
|
415
|
+
ctx.textBaseline = 'middle';
|
|
416
|
+
let currentY = legendY + padding;
|
|
417
|
+
for (let index = 0; index < legend.length; index++) {
|
|
418
|
+
const entry = legend[index];
|
|
419
|
+
const entryHeight = entryHeights[index];
|
|
420
|
+
const centerY = currentY + entryHeight / 2;
|
|
421
|
+
// Draw color box (gradient or color)
|
|
422
|
+
ctx.beginPath();
|
|
423
|
+
ctx.rect(legendX + padding, centerY - boxSize / 2, boxSize, boxSize);
|
|
424
|
+
fillWithGradientOrColor(ctx, entry.gradient, entry.color || '#4A90E2', '#4A90E2', { x: legendX + padding, y: centerY - boxSize / 2, w: boxSize, h: boxSize });
|
|
425
|
+
ctx.fill();
|
|
426
|
+
ctx.strokeStyle = effectiveBorderColor;
|
|
427
|
+
ctx.lineWidth = 1;
|
|
428
|
+
ctx.strokeRect(legendX + padding, centerY - boxSize / 2, boxSize, boxSize);
|
|
429
|
+
const textX = legendX + padding + boxSize + textSpacing;
|
|
430
|
+
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
431
|
+
const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
|
|
432
|
+
const lineHeight = fontSize * 1.2;
|
|
433
|
+
const startY = centerY - (wrappedLines.length - 1) * lineHeight / 2;
|
|
434
|
+
for (let lineIndex = 0; lineIndex < wrappedLines.length; lineIndex++) {
|
|
435
|
+
await renderEnhancedText(ctx, wrappedLines[lineIndex], textX, startY + lineIndex * lineHeight, textStyle, fontSize, effectiveTextColor, textGradient);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
await renderEnhancedText(ctx, entry.label, textX, centerY, textStyle, fontSize, effectiveTextColor, textGradient);
|
|
440
|
+
}
|
|
441
|
+
currentY += entryHeight + spacing;
|
|
442
|
+
}
|
|
443
|
+
ctx.restore();
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Draws legend/key showing colors and their meanings
|
|
447
|
+
*/
|
|
448
|
+
function drawLegend(ctx, legend, position, width, height, padding, fontSize, backgroundColor = '#FFFFFF', legendSpacing = 20) {
|
|
449
|
+
if (!legend || legend.length === 0)
|
|
450
|
+
return;
|
|
451
|
+
ctx.save();
|
|
452
|
+
const boxSize = 15;
|
|
453
|
+
const spacing = 10;
|
|
454
|
+
const paddingBox = 8;
|
|
455
|
+
ctx.font = `${fontSize}px Arial`;
|
|
456
|
+
const maxLabelWidth = Math.max(...legend.map(e => ctx.measureText(e.label).width));
|
|
457
|
+
const legendWidth = boxSize + spacing + maxLabelWidth + paddingBox * 2;
|
|
458
|
+
const legendHeight = legend.length * (boxSize + spacing) + paddingBox * 2;
|
|
459
|
+
let legendX, legendY;
|
|
460
|
+
switch (position) {
|
|
461
|
+
case 'top':
|
|
462
|
+
legendX = width - padding.right - legendWidth - legendSpacing;
|
|
463
|
+
legendY = padding.top + legendSpacing;
|
|
464
|
+
break;
|
|
465
|
+
case 'bottom':
|
|
466
|
+
legendX = width - padding.right - legendWidth - legendSpacing;
|
|
467
|
+
legendY = height - padding.bottom - legendHeight - legendSpacing;
|
|
468
|
+
break;
|
|
469
|
+
case 'right':
|
|
470
|
+
legendX = width - padding.right - legendWidth - legendSpacing;
|
|
471
|
+
legendY = padding.top + legendSpacing;
|
|
472
|
+
break;
|
|
473
|
+
case 'left':
|
|
474
|
+
legendX = padding.left + legendSpacing;
|
|
475
|
+
legendY = padding.top + legendSpacing;
|
|
476
|
+
break;
|
|
477
|
+
default:
|
|
478
|
+
legendX = width - padding.right - legendWidth - legendSpacing;
|
|
479
|
+
legendY = padding.top + legendSpacing;
|
|
480
|
+
}
|
|
481
|
+
const isDarkBackground = backgroundColor === '#000000' || backgroundColor.toLowerCase() === 'black';
|
|
482
|
+
const textColor = isDarkBackground ? '#FFFFFF' : '#000000';
|
|
483
|
+
const bgColor = isDarkBackground ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)';
|
|
484
|
+
const borderColor = isDarkBackground ? '#FFFFFF' : '#000000';
|
|
485
|
+
ctx.fillStyle = bgColor;
|
|
486
|
+
ctx.fillRect(legendX, legendY, legendWidth, legendHeight);
|
|
487
|
+
ctx.strokeStyle = borderColor;
|
|
488
|
+
ctx.lineWidth = 1;
|
|
489
|
+
ctx.strokeRect(legendX, legendY, legendWidth, legendHeight);
|
|
490
|
+
ctx.font = `${fontSize}px Arial`;
|
|
491
|
+
ctx.textAlign = 'left';
|
|
492
|
+
ctx.textBaseline = 'middle';
|
|
493
|
+
legend.forEach((entry, index) => {
|
|
494
|
+
const y = legendY + paddingBox + index * (boxSize + spacing) + boxSize / 2;
|
|
495
|
+
const x = legendX + paddingBox;
|
|
496
|
+
ctx.beginPath();
|
|
497
|
+
ctx.rect(x, y - boxSize / 2, boxSize, boxSize);
|
|
498
|
+
fillWithGradientOrColor(ctx, entry.gradient, entry.color || '#4A90E2', '#4A90E2', { x, y: y - boxSize / 2, w: boxSize, h: boxSize });
|
|
499
|
+
ctx.fill();
|
|
500
|
+
ctx.strokeStyle = borderColor;
|
|
501
|
+
ctx.lineWidth = 1;
|
|
502
|
+
ctx.strokeRect(x, y - boxSize / 2, boxSize, boxSize);
|
|
503
|
+
ctx.fillStyle = textColor;
|
|
504
|
+
ctx.fillText(entry.label, x + boxSize + spacing, y);
|
|
505
|
+
});
|
|
506
|
+
ctx.restore();
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Calculates responsive canvas height based on number of bars
|
|
510
|
+
*/
|
|
511
|
+
function calculateResponsiveHeight(dataLength, options = {}) {
|
|
512
|
+
const padding = options.dimensions?.padding || {};
|
|
513
|
+
const paddingTop = padding.top ?? 60;
|
|
514
|
+
const paddingBottom = padding.bottom ?? 80;
|
|
515
|
+
const minBarHeight = options.bars?.minHeight ?? 40;
|
|
516
|
+
const barSpacing = options.bars?.spacing ?? 15; // Use same default spacing
|
|
517
|
+
// Calculate minimum height needed: (number of bars * bar height) + (spacing between bars)
|
|
518
|
+
// Each bar needs minBarHeight, and between each pair of bars we need barSpacing
|
|
519
|
+
const chartAreaHeight = dataLength * minBarHeight + (dataLength - 1) * barSpacing;
|
|
520
|
+
// Add title height if needed
|
|
521
|
+
const titleHeight = options.labels?.title?.text ? (options.labels.title.fontSize ?? 24) + 30 : 0;
|
|
522
|
+
const axisLabelHeight = ((options.axes?.x?.label || options.axes?.y?.label) ? (options.labels?.barLabelDefaults?.fontSize ?? 14) + 20 : 0);
|
|
523
|
+
return paddingTop + titleHeight + chartAreaHeight + axisLabelHeight + paddingBottom;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Creates a horizontal bar chart
|
|
527
|
+
* @param data Array of horizontal bar chart data
|
|
528
|
+
* @param options Chart options
|
|
529
|
+
* @returns Canvas buffer
|
|
530
|
+
*/
|
|
531
|
+
async function createHorizontalBarChart(data, options = {}) {
|
|
532
|
+
// Extract and map organized config to internal variables
|
|
533
|
+
let width = options.dimensions?.width ?? 800;
|
|
534
|
+
const padding = options.dimensions?.padding || {};
|
|
535
|
+
// Appearance
|
|
536
|
+
const backgroundColor = options.appearance?.backgroundColor ?? '#FFFFFF';
|
|
537
|
+
const backgroundGradient = options.appearance?.backgroundGradient;
|
|
538
|
+
const backgroundImage = options.appearance?.backgroundImage;
|
|
539
|
+
const axisColor = options.appearance?.axisColor ?? options.axes?.x?.color ?? options.axes?.y?.color ?? '#000000';
|
|
540
|
+
const axisWidth = options.appearance?.axisWidth ?? options.axes?.x?.width ?? options.axes?.y?.width ?? 2;
|
|
541
|
+
const arrowSize = options.appearance?.arrowSize ?? 10;
|
|
542
|
+
// Labels
|
|
543
|
+
const chartTitle = options.labels?.title?.text;
|
|
544
|
+
const chartTitleFontSize = options.labels?.title?.fontSize ?? 24;
|
|
545
|
+
const showBarLabels = options.labels?.barLabelDefaults?.show ?? true;
|
|
546
|
+
const barLabelPosition = options.labels?.barLabelDefaults?.defaultPosition ?? 'left';
|
|
547
|
+
const axisLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
|
|
548
|
+
const showValues = options.labels?.valueLabelDefaults?.show ?? true;
|
|
549
|
+
const valueFontSize = options.labels?.valueLabelDefaults?.fontSize ?? 12;
|
|
550
|
+
const valueColor = options.labels?.valueLabelDefaults?.defaultColor ?? '#000000';
|
|
551
|
+
// Axes
|
|
552
|
+
const xAxisLabel = options.axes?.x?.label;
|
|
553
|
+
const yAxisLabel = options.axes?.y?.label;
|
|
554
|
+
const axisLabelColor = options.axes?.x?.labelColor ?? options.axes?.y?.labelColor ?? '#000000';
|
|
555
|
+
const xAxisRange = options.axes?.x?.range;
|
|
556
|
+
const xAxisValues = options.axes?.x?.values;
|
|
557
|
+
const baseline = options.axes?.x?.baseline ?? 0; // Custom baseline value (default: 0)
|
|
558
|
+
const yAxisValues = options.axes?.y?.values;
|
|
559
|
+
const tickFontSize = options.axes?.x?.tickFontSize ?? options.axes?.y?.tickFontSize ?? 12;
|
|
560
|
+
const xAxisValueSpacing = options.axes?.x?.valueSpacing;
|
|
561
|
+
const yAxisValueSpacing = options.axes?.y?.valueSpacing;
|
|
562
|
+
// Legend
|
|
563
|
+
const showLegend = options.legend?.show ?? false;
|
|
564
|
+
const legend = options.legend?.entries;
|
|
565
|
+
const legendPosition = options.legend?.position ?? 'right'; // Default: right
|
|
566
|
+
// Grid
|
|
567
|
+
const showGrid = options.grid?.show ?? false;
|
|
568
|
+
const gridColor = options.grid?.color ?? '#E0E0E0';
|
|
569
|
+
const gridWidth = options.grid?.width ?? 1;
|
|
570
|
+
// Chart type
|
|
571
|
+
const chartType = options.type ?? 'standard';
|
|
572
|
+
// Bars
|
|
573
|
+
const minBarHeight = options.bars?.minHeight ?? 30;
|
|
574
|
+
const barSpacing = options.bars?.spacing;
|
|
575
|
+
const groupSpacing = options.bars?.groupSpacing ?? 10;
|
|
576
|
+
const segmentSpacing = options.bars?.segmentSpacing ?? 2;
|
|
577
|
+
const lollipopLineWidth = options.bars?.lineWidth ?? 2; // Line width for lollipop charts (default: 2)
|
|
578
|
+
const lollipopDotSize = options.bars?.dotSize ?? 8; // Dot/circle size for lollipop charts (default: 8)
|
|
579
|
+
const paddingTop = padding.top ?? 60;
|
|
580
|
+
const paddingRight = padding.right ?? 80;
|
|
581
|
+
const paddingBottom = padding.bottom ?? 80;
|
|
582
|
+
const paddingLeft = padding.left ?? 100;
|
|
583
|
+
// Calculate responsive height based on number of bars
|
|
584
|
+
let baseHeight = calculateResponsiveHeight(data.length, options);
|
|
585
|
+
// Calculate legend dimensions and adjust canvas size based on legend position
|
|
586
|
+
let legendWidth = 0;
|
|
587
|
+
let legendHeight = 0;
|
|
588
|
+
let extraWidth = 0;
|
|
589
|
+
let extraHeight = 0;
|
|
590
|
+
const minLegendSpacing = 10; // Minimum spacing from chart area
|
|
591
|
+
if (showLegend && legend && legend.length > 0) {
|
|
592
|
+
const legendMaxWidth = options.legend?.maxWidth;
|
|
593
|
+
const legendWrapText = options.legend?.wrapText !== false;
|
|
594
|
+
const legendPadding = options.legend?.padding;
|
|
595
|
+
const legendDims = calculateLegendDimensions(legend, axisLabelFontSize, legendMaxWidth, legendWrapText, legendPadding);
|
|
596
|
+
legendWidth = legendDims.width;
|
|
597
|
+
legendHeight = legendDims.height;
|
|
598
|
+
const legendSpacing = options.legend?.spacing ?? 20;
|
|
599
|
+
// Adjust canvas dimensions based on legend position
|
|
600
|
+
// For left position, add extra space for Y-axis labels and bar labels
|
|
601
|
+
if (legendPosition === 'left') {
|
|
602
|
+
// Estimate Y-axis label width: measure potential category labels or numeric values
|
|
603
|
+
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
604
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
605
|
+
let estimatedYAxisLabelWidth = 80; // Default estimate (category labels can be longer)
|
|
606
|
+
if (tempCtx) {
|
|
607
|
+
// Check if bar labels are on the left (they act as Y-axis labels)
|
|
608
|
+
const barLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
|
|
609
|
+
const showBarLabels = options.labels?.barLabelDefaults?.show ?? true;
|
|
610
|
+
const barLabelPosition = options.labels?.barLabelDefaults?.defaultPosition ?? 'left';
|
|
611
|
+
const hasLeftLabels = showBarLabels && (barLabelPosition === 'left' ||
|
|
612
|
+
data.some(item => (item.labelPosition ?? barLabelPosition) === 'left'));
|
|
613
|
+
if (hasLeftLabels) {
|
|
614
|
+
// Measure category labels (bar labels) which are typically longer
|
|
615
|
+
tempCtx.font = `${barLabelFontSize}px Arial`;
|
|
616
|
+
data.forEach(d => {
|
|
617
|
+
const labelWidth = tempCtx.measureText(d.label).width;
|
|
618
|
+
estimatedYAxisLabelWidth = Math.max(estimatedYAxisLabelWidth, labelWidth);
|
|
619
|
+
});
|
|
620
|
+
// Add padding: 5px (label offset from originX) + 10px (spacing) = 15px total
|
|
621
|
+
estimatedYAxisLabelWidth += 15;
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
// No left labels, but might have Y-axis numeric ticks
|
|
625
|
+
tempCtx.font = `${tickFontSize}px Arial`;
|
|
626
|
+
// Estimate for numeric Y-axis values if custom values are provided
|
|
627
|
+
estimatedYAxisLabelWidth = 60; // Default for numeric values
|
|
628
|
+
// Add padding: 10px (label offset) + 5px (tick) + 15px (spacing) = 30px total
|
|
629
|
+
estimatedYAxisLabelWidth += 30;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
extraWidth = legendWidth + legendSpacing + estimatedYAxisLabelWidth + minLegendSpacing;
|
|
633
|
+
}
|
|
634
|
+
else if (legendPosition === 'right') {
|
|
635
|
+
extraWidth = legendWidth + legendSpacing + minLegendSpacing;
|
|
636
|
+
}
|
|
637
|
+
else if (legendPosition === 'top' || legendPosition === 'bottom') {
|
|
638
|
+
extraHeight = legendHeight + legendSpacing + minLegendSpacing;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// adjustedWidth and adjustedHeight are already calculated above
|
|
642
|
+
// Determine X-axis (value axis) range
|
|
643
|
+
// For grouped charts: find max value across all segments
|
|
644
|
+
// For stacked charts: find max sum of values per category
|
|
645
|
+
// For lollipop charts: same as standard (single value per bar)
|
|
646
|
+
let allValues = [];
|
|
647
|
+
if (chartType === 'grouped' || chartType === 'stacked' || chartType === 'lollipop') {
|
|
648
|
+
if (chartType === 'grouped') {
|
|
649
|
+
// For grouped: find max value across all segments
|
|
650
|
+
data.forEach(d => {
|
|
651
|
+
if (d.values && d.values.length > 0) {
|
|
652
|
+
d.values.forEach(seg => allValues.push(seg.value));
|
|
653
|
+
}
|
|
654
|
+
else if (d.value !== undefined) {
|
|
655
|
+
allValues.push(d.value);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
// For stacked: find max sum per category
|
|
661
|
+
data.forEach(d => {
|
|
662
|
+
if (d.values && d.values.length > 0) {
|
|
663
|
+
const sum = d.values.reduce((acc, seg) => acc + seg.value, 0);
|
|
664
|
+
allValues.push(sum);
|
|
665
|
+
}
|
|
666
|
+
else if (d.value !== undefined) {
|
|
667
|
+
allValues.push(d.value);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
// Standard chart: use value directly
|
|
674
|
+
allValues = data.map(d => d.value ?? 0).filter(v => v !== undefined && v !== null);
|
|
675
|
+
}
|
|
676
|
+
let xMin, xMax;
|
|
677
|
+
let xAxisCustomValues = xAxisValues;
|
|
678
|
+
const hasExplicitXRange = xAxisRange && xAxisRange.min !== undefined && xAxisRange.max !== undefined;
|
|
679
|
+
// Check if any bars have xStart/xEnd (value ranges)
|
|
680
|
+
const hasValueRanges = data.some(d => d.xStart !== undefined || d.xEnd !== undefined);
|
|
681
|
+
if (hasValueRanges) {
|
|
682
|
+
const allXStarts = data.map(d => d.xStart ?? d.value ?? 0).filter(v => v !== undefined);
|
|
683
|
+
const allXEnds = data.map(d => d.xEnd ?? d.value ?? 0).filter(v => v !== undefined);
|
|
684
|
+
xMin = Math.min(...allXStarts, ...allXEnds);
|
|
685
|
+
xMax = Math.max(...allXStarts, ...allXEnds);
|
|
686
|
+
// Add some padding
|
|
687
|
+
const xPadding = (xMax - xMin) * 0.1;
|
|
688
|
+
xMin = Math.max(0, xMin - xPadding);
|
|
689
|
+
xMax = xMax + xPadding;
|
|
690
|
+
}
|
|
691
|
+
else if (xAxisCustomValues && xAxisCustomValues.length > 0) {
|
|
692
|
+
xMin = Math.min(...xAxisCustomValues);
|
|
693
|
+
xMax = Math.max(...xAxisCustomValues);
|
|
694
|
+
}
|
|
695
|
+
else if (hasExplicitXRange) {
|
|
696
|
+
xMin = xAxisRange.min;
|
|
697
|
+
xMax = xAxisRange.max;
|
|
698
|
+
// Ensure baseline is within range
|
|
699
|
+
const effectiveBaseline = baseline !== undefined ? baseline : 0;
|
|
700
|
+
xMin = Math.min(xMin, effectiveBaseline);
|
|
701
|
+
xMax = Math.max(xMax, effectiveBaseline);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
xMin = 0;
|
|
705
|
+
xMax = Math.max(...allValues, 1);
|
|
706
|
+
const xPadding = (xMax - xMin) * 0.1;
|
|
707
|
+
const effectiveBaseline = baseline !== undefined ? baseline : 0;
|
|
708
|
+
// Ensure baseline is always included in the range
|
|
709
|
+
xMin = Math.min(Math.max(0, xMin - xPadding), effectiveBaseline);
|
|
710
|
+
xMax = xMax + xPadding;
|
|
711
|
+
}
|
|
712
|
+
// Determine Y-axis (category axis) range - similar to X-axis in standard chart
|
|
713
|
+
const yAxisRange = options.axes?.y?.range;
|
|
714
|
+
let yMin, yMax;
|
|
715
|
+
let yAxisCustomValues = yAxisValues;
|
|
716
|
+
const hasExplicitYRange = yAxisRange && yAxisRange.min !== undefined && yAxisRange.max !== undefined;
|
|
717
|
+
if (yAxisCustomValues && yAxisCustomValues.length > 0) {
|
|
718
|
+
yMin = Math.min(...yAxisCustomValues);
|
|
719
|
+
yMax = Math.max(...yAxisCustomValues);
|
|
720
|
+
}
|
|
721
|
+
else if (hasExplicitYRange) {
|
|
722
|
+
yMin = yAxisRange.min;
|
|
723
|
+
yMax = yAxisRange.max;
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
// Auto-calculate from data indices (0 to data.length - 1)
|
|
727
|
+
yMin = 0;
|
|
728
|
+
yMax = data.length - 1;
|
|
729
|
+
}
|
|
730
|
+
// Validate data values against explicit axis ranges
|
|
731
|
+
if (hasExplicitXRange || xAxisCustomValues) {
|
|
732
|
+
const effectiveXMin = xAxisCustomValues ? Math.min(...xAxisCustomValues) : xAxisRange.min;
|
|
733
|
+
const effectiveXMax = xAxisCustomValues ? Math.max(...xAxisCustomValues) : xAxisRange.max;
|
|
734
|
+
data.forEach((item, itemIndex) => {
|
|
735
|
+
// Check value (X-axis for horizontal bars)
|
|
736
|
+
if (item.value !== undefined && (item.value < effectiveXMin || item.value > effectiveXMax)) {
|
|
737
|
+
throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
|
|
738
|
+
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has value ${item.value}, ` +
|
|
739
|
+
`which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
|
|
740
|
+
}
|
|
741
|
+
// Check xStart and xEnd if they exist
|
|
742
|
+
if (item.xStart !== undefined && (item.xStart < effectiveXMin || item.xStart > effectiveXMax)) {
|
|
743
|
+
throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
|
|
744
|
+
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has xStart value ${item.xStart}, ` +
|
|
745
|
+
`which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
|
|
746
|
+
}
|
|
747
|
+
if (item.xEnd !== undefined && (item.xEnd < effectiveXMin || item.xEnd > effectiveXMax)) {
|
|
748
|
+
throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
|
|
749
|
+
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has xEnd value ${item.xEnd}, ` +
|
|
750
|
+
`which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
|
|
751
|
+
}
|
|
752
|
+
// Check grouped/stacked values
|
|
753
|
+
if (item.values && item.values.length > 0) {
|
|
754
|
+
item.values.forEach((seg, segIndex) => {
|
|
755
|
+
if (seg.value < effectiveXMin || seg.value > effectiveXMax) {
|
|
756
|
+
throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
|
|
757
|
+
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" segment ${segIndex} has value ${seg.value}, ` +
|
|
758
|
+
`which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
if (hasExplicitYRange || yAxisCustomValues) {
|
|
765
|
+
const effectiveYMin = yAxisCustomValues ? Math.min(...yAxisCustomValues) : yAxisRange.min;
|
|
766
|
+
const effectiveYMax = yAxisCustomValues ? Math.max(...yAxisCustomValues) : yAxisRange.max;
|
|
767
|
+
data.forEach((item, itemIndex) => {
|
|
768
|
+
// Check yStart and yEnd (Y-axis for horizontal bars)
|
|
769
|
+
if (item.yStart !== undefined && (item.yStart < effectiveYMin || item.yStart > effectiveYMax)) {
|
|
770
|
+
throw new Error(`Horizontal Bar Chart Error: Data value out of Y-axis bounds.\n` +
|
|
771
|
+
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has yStart value ${item.yStart}, ` +
|
|
772
|
+
`which exceeds the Y-axis range [${effectiveYMin}, ${effectiveYMax}].`);
|
|
773
|
+
}
|
|
774
|
+
if (item.yEnd !== undefined && (item.yEnd < effectiveYMin || item.yEnd > effectiveYMax)) {
|
|
775
|
+
throw new Error(`Horizontal Bar Chart Error: Data value out of Y-axis bounds.\n` +
|
|
776
|
+
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has yEnd value ${item.yEnd}, ` +
|
|
777
|
+
`which exceeds the Y-axis range [${effectiveYMin}, ${effectiveYMax}].`);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
// Legend dimensions already calculated above, no need to recalculate
|
|
782
|
+
// Calculate adjusted dimensions (needed before creating canvas)
|
|
783
|
+
const adjustedWidth = width + extraWidth;
|
|
784
|
+
const adjustedHeight = baseHeight + extraHeight;
|
|
785
|
+
// Create canvas
|
|
786
|
+
const canvas = (0, canvas_1.createCanvas)(adjustedWidth, adjustedHeight);
|
|
787
|
+
const ctx = canvas.getContext('2d');
|
|
788
|
+
// Fill background (gradient, image, or color)
|
|
789
|
+
if (backgroundImage) {
|
|
790
|
+
try {
|
|
791
|
+
const bgImage = await (0, canvas_1.loadImage)(backgroundImage);
|
|
792
|
+
ctx.drawImage(bgImage, 0, 0, adjustedWidth, adjustedHeight);
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
console.warn(`Failed to load background image: ${backgroundImage}`, error);
|
|
796
|
+
// Fallback to gradient or color if image fails to load
|
|
797
|
+
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
|
|
798
|
+
x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
|
|
799
|
+
});
|
|
800
|
+
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
|
|
805
|
+
x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
|
|
806
|
+
});
|
|
807
|
+
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
808
|
+
}
|
|
809
|
+
// Calculate axis positions
|
|
810
|
+
const titleHeight = chartTitle ? chartTitleFontSize + 30 : 0;
|
|
811
|
+
const axisLabelHeight = (xAxisLabel || yAxisLabel) ? axisLabelFontSize + 20 : 0;
|
|
812
|
+
// Adjust chart area based on legend position
|
|
813
|
+
// Note: adjustedWidth and adjustedHeight are already calculated above (before canvas creation)
|
|
814
|
+
let chartAreaLeft = paddingLeft;
|
|
815
|
+
let chartAreaRight = width - paddingRight;
|
|
816
|
+
let chartAreaTop = paddingTop + titleHeight;
|
|
817
|
+
let chartAreaBottom = adjustedHeight - paddingBottom;
|
|
818
|
+
if (showLegend && legend && legend.length > 0) {
|
|
819
|
+
const legendSpacing = options.legend?.spacing ?? 20;
|
|
820
|
+
if (legendPosition === 'left') {
|
|
821
|
+
// Calculate actual Y-axis label width (category labels or numeric values)
|
|
822
|
+
let actualYAxisLabelWidth = 80; // Default estimate
|
|
823
|
+
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
824
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
825
|
+
if (tempCtx) {
|
|
826
|
+
// Check if bar labels are positioned on the left (they act as Y-axis labels)
|
|
827
|
+
const barLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
|
|
828
|
+
tempCtx.font = `${barLabelFontSize}px Arial`;
|
|
829
|
+
// Check if bar labels are on the left side
|
|
830
|
+
const hasLeftLabels = barLabelPosition === 'left' ||
|
|
831
|
+
data.some(item => (item.labelPosition ?? barLabelPosition) === 'left');
|
|
832
|
+
if (hasLeftLabels && showBarLabels) {
|
|
833
|
+
// Measure category labels (bar labels) - these are the Y-axis labels
|
|
834
|
+
data.forEach(d => {
|
|
835
|
+
const labelWidth = tempCtx.measureText(d.label).width;
|
|
836
|
+
actualYAxisLabelWidth = Math.max(actualYAxisLabelWidth, labelWidth);
|
|
837
|
+
});
|
|
838
|
+
// Add padding: 5px (label offset from originX) + 10px (spacing) = 15px total
|
|
839
|
+
actualYAxisLabelWidth += 15;
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
// No left labels, but might have Y-axis numeric ticks
|
|
843
|
+
tempCtx.font = `${tickFontSize}px Arial`;
|
|
844
|
+
// Estimate for numeric Y-axis values if custom values are provided
|
|
845
|
+
actualYAxisLabelWidth = 60; // Default for numeric values
|
|
846
|
+
// Add padding: 10px (label offset) + 5px (tick) + 15px (spacing) = 30px total
|
|
847
|
+
actualYAxisLabelWidth += 30;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// Position chart area to leave room for legend + Y-axis labels
|
|
851
|
+
chartAreaLeft = paddingLeft + legendWidth + legendSpacing + actualYAxisLabelWidth;
|
|
852
|
+
chartAreaRight = width - paddingRight;
|
|
853
|
+
}
|
|
854
|
+
else if (legendPosition === 'right') {
|
|
855
|
+
chartAreaLeft = paddingLeft;
|
|
856
|
+
chartAreaRight = width - paddingRight;
|
|
857
|
+
}
|
|
858
|
+
else if (legendPosition === 'top') {
|
|
859
|
+
chartAreaTop = paddingTop + titleHeight + legendHeight + legendSpacing + minLegendSpacing;
|
|
860
|
+
chartAreaBottom = adjustedHeight - paddingBottom;
|
|
861
|
+
}
|
|
862
|
+
else if (legendPosition === 'bottom') {
|
|
863
|
+
chartAreaTop = paddingTop + titleHeight;
|
|
864
|
+
chartAreaBottom = adjustedHeight - paddingBottom;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const originX = chartAreaLeft;
|
|
868
|
+
// Use adjustedHeight for originY calculation to account for legend space
|
|
869
|
+
const originY = adjustedHeight - paddingBottom - axisLabelHeight;
|
|
870
|
+
const axisEndY = chartAreaTop;
|
|
871
|
+
const axisEndX = chartAreaRight;
|
|
872
|
+
// Draw chart title
|
|
873
|
+
if (chartTitle) {
|
|
874
|
+
ctx.save();
|
|
875
|
+
ctx.textAlign = 'center';
|
|
876
|
+
ctx.textBaseline = 'top';
|
|
877
|
+
// Title positioned with proper spacing from top
|
|
878
|
+
const titleY = paddingTop + 10;
|
|
879
|
+
const titleX = adjustedWidth / 2;
|
|
880
|
+
await renderEnhancedText(ctx, chartTitle, titleX, titleY, options.labels?.title?.textStyle, chartTitleFontSize, options.labels?.title?.color, options.labels?.title?.gradient);
|
|
881
|
+
ctx.restore();
|
|
882
|
+
}
|
|
883
|
+
// Set axis style
|
|
884
|
+
ctx.strokeStyle = axisColor;
|
|
885
|
+
ctx.fillStyle = axisColor;
|
|
886
|
+
ctx.lineWidth = axisWidth;
|
|
887
|
+
ctx.lineCap = 'round';
|
|
888
|
+
// X-axis will be drawn after calculating zero line
|
|
889
|
+
// Draw Y-axis (vertical - category axis)
|
|
890
|
+
ctx.beginPath();
|
|
891
|
+
ctx.moveTo(originX, originY);
|
|
892
|
+
ctx.lineTo(originX, axisEndY);
|
|
893
|
+
ctx.stroke();
|
|
894
|
+
// Draw Y-axis arrow
|
|
895
|
+
drawArrow(ctx, originX, axisEndY, -Math.PI / 2, arrowSize); // Y-axis arrow (up)
|
|
896
|
+
// Calculate X-axis step
|
|
897
|
+
const xStep = xAxisRange?.step ?? Math.ceil((xMax - xMin) / 10);
|
|
898
|
+
// Calculate Y-axis step
|
|
899
|
+
const yStep = yAxisRange?.step ?? 1;
|
|
900
|
+
// Calculate chart area dimensions (needed for baseline calculation)
|
|
901
|
+
const chartAreaWidth = axisEndX - originX;
|
|
902
|
+
// Calculate baseline position for X-axis (custom baseline value, default is 0)
|
|
903
|
+
const baselineX = originX + ((baseline - xMin) / (xMax - xMin)) * chartAreaWidth;
|
|
904
|
+
// Draw X-axis at baseline position (horizontal line at originY)
|
|
905
|
+
ctx.beginPath();
|
|
906
|
+
ctx.moveTo(originX, originY);
|
|
907
|
+
ctx.lineTo(axisEndX, originY);
|
|
908
|
+
ctx.stroke();
|
|
909
|
+
// Draw X-axis arrow
|
|
910
|
+
drawArrow(ctx, axisEndX, originY, 0, arrowSize);
|
|
911
|
+
// Draw X-axis ticks and labels at baseline position
|
|
912
|
+
drawXAxisTicks(ctx, originX, originY, axisEndX, xMin, xMax, xStep, tickFontSize, xAxisCustomValues, xAxisValueSpacing);
|
|
913
|
+
// Draw Y-axis ticks and labels (with values/range support)
|
|
914
|
+
drawYAxisTicks(ctx, originX, originY, axisEndY, yMin, yMax, yStep, tickFontSize, yAxisCustomValues, yAxisValueSpacing);
|
|
915
|
+
// Draw axis labels
|
|
916
|
+
if (xAxisLabel) {
|
|
917
|
+
ctx.save();
|
|
918
|
+
ctx.fillStyle = axisLabelColor;
|
|
919
|
+
ctx.font = `${axisLabelFontSize}px Arial`;
|
|
920
|
+
ctx.textAlign = 'center';
|
|
921
|
+
ctx.textBaseline = 'top';
|
|
922
|
+
ctx.fillText(xAxisLabel, (originX + axisEndX) / 2, originY + 25);
|
|
923
|
+
ctx.restore();
|
|
924
|
+
}
|
|
925
|
+
if (yAxisLabel) {
|
|
926
|
+
// Check if bar labels are on the left side - if so, position Y-axis label further left
|
|
927
|
+
let maxBarLabelWidth = 0;
|
|
928
|
+
if (showBarLabels) {
|
|
929
|
+
// Check if default position or any bar has labels on the left
|
|
930
|
+
const hasLeftLabels = barLabelPosition === 'left' ||
|
|
931
|
+
data.some(item => (item.labelPosition ?? barLabelPosition) === 'left');
|
|
932
|
+
if (hasLeftLabels) {
|
|
933
|
+
// Calculate maximum width of bar labels
|
|
934
|
+
ctx.save();
|
|
935
|
+
ctx.font = `${axisLabelFontSize}px Arial`;
|
|
936
|
+
data.forEach(item => {
|
|
937
|
+
const currentLabelPosition = item.labelPosition ?? barLabelPosition;
|
|
938
|
+
if (currentLabelPosition === 'left') {
|
|
939
|
+
const labelWidth = ctx.measureText(item.label).width;
|
|
940
|
+
maxBarLabelWidth = Math.max(maxBarLabelWidth, labelWidth);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
ctx.restore();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
ctx.save();
|
|
947
|
+
ctx.fillStyle = axisLabelColor;
|
|
948
|
+
ctx.font = `${axisLabelFontSize}px Arial`;
|
|
949
|
+
ctx.textAlign = 'center';
|
|
950
|
+
ctx.textBaseline = 'bottom';
|
|
951
|
+
// Position Y-axis label further left if bar labels are on the left
|
|
952
|
+
// Add extra spacing (20px) after the bar labels
|
|
953
|
+
const labelX = originX - maxBarLabelWidth - 20 - 30;
|
|
954
|
+
const labelY = (originY + axisEndY) / 2;
|
|
955
|
+
ctx.translate(labelX, labelY);
|
|
956
|
+
ctx.rotate(-Math.PI / 2);
|
|
957
|
+
ctx.fillText(yAxisLabel, 0, 0);
|
|
958
|
+
ctx.restore();
|
|
959
|
+
}
|
|
960
|
+
// Draw grid lines if enabled
|
|
961
|
+
if (showGrid) {
|
|
962
|
+
drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep, yMin, yMax, yStep, yAxisCustomValues, xAxisCustomValues, gridColor, gridWidth);
|
|
963
|
+
}
|
|
964
|
+
// Draw legend if provided - positioned based on legendPosition option
|
|
965
|
+
if (showLegend && legend && legend.length > 0) {
|
|
966
|
+
const legendSpacing = options.legend?.spacing ?? 20;
|
|
967
|
+
const legendFontSize = options.legend?.fontSize ?? 16;
|
|
968
|
+
const legendTextColor = options.legend?.textColor;
|
|
969
|
+
const legendBorderColor = options.legend?.borderColor;
|
|
970
|
+
const legendBgColor = options.legend?.backgroundColor;
|
|
971
|
+
const legendPadding = options.legend?.padding;
|
|
972
|
+
const legendMaxWidth = options.legend?.maxWidth;
|
|
973
|
+
const legendWrapText = options.legend?.wrapText !== false;
|
|
974
|
+
// Calculate legend position based on legendPosition option
|
|
975
|
+
let legendX, legendY;
|
|
976
|
+
const chartAreaHeight = originY - axisEndY;
|
|
977
|
+
const chartAreaWidth = axisEndX - originX;
|
|
978
|
+
switch (legendPosition) {
|
|
979
|
+
case 'top':
|
|
980
|
+
legendX = (adjustedWidth - legendWidth) / 2; // Centered horizontally
|
|
981
|
+
legendY = paddingTop + titleHeight + minLegendSpacing;
|
|
982
|
+
break;
|
|
983
|
+
case 'bottom':
|
|
984
|
+
legendX = (adjustedWidth - legendWidth) / 2; // Centered horizontally
|
|
985
|
+
legendY = adjustedHeight - paddingBottom - legendHeight - minLegendSpacing;
|
|
986
|
+
break;
|
|
987
|
+
case 'left':
|
|
988
|
+
// Position legend at the very left edge to make maximum room for Y-axis labels
|
|
989
|
+
// The chart area already accounts for legend width + label width, so position legend at leftmost
|
|
990
|
+
legendX = paddingLeft;
|
|
991
|
+
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2; // Vertically centered in chart area
|
|
992
|
+
break;
|
|
993
|
+
case 'right':
|
|
994
|
+
default:
|
|
995
|
+
legendX = axisEndX + minLegendSpacing;
|
|
996
|
+
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2; // Vertically centered in chart area
|
|
997
|
+
break;
|
|
998
|
+
}
|
|
999
|
+
await drawLegendAtPosition(ctx, legend, legendX, legendY, legendFontSize, legendBgColor || backgroundColor, legendTextColor, legendBorderColor, legendPadding, legendMaxWidth, legendWrapText, options.legend?.backgroundGradient, options.legend?.textGradient, options.legend?.textStyle);
|
|
1000
|
+
}
|
|
1001
|
+
// Calculate chart area dimensions (Y-axis area for bars)
|
|
1002
|
+
// chartAreaWidth and baselineX already calculated above when drawing X-axis
|
|
1003
|
+
const chartAreaHeight = originY - axisEndY;
|
|
1004
|
+
// Calculate bar dimensions to fit within Y-axis bounds (between axisEndY and originY)
|
|
1005
|
+
const calculatedBarSpacing = barSpacing ?? 15;
|
|
1006
|
+
const totalSpacing = (data.length - 1) * calculatedBarSpacing;
|
|
1007
|
+
const availableHeight = chartAreaHeight - totalSpacing;
|
|
1008
|
+
const calculatedBarHeight = Math.max(minBarHeight, availableHeight / data.length);
|
|
1009
|
+
const labelsToDraw = [];
|
|
1010
|
+
// Track value label positions per bar (for adjusting bar label positions)
|
|
1011
|
+
const valueLabelPositions = new Map();
|
|
1012
|
+
// First pass: Draw all bars (no labels)
|
|
1013
|
+
data.forEach((item, index) => {
|
|
1014
|
+
// Calculate bar Y position - start from axisEndY (top) and space bars downward
|
|
1015
|
+
// First bar starts after spacing, each subsequent bar: previous position + bar height + spacing
|
|
1016
|
+
const barY = axisEndY + (index * (calculatedBarHeight + calculatedBarSpacing)) + calculatedBarSpacing;
|
|
1017
|
+
const barCenterY = barY + calculatedBarHeight / 2;
|
|
1018
|
+
// Ensure bar stays within Y-axis bounds (between axisEndY and originY)
|
|
1019
|
+
if (barY + calculatedBarHeight > originY) {
|
|
1020
|
+
// Bar would exceed Y-axis bottom - skip it to prevent overflow
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
// Ensure bar doesn't exceed Y-axis bounds
|
|
1024
|
+
if (barY + calculatedBarHeight > originY) {
|
|
1025
|
+
// Adjust if bar would go below originY (Y-axis bottom)
|
|
1026
|
+
return; // Skip this bar if it doesn't fit
|
|
1027
|
+
}
|
|
1028
|
+
// Calculate bar position and dimensions for label positioning (used for all chart types)
|
|
1029
|
+
let barX, barEndX, barLength;
|
|
1030
|
+
// Handle grouped/stacked/lollipop vs standard charts
|
|
1031
|
+
if ((chartType === 'grouped' || chartType === 'stacked' || chartType === 'lollipop') && item.values && item.values.length > 0) {
|
|
1032
|
+
// Grouped or stacked chart
|
|
1033
|
+
const segments = item.values;
|
|
1034
|
+
const numSegments = segments.length;
|
|
1035
|
+
if (chartType === 'grouped') {
|
|
1036
|
+
// Grouped: bars side-by-side (vertically stacked in horizontal chart)
|
|
1037
|
+
const segmentHeight = (calculatedBarHeight - (groupSpacing * (numSegments - 1))) / numSegments;
|
|
1038
|
+
// Calculate overall bar bounds for label positioning (use max segment)
|
|
1039
|
+
const maxSegment = segments.reduce((max, seg) => seg.value > max.value ? seg : max, segments[0]);
|
|
1040
|
+
if (item.xStart !== undefined || item.xEnd !== undefined) {
|
|
1041
|
+
const startValue = item.xStart ?? xMin;
|
|
1042
|
+
const endValue = item.xEnd ?? maxSegment.value;
|
|
1043
|
+
const startRatio = (startValue - xMin) / (xMax - xMin);
|
|
1044
|
+
const endRatio = (endValue - xMin) / (xMax - xMin);
|
|
1045
|
+
barX = originX + startRatio * chartAreaWidth;
|
|
1046
|
+
barEndX = originX + endRatio * chartAreaWidth;
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
// Calculate based on positive/negative
|
|
1050
|
+
if (maxSegment.value >= 0) {
|
|
1051
|
+
const positiveRatio = (maxSegment.value - 0) / (xMax - xMin);
|
|
1052
|
+
barX = baselineX;
|
|
1053
|
+
barEndX = baselineX + positiveRatio * chartAreaWidth;
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
const negativeRatio = (baseline - maxSegment.value) / (xMax - xMin);
|
|
1057
|
+
barX = baselineX - negativeRatio * chartAreaWidth;
|
|
1058
|
+
barEndX = baselineX;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
barLength = Math.abs(barEndX - barX);
|
|
1062
|
+
segments.forEach((segment, segIndex) => {
|
|
1063
|
+
const segY = barY + (segIndex * (segmentHeight + groupSpacing));
|
|
1064
|
+
const segCenterY = segY + segmentHeight / 2;
|
|
1065
|
+
// Calculate segment bar position and length
|
|
1066
|
+
let segBarX, segBarEndX;
|
|
1067
|
+
if (item.xStart !== undefined || item.xEnd !== undefined) {
|
|
1068
|
+
const startValue = item.xStart ?? xMin;
|
|
1069
|
+
const endValue = item.xEnd ?? segment.value;
|
|
1070
|
+
const startRatio = (startValue - xMin) / (xMax - xMin);
|
|
1071
|
+
const endRatio = (endValue - xMin) / (xMax - xMin);
|
|
1072
|
+
segBarX = originX + startRatio * chartAreaWidth;
|
|
1073
|
+
segBarEndX = originX + endRatio * chartAreaWidth;
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
// Calculate bar position based on positive/negative value
|
|
1077
|
+
if (segment.value >= baseline) {
|
|
1078
|
+
const positiveRatio = (segment.value - baseline) / (xMax - xMin);
|
|
1079
|
+
segBarX = baselineX;
|
|
1080
|
+
segBarEndX = baselineX + positiveRatio * chartAreaWidth;
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
const negativeRatio = (baseline - segment.value) / (xMax - xMin);
|
|
1084
|
+
segBarX = baselineX - negativeRatio * chartAreaWidth;
|
|
1085
|
+
segBarEndX = baselineX;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const segBarLength = Math.abs(segBarEndX - segBarX);
|
|
1089
|
+
// Draw segment bar with gradient or color
|
|
1090
|
+
ctx.beginPath();
|
|
1091
|
+
ctx.rect(segBarX, segY, segBarLength, segmentHeight);
|
|
1092
|
+
fillWithGradientOrColor(ctx, segment.gradient || item.gradient, segment.color || item.color || '#4A90E2', '#4A90E2', { x: segBarX, y: segY, w: segBarLength, h: segmentHeight });
|
|
1093
|
+
ctx.fill();
|
|
1094
|
+
// Store value label for later drawing
|
|
1095
|
+
const shouldShowValue = segment.showValue !== undefined ? segment.showValue : showValues;
|
|
1096
|
+
if (shouldShowValue) {
|
|
1097
|
+
labelsToDraw.push({
|
|
1098
|
+
type: 'value',
|
|
1099
|
+
text: segment.value.toString(),
|
|
1100
|
+
x: segment.value >= baseline ? segBarEndX + 5 : segBarX - 5,
|
|
1101
|
+
y: segCenterY,
|
|
1102
|
+
align: segment.value >= baseline ? 'left' : 'right',
|
|
1103
|
+
baseline: 'middle',
|
|
1104
|
+
color: segment.valueColor || valueColor,
|
|
1105
|
+
fontSize: valueFontSize
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
// Stacked: bars on top of each other (horizontally stacked in horizontal chart)
|
|
1112
|
+
let accumulatedLength = 0;
|
|
1113
|
+
segments.forEach((segment, segIndex) => {
|
|
1114
|
+
// For stacked, separate positive and negative segments
|
|
1115
|
+
let segmentLength;
|
|
1116
|
+
let segBarX;
|
|
1117
|
+
if (segment.value >= baseline) {
|
|
1118
|
+
const positiveRatio = (segment.value - baseline) / (xMax - xMin);
|
|
1119
|
+
segmentLength = positiveRatio * chartAreaWidth;
|
|
1120
|
+
segBarX = baselineX + accumulatedLength;
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
const negativeRatio = (baseline - segment.value) / (xMax - xMin);
|
|
1124
|
+
segmentLength = negativeRatio * chartAreaWidth;
|
|
1125
|
+
segBarX = baselineX - accumulatedLength - segmentLength;
|
|
1126
|
+
}
|
|
1127
|
+
// Draw segment bar
|
|
1128
|
+
ctx.fillStyle = segment.color || item.color || '#4A90E2';
|
|
1129
|
+
ctx.fillRect(segBarX, barY, segmentLength, calculatedBarHeight);
|
|
1130
|
+
// Store value label for later drawing
|
|
1131
|
+
const shouldShowValue = segment.showValue !== undefined ? segment.showValue : showValues;
|
|
1132
|
+
if (shouldShowValue && segmentLength > valueFontSize + 10) {
|
|
1133
|
+
labelsToDraw.push({
|
|
1134
|
+
type: 'value',
|
|
1135
|
+
text: segment.value.toString(),
|
|
1136
|
+
x: segBarX + segmentLength / 2,
|
|
1137
|
+
y: barCenterY,
|
|
1138
|
+
align: 'center',
|
|
1139
|
+
baseline: 'middle',
|
|
1140
|
+
color: segment.valueColor || valueColor,
|
|
1141
|
+
fontSize: valueFontSize
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
accumulatedLength += segmentLength;
|
|
1145
|
+
});
|
|
1146
|
+
// Calculate overall bar bounds for label positioning
|
|
1147
|
+
barX = originX;
|
|
1148
|
+
barEndX = originX + accumulatedLength;
|
|
1149
|
+
barLength = accumulatedLength;
|
|
1150
|
+
// Store total value label for later drawing
|
|
1151
|
+
const totalValue = segments.reduce((sum, seg) => sum + seg.value, 0);
|
|
1152
|
+
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1153
|
+
if (shouldShowValue) {
|
|
1154
|
+
// Calculate total position
|
|
1155
|
+
const totalPositive = segments.filter(s => s.value >= 0).reduce((sum, s) => sum + s.value, 0);
|
|
1156
|
+
const totalNegative = segments.filter(s => s.value < 0).reduce((sum, s) => sum + Math.abs(s.value), 0);
|
|
1157
|
+
const totalPositiveLength = (totalPositive / (xMax - xMin)) * chartAreaWidth;
|
|
1158
|
+
const totalNegativeLength = (totalNegative / (xMax - xMin)) * chartAreaWidth;
|
|
1159
|
+
const totalX = totalValue >= 0
|
|
1160
|
+
? baselineX + totalPositiveLength + 5
|
|
1161
|
+
: baselineX - totalNegativeLength - 5;
|
|
1162
|
+
labelsToDraw.push({
|
|
1163
|
+
type: 'value',
|
|
1164
|
+
text: totalValue.toString(),
|
|
1165
|
+
x: totalX,
|
|
1166
|
+
y: barCenterY,
|
|
1167
|
+
align: totalValue >= 0 ? 'left' : 'right',
|
|
1168
|
+
baseline: 'middle',
|
|
1169
|
+
color: item.valueColor || valueColor,
|
|
1170
|
+
fontSize: valueFontSize
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
else if (chartType === 'lollipop') {
|
|
1176
|
+
// Lollipop chart: line with dot at end (horizontal)
|
|
1177
|
+
const value = item.value ?? baseline;
|
|
1178
|
+
// Calculate value X position
|
|
1179
|
+
let valueX;
|
|
1180
|
+
if (value >= baseline) {
|
|
1181
|
+
// Value to the right of baseline
|
|
1182
|
+
const positiveRatio = (value - baseline) / (xMax - xMin);
|
|
1183
|
+
valueX = baselineX + positiveRatio * chartAreaWidth;
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
// Value to the left of baseline
|
|
1187
|
+
const negativeRatio = (baseline - value) / (xMax - xMin);
|
|
1188
|
+
valueX = baselineX - negativeRatio * chartAreaWidth;
|
|
1189
|
+
}
|
|
1190
|
+
// Draw horizontal line from baseline to value position
|
|
1191
|
+
ctx.save();
|
|
1192
|
+
ctx.strokeStyle = item.color || '#4A90E2';
|
|
1193
|
+
ctx.lineWidth = lollipopLineWidth;
|
|
1194
|
+
ctx.beginPath();
|
|
1195
|
+
ctx.moveTo(baselineX, barCenterY);
|
|
1196
|
+
ctx.lineTo(valueX, barCenterY);
|
|
1197
|
+
ctx.stroke();
|
|
1198
|
+
// Draw dot/circle at value position
|
|
1199
|
+
ctx.fillStyle = item.color || '#4A90E2';
|
|
1200
|
+
ctx.beginPath();
|
|
1201
|
+
ctx.arc(valueX, barCenterY, lollipopDotSize / 2, 0, Math.PI * 2);
|
|
1202
|
+
ctx.fill();
|
|
1203
|
+
// Draw dot border for better visibility
|
|
1204
|
+
ctx.strokeStyle = item.color || '#4A90E2';
|
|
1205
|
+
ctx.lineWidth = 1;
|
|
1206
|
+
ctx.stroke();
|
|
1207
|
+
ctx.restore();
|
|
1208
|
+
// Store value label for later drawing
|
|
1209
|
+
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1210
|
+
if (shouldShowValue) {
|
|
1211
|
+
// Store value label position for this bar (for adjusting bar label position)
|
|
1212
|
+
if (value >= baseline) {
|
|
1213
|
+
valueLabelPositions.set(index, { x: valueX + lollipopDotSize / 2 + 5, fontSize: valueFontSize, align: 'left' });
|
|
1214
|
+
}
|
|
1215
|
+
labelsToDraw.push({
|
|
1216
|
+
type: 'value',
|
|
1217
|
+
text: value.toString(),
|
|
1218
|
+
x: value >= baseline ? valueX + lollipopDotSize / 2 + 5 : valueX - lollipopDotSize / 2 - 5,
|
|
1219
|
+
y: barCenterY,
|
|
1220
|
+
align: value >= baseline ? 'left' : 'right',
|
|
1221
|
+
baseline: 'middle',
|
|
1222
|
+
color: item.valueColor || valueColor,
|
|
1223
|
+
fontSize: valueFontSize
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
// Set bar bounds for label positioning
|
|
1227
|
+
barX = baselineX;
|
|
1228
|
+
barEndX = valueX;
|
|
1229
|
+
barLength = Math.abs(barEndX - barX);
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
// Standard chart: single bar
|
|
1233
|
+
// Calculate bar position and length
|
|
1234
|
+
// If xStart/xEnd are provided, use them for bar range; otherwise use value
|
|
1235
|
+
if (item.xStart !== undefined || item.xEnd !== undefined) {
|
|
1236
|
+
const startValue = item.xStart ?? xMin;
|
|
1237
|
+
const endValue = item.xEnd ?? (item.value ?? 0);
|
|
1238
|
+
const startRatio = (startValue - xMin) / (xMax - xMin);
|
|
1239
|
+
const endRatio = (endValue - xMin) / (xMax - xMin);
|
|
1240
|
+
barX = originX + startRatio * chartAreaWidth;
|
|
1241
|
+
barEndX = originX + endRatio * chartAreaWidth;
|
|
1242
|
+
}
|
|
1243
|
+
else {
|
|
1244
|
+
// Use value as end position, handle relative to baseline
|
|
1245
|
+
const value = item.value ?? baseline;
|
|
1246
|
+
if (value >= baseline) {
|
|
1247
|
+
const positiveRatio = (value - baseline) / (xMax - xMin);
|
|
1248
|
+
barX = baselineX;
|
|
1249
|
+
barEndX = baselineX + positiveRatio * chartAreaWidth;
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
const negativeRatio = (baseline - value) / (xMax - xMin);
|
|
1253
|
+
barX = baselineX - negativeRatio * chartAreaWidth;
|
|
1254
|
+
barEndX = baselineX;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
barLength = barEndX - barX;
|
|
1258
|
+
// Draw horizontal bar
|
|
1259
|
+
ctx.beginPath();
|
|
1260
|
+
ctx.rect(barX, barY, barLength, calculatedBarHeight);
|
|
1261
|
+
fillWithGradientOrColor(ctx, item.gradient, item.color || '#4A90E2', '#4A90E2', { x: barX, y: barY, w: barLength, h: calculatedBarHeight });
|
|
1262
|
+
ctx.fill();
|
|
1263
|
+
// Store value label for later drawing
|
|
1264
|
+
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1265
|
+
if (shouldShowValue) {
|
|
1266
|
+
const value = item.value ?? baseline;
|
|
1267
|
+
const valueLabelX = value >= baseline ? barEndX + 5 : barX - 5;
|
|
1268
|
+
const valueLabelAlign = value >= baseline ? 'left' : 'right';
|
|
1269
|
+
// Store value label position for this bar (for adjusting bar label position)
|
|
1270
|
+
if (value >= baseline) {
|
|
1271
|
+
valueLabelPositions.set(index, { x: valueLabelX, fontSize: valueFontSize, align: valueLabelAlign });
|
|
1272
|
+
}
|
|
1273
|
+
labelsToDraw.push({
|
|
1274
|
+
type: 'value',
|
|
1275
|
+
text: value.toString(),
|
|
1276
|
+
x: valueLabelX,
|
|
1277
|
+
y: barCenterY,
|
|
1278
|
+
align: valueLabelAlign,
|
|
1279
|
+
baseline: 'middle',
|
|
1280
|
+
color: item.valueColor || valueColor,
|
|
1281
|
+
fontSize: valueFontSize
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
// Store bar label information for later drawing
|
|
1286
|
+
if (showBarLabels) {
|
|
1287
|
+
let labelX, labelY;
|
|
1288
|
+
let textAlign = 'right';
|
|
1289
|
+
let textBaseline = 'middle';
|
|
1290
|
+
const currentLabelPosition = item.labelPosition ?? barLabelPosition;
|
|
1291
|
+
switch (currentLabelPosition) {
|
|
1292
|
+
case 'left':
|
|
1293
|
+
labelX = originX - 5;
|
|
1294
|
+
labelY = barCenterY;
|
|
1295
|
+
textAlign = 'right';
|
|
1296
|
+
textBaseline = 'middle';
|
|
1297
|
+
break;
|
|
1298
|
+
case 'right':
|
|
1299
|
+
labelX = barEndX + 5;
|
|
1300
|
+
labelY = barCenterY;
|
|
1301
|
+
// Check if there's a value label at the right - if so, position bar label to the right of it
|
|
1302
|
+
const valueLabelInfo = valueLabelPositions.get(index);
|
|
1303
|
+
if (valueLabelInfo && valueLabelInfo.align === 'left') {
|
|
1304
|
+
// Value label is at right, so position bar label to the right of it
|
|
1305
|
+
// Calculate spacing: value label width + gap
|
|
1306
|
+
ctx.save();
|
|
1307
|
+
ctx.font = `${valueLabelInfo.fontSize}px Arial`;
|
|
1308
|
+
const valueLabelWidth = ctx.measureText((item.value ?? baseline).toString()).width;
|
|
1309
|
+
ctx.restore();
|
|
1310
|
+
const spacing = 5; // Gap between value and bar label
|
|
1311
|
+
labelX = valueLabelInfo.x + valueLabelWidth + spacing;
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
labelX = barEndX + 5;
|
|
1315
|
+
}
|
|
1316
|
+
textAlign = 'left';
|
|
1317
|
+
textBaseline = 'middle';
|
|
1318
|
+
break;
|
|
1319
|
+
case 'top':
|
|
1320
|
+
labelX = barX + barLength / 2;
|
|
1321
|
+
// Check if there's a value label - for horizontal charts, value labels are at the end (right side)
|
|
1322
|
+
// So 'top' bar label won't conflict with value labels (they're on different axes)
|
|
1323
|
+
// But we still need to check if value is shown and adjust if needed
|
|
1324
|
+
// For horizontal charts, 'top' means above the bar, value labels are at the end
|
|
1325
|
+
// So no conflict, but if we want to be safe, we can check
|
|
1326
|
+
labelY = barY - 5;
|
|
1327
|
+
textAlign = 'center';
|
|
1328
|
+
textBaseline = 'bottom';
|
|
1329
|
+
break;
|
|
1330
|
+
case 'bottom':
|
|
1331
|
+
labelX = barX + barLength / 2;
|
|
1332
|
+
labelY = barY + calculatedBarHeight + 5;
|
|
1333
|
+
textAlign = 'center';
|
|
1334
|
+
textBaseline = 'top';
|
|
1335
|
+
break;
|
|
1336
|
+
case 'inside':
|
|
1337
|
+
labelX = barX + barLength / 2;
|
|
1338
|
+
labelY = barCenterY;
|
|
1339
|
+
textAlign = 'center';
|
|
1340
|
+
textBaseline = 'middle';
|
|
1341
|
+
break;
|
|
1342
|
+
default:
|
|
1343
|
+
labelX = originX - 5;
|
|
1344
|
+
labelY = barCenterY;
|
|
1345
|
+
textAlign = 'right';
|
|
1346
|
+
textBaseline = 'middle';
|
|
1347
|
+
}
|
|
1348
|
+
// Calculate label color (for 'inside' position, check if bar is dark)
|
|
1349
|
+
let labelColor = item.labelColor || '#000000';
|
|
1350
|
+
if (currentLabelPosition === 'inside') {
|
|
1351
|
+
const barColor = item.color || '#4A90E2';
|
|
1352
|
+
const isDark = barColor === '#000000' || barColor.toLowerCase().includes('dark') ||
|
|
1353
|
+
(barColor.startsWith('#') && parseInt(barColor.slice(1, 3), 16) < 128);
|
|
1354
|
+
labelColor = isDark ? '#FFFFFF' : (item.labelColor || '#000000');
|
|
1355
|
+
}
|
|
1356
|
+
labelsToDraw.push({
|
|
1357
|
+
type: 'bar',
|
|
1358
|
+
text: item.label,
|
|
1359
|
+
x: labelX,
|
|
1360
|
+
y: labelY,
|
|
1361
|
+
align: textAlign,
|
|
1362
|
+
baseline: textBaseline,
|
|
1363
|
+
color: labelColor,
|
|
1364
|
+
fontSize: axisLabelFontSize
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
// Second pass: Draw all labels (values and bar labels) on top of everything
|
|
1369
|
+
for (const label of labelsToDraw) {
|
|
1370
|
+
ctx.save();
|
|
1371
|
+
ctx.textAlign = label.align;
|
|
1372
|
+
ctx.textBaseline = label.baseline;
|
|
1373
|
+
// Determine text style and gradient based on label type
|
|
1374
|
+
let textStyle;
|
|
1375
|
+
let textGradient;
|
|
1376
|
+
if (label.type === 'bar') {
|
|
1377
|
+
textStyle = options.labels?.barLabelDefaults?.textStyle || label.textStyle;
|
|
1378
|
+
textGradient = options.labels?.barLabelDefaults?.gradient || label.gradient;
|
|
1379
|
+
}
|
|
1380
|
+
else if (label.type === 'value') {
|
|
1381
|
+
textStyle = options.labels?.valueLabelDefaults?.textStyle || label.textStyle;
|
|
1382
|
+
textGradient = options.labels?.valueLabelDefaults?.gradient || label.gradient;
|
|
1383
|
+
}
|
|
1384
|
+
await renderEnhancedText(ctx, label.text, label.x, label.y, textStyle, label.fontSize, label.color, textGradient);
|
|
1385
|
+
ctx.restore();
|
|
1386
|
+
}
|
|
1387
|
+
return canvas.toBuffer('image/png');
|
|
1388
|
+
}
|
|
1389
|
+
//# sourceMappingURL=horizontalbarchart.js.map
|