apexify.js 5.2.2 → 5.2.6
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 +173 -4
- package/README.md +66 -0
- package/dist/cjs/Canvas/ApexPainter.d.ts +191 -4
- package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/cjs/Canvas/ApexPainter.js +201 -96
- package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
- package/dist/cjs/Canvas/extended/CanvasCreator.d.ts.map +1 -1
- package/dist/cjs/Canvas/extended/CanvasCreator.js +3 -18
- package/dist/cjs/Canvas/extended/CanvasCreator.js.map +1 -1
- package/dist/cjs/Canvas/extended/GIFCreator.d.ts +15 -3
- package/dist/cjs/Canvas/extended/GIFCreator.d.ts.map +1 -1
- package/dist/cjs/Canvas/extended/GIFCreator.js +107 -9
- package/dist/cjs/Canvas/extended/GIFCreator.js.map +1 -1
- package/dist/cjs/Canvas/extended/HitDetectionCreator.d.ts +148 -0
- package/dist/cjs/Canvas/extended/HitDetectionCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/HitDetectionCreator.js +294 -0
- package/dist/cjs/Canvas/extended/HitDetectionCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/ImageCreator.d.ts +3 -2
- package/dist/cjs/Canvas/extended/ImageCreator.d.ts.map +1 -1
- package/dist/cjs/Canvas/extended/ImageCreator.js +43 -4
- package/dist/cjs/Canvas/extended/ImageCreator.js.map +1 -1
- package/dist/cjs/Canvas/extended/Path2DCreator.d.ts +166 -0
- package/dist/cjs/Canvas/extended/Path2DCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/Path2DCreator.js +246 -0
- package/dist/cjs/Canvas/extended/Path2DCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/PixelDataCreator.d.ts +69 -0
- package/dist/cjs/Canvas/extended/PixelDataCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/PixelDataCreator.js +232 -0
- package/dist/cjs/Canvas/extended/PixelDataCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/TextCreator.d.ts.map +1 -1
- package/dist/cjs/Canvas/extended/TextCreator.js +0 -7
- package/dist/cjs/Canvas/extended/TextCreator.js.map +1 -1
- package/dist/cjs/Canvas/extended/TextMetricsCreator.d.ts +18 -0
- package/dist/cjs/Canvas/extended/TextMetricsCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/TextMetricsCreator.js +194 -0
- package/dist/cjs/Canvas/extended/TextMetricsCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/VideoCreator.d.ts.map +1 -1
- package/dist/cjs/Canvas/extended/VideoCreator.js +1 -42
- package/dist/cjs/Canvas/extended/VideoCreator.js.map +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.js +2 -15
- package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/barchart.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/barchart.js +32 -277
- package/dist/cjs/Canvas/utils/Charts/barchart.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.js +0 -41
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js +42 -241
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/linechart.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/linechart.js +16 -148
- package/dist/cjs/Canvas/utils/Charts/linechart.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/piechart.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/piechart.js +21 -117
- package/dist/cjs/Canvas/utils/Charts/piechart.js.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/advancedLines.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/advancedLines.js +3 -16
- package/dist/cjs/Canvas/utils/Custom/advancedLines.js.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/customLines.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Custom/customLines.js +0 -7
- package/dist/cjs/Canvas/utils/Custom/customLines.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/batchOperations.js +0 -4
- package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/conversion.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/conversion.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 +0 -19
- package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageCompression.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageCompression.js +0 -18
- package/dist/cjs/Canvas/utils/General/imageCompression.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageStitching.js +0 -14
- package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageEffects.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageEffects.js +8 -16
- package/dist/cjs/Canvas/utils/Image/imageEffects.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageFilters.js +12 -30
- package/dist/cjs/Canvas/utils/Image/imageFilters.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageMasking.js +0 -22
- package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.js +2 -37
- package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/professionalImageFilters.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/professionalImageFilters.js +6 -23
- package/dist/cjs/Canvas/utils/Image/professionalImageFilters.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.js +7 -16
- package/dist/cjs/Canvas/utils/Image/simpleProfessionalFilters.js.map +1 -1
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js +1 -16
- package/dist/cjs/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Shapes/shapes.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Shapes/shapes.js +120 -39
- package/dist/cjs/Canvas/utils/Shapes/shapes.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +5 -58
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js +1 -4
- package/dist/cjs/Canvas/utils/Texts/textPathRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.js +0 -18
- package/dist/cjs/Canvas/utils/Texts/textProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Video/videoHelpers.js +9 -110
- package/dist/cjs/Canvas/utils/Video/videoHelpers.js.map +1 -1
- package/dist/cjs/Canvas/utils/types.d.ts +136 -1
- 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 -2
- package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/utils.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/Canvas/ApexPainter.d.ts +191 -4
- package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/esm/Canvas/ApexPainter.js +201 -96
- package/dist/esm/Canvas/ApexPainter.js.map +1 -1
- package/dist/esm/Canvas/extended/CanvasCreator.d.ts.map +1 -1
- package/dist/esm/Canvas/extended/CanvasCreator.js +3 -18
- package/dist/esm/Canvas/extended/CanvasCreator.js.map +1 -1
- package/dist/esm/Canvas/extended/GIFCreator.d.ts +15 -3
- package/dist/esm/Canvas/extended/GIFCreator.d.ts.map +1 -1
- package/dist/esm/Canvas/extended/GIFCreator.js +107 -9
- package/dist/esm/Canvas/extended/GIFCreator.js.map +1 -1
- package/dist/esm/Canvas/extended/HitDetectionCreator.d.ts +148 -0
- package/dist/esm/Canvas/extended/HitDetectionCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/HitDetectionCreator.js +294 -0
- package/dist/esm/Canvas/extended/HitDetectionCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/ImageCreator.d.ts +3 -2
- package/dist/esm/Canvas/extended/ImageCreator.d.ts.map +1 -1
- package/dist/esm/Canvas/extended/ImageCreator.js +43 -4
- package/dist/esm/Canvas/extended/ImageCreator.js.map +1 -1
- package/dist/esm/Canvas/extended/Path2DCreator.d.ts +166 -0
- package/dist/esm/Canvas/extended/Path2DCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/Path2DCreator.js +246 -0
- package/dist/esm/Canvas/extended/Path2DCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/PixelDataCreator.d.ts +69 -0
- package/dist/esm/Canvas/extended/PixelDataCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/PixelDataCreator.js +232 -0
- package/dist/esm/Canvas/extended/PixelDataCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/TextCreator.d.ts.map +1 -1
- package/dist/esm/Canvas/extended/TextCreator.js +0 -7
- package/dist/esm/Canvas/extended/TextCreator.js.map +1 -1
- package/dist/esm/Canvas/extended/TextMetricsCreator.d.ts +18 -0
- package/dist/esm/Canvas/extended/TextMetricsCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/TextMetricsCreator.js +194 -0
- package/dist/esm/Canvas/extended/TextMetricsCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/VideoCreator.d.ts.map +1 -1
- package/dist/esm/Canvas/extended/VideoCreator.js +1 -42
- package/dist/esm/Canvas/extended/VideoCreator.js.map +1 -1
- package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Background/bg.js +2 -15
- package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/barchart.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Charts/barchart.js +32 -277
- package/dist/esm/Canvas/utils/Charts/barchart.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Charts/comparisonchart.js +0 -41
- package/dist/esm/Canvas/utils/Charts/comparisonchart.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js +42 -241
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/linechart.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Charts/linechart.js +16 -148
- package/dist/esm/Canvas/utils/Charts/linechart.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/piechart.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Charts/piechart.js +21 -117
- package/dist/esm/Canvas/utils/Charts/piechart.js.map +1 -1
- package/dist/esm/Canvas/utils/Custom/advancedLines.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Custom/advancedLines.js +3 -16
- package/dist/esm/Canvas/utils/Custom/advancedLines.js.map +1 -1
- package/dist/esm/Canvas/utils/Custom/customLines.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Custom/customLines.js +0 -7
- package/dist/esm/Canvas/utils/Custom/customLines.js.map +1 -1
- package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/batchOperations.js +0 -4
- package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -1
- package/dist/esm/Canvas/utils/General/conversion.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/conversion.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 +0 -19
- package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/esm/Canvas/utils/General/imageCompression.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/imageCompression.js +0 -18
- package/dist/esm/Canvas/utils/General/imageCompression.js.map +1 -1
- package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/imageStitching.js +0 -14
- package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageEffects.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageEffects.js +8 -16
- package/dist/esm/Canvas/utils/Image/imageEffects.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageFilters.js +12 -30
- package/dist/esm/Canvas/utils/Image/imageFilters.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageMasking.js +0 -22
- package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.js +2 -37
- package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/professionalImageFilters.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/professionalImageFilters.js +6 -23
- package/dist/esm/Canvas/utils/Image/professionalImageFilters.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.js +7 -16
- package/dist/esm/Canvas/utils/Image/simpleProfessionalFilters.js.map +1 -1
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js +1 -16
- package/dist/esm/Canvas/utils/Patterns/enhancedPatternRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Shapes/shapes.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Shapes/shapes.js +120 -39
- package/dist/esm/Canvas/utils/Shapes/shapes.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +5 -58
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.js +1 -4
- package/dist/esm/Canvas/utils/Texts/textPathRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.js +0 -18
- package/dist/esm/Canvas/utils/Texts/textProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Video/videoHelpers.js +9 -110
- package/dist/esm/Canvas/utils/Video/videoHelpers.js.map +1 -1
- package/dist/esm/Canvas/utils/types.d.ts +136 -1
- 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 -2
- package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/utils.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +1 -3
|
@@ -13,7 +13,6 @@ const imageProperties_1 = require("../Image/imageProperties");
|
|
|
13
13
|
*/
|
|
14
14
|
async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textGradient) {
|
|
15
15
|
ctx.save();
|
|
16
|
-
// Preserve text alignment settings
|
|
17
16
|
const savedTextAlign = ctx.textAlign;
|
|
18
17
|
const savedTextBaseline = ctx.textBaseline;
|
|
19
18
|
const effectiveFontSize = fontSize || style?.fontSize || 16;
|
|
@@ -25,10 +24,8 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
25
24
|
fontString += 'italic ';
|
|
26
25
|
fontString += `${effectiveFontSize}px "${fontFamily}"`;
|
|
27
26
|
ctx.font = fontString;
|
|
28
|
-
// Restore text alignment to ensure correct positioning
|
|
29
27
|
ctx.textAlign = savedTextAlign;
|
|
30
28
|
ctx.textBaseline = savedTextBaseline;
|
|
31
|
-
// Register custom font if provided
|
|
32
29
|
if (style?.fontPath && style?.fontName) {
|
|
33
30
|
try {
|
|
34
31
|
const { GlobalFonts } = await import('@napi-rs/canvas');
|
|
@@ -41,7 +38,6 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
41
38
|
console.warn(`Failed to register font: ${style.fontPath}`, error);
|
|
42
39
|
}
|
|
43
40
|
}
|
|
44
|
-
// Apply shadow
|
|
45
41
|
if (style?.shadow) {
|
|
46
42
|
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.5)';
|
|
47
43
|
ctx.shadowOffsetX = style.shadow.offsetX || 2;
|
|
@@ -51,7 +47,6 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
51
47
|
ctx.globalAlpha = style.shadow.opacity;
|
|
52
48
|
}
|
|
53
49
|
}
|
|
54
|
-
// Set fill style (gradient or color)
|
|
55
50
|
if (textGradient) {
|
|
56
51
|
const metrics = ctx.measureText(text);
|
|
57
52
|
ctx.fillStyle = (0, imageProperties_1.createGradientFill)(ctx, textGradient, {
|
|
@@ -61,9 +56,7 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
61
56
|
else if (color) {
|
|
62
57
|
ctx.fillStyle = color;
|
|
63
58
|
}
|
|
64
|
-
// Draw text
|
|
65
59
|
ctx.fillText(text, x, y);
|
|
66
|
-
// Apply stroke
|
|
67
60
|
if (style?.stroke) {
|
|
68
61
|
ctx.strokeStyle = style.stroke.color || '#000000';
|
|
69
62
|
ctx.lineWidth = style.stroke.width || 1;
|
|
@@ -75,7 +68,6 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
75
68
|
}
|
|
76
69
|
ctx.strokeText(text, x, y);
|
|
77
70
|
}
|
|
78
|
-
// Reset shadow and alpha
|
|
79
71
|
ctx.shadowColor = 'transparent';
|
|
80
72
|
ctx.shadowOffsetX = 0;
|
|
81
73
|
ctx.shadowOffsetY = 0;
|
|
@@ -99,10 +91,8 @@ function fillWithGradientOrColor(ctx, gradient, color, defaultColor = '#000000',
|
|
|
99
91
|
*/
|
|
100
92
|
function drawBar(ctx, x, y, width, height, color, gradient, opacity, shadow, stroke, globalShadow, globalStroke) {
|
|
101
93
|
ctx.save();
|
|
102
|
-
// Apply opacity
|
|
103
94
|
const effectiveOpacity = opacity !== undefined ? opacity : 1;
|
|
104
95
|
ctx.globalAlpha = effectiveOpacity;
|
|
105
|
-
// Apply shadow (segment/item shadow takes precedence over global)
|
|
106
96
|
const effectiveShadow = shadow || globalShadow;
|
|
107
97
|
if (effectiveShadow) {
|
|
108
98
|
ctx.shadowColor = effectiveShadow.color || 'rgba(0,0,0,0.3)';
|
|
@@ -110,7 +100,6 @@ function drawBar(ctx, x, y, width, height, color, gradient, opacity, shadow, str
|
|
|
110
100
|
ctx.shadowOffsetY = effectiveShadow.offsetY ?? 2;
|
|
111
101
|
ctx.shadowBlur = effectiveShadow.blur ?? 4;
|
|
112
102
|
}
|
|
113
|
-
// Draw bar fill
|
|
114
103
|
ctx.beginPath();
|
|
115
104
|
ctx.rect(x, y, width, height);
|
|
116
105
|
if (gradient) {
|
|
@@ -120,14 +109,12 @@ function drawBar(ctx, x, y, width, height, color, gradient, opacity, shadow, str
|
|
|
120
109
|
ctx.fillStyle = color;
|
|
121
110
|
}
|
|
122
111
|
ctx.fill();
|
|
123
|
-
// Reset shadow before stroke
|
|
124
112
|
if (effectiveShadow) {
|
|
125
113
|
ctx.shadowColor = 'transparent';
|
|
126
114
|
ctx.shadowOffsetX = 0;
|
|
127
115
|
ctx.shadowOffsetY = 0;
|
|
128
116
|
ctx.shadowBlur = 0;
|
|
129
117
|
}
|
|
130
|
-
// Apply stroke (segment/item stroke takes precedence over global)
|
|
131
118
|
const effectiveStroke = stroke || globalStroke;
|
|
132
119
|
if (effectiveStroke && effectiveStroke.width && effectiveStroke.width > 0) {
|
|
133
120
|
ctx.beginPath();
|
|
@@ -174,62 +161,48 @@ function drawYAxisTicks(ctx, originX, originY, axisEndY, minValue, maxValue, ste
|
|
|
174
161
|
ctx.textBaseline = 'middle';
|
|
175
162
|
const chartHeight = originY - axisEndY;
|
|
176
163
|
if (customValues && customValues.length > 0) {
|
|
177
|
-
// Use custom Y-axis values
|
|
178
164
|
const actualMin = Math.min(...customValues);
|
|
179
165
|
const actualMax = Math.max(...customValues);
|
|
180
166
|
const range = actualMax - actualMin;
|
|
181
|
-
// Always position ticks based on their actual values
|
|
182
|
-
// valueSpacing is used only to prevent label overlap (skip labels that are too close)
|
|
183
167
|
let lastLabelY = Infinity;
|
|
184
|
-
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30;
|
|
168
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30;
|
|
185
169
|
customValues.forEach((value) => {
|
|
186
170
|
const y = originY - ((value - actualMin) / range) * chartHeight;
|
|
187
|
-
// Check if this label would overlap with the previous one
|
|
188
171
|
if (Math.abs(y - lastLabelY) < minLabelSpacing) {
|
|
189
|
-
// Skip this label to prevent overlap, but still draw the tick mark
|
|
190
172
|
ctx.beginPath();
|
|
191
173
|
ctx.moveTo(originX - 5, y);
|
|
192
174
|
ctx.lineTo(originX, y);
|
|
193
175
|
ctx.stroke();
|
|
194
176
|
return;
|
|
195
177
|
}
|
|
196
|
-
// Draw tick mark
|
|
197
178
|
ctx.beginPath();
|
|
198
179
|
ctx.moveTo(originX - 5, y);
|
|
199
180
|
ctx.lineTo(originX, y);
|
|
200
181
|
ctx.stroke();
|
|
201
|
-
// Draw label
|
|
202
182
|
ctx.fillText(value.toFixed(1), originX - 10, y);
|
|
203
|
-
lastLabelY = y;
|
|
183
|
+
lastLabelY = y;
|
|
204
184
|
});
|
|
205
185
|
}
|
|
206
186
|
else {
|
|
207
|
-
// Use regular step-based ticks
|
|
208
187
|
const range = maxValue - minValue;
|
|
209
|
-
// Always position ticks based on their actual values
|
|
210
|
-
// valueSpacing is used only to prevent label overlap (skip labels that are too close)
|
|
211
188
|
let lastLabelY = Infinity;
|
|
212
|
-
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30;
|
|
189
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30;
|
|
213
190
|
for (let value = minValue; value <= maxValue; value += step) {
|
|
214
191
|
const y = originY - ((value - minValue) / range) * chartHeight;
|
|
215
|
-
// Check if this label would overlap with the previous one
|
|
216
192
|
if (Math.abs(y - lastLabelY) < minLabelSpacing && value > minValue) {
|
|
217
|
-
// Skip this label to prevent overlap, but still draw the tick mark
|
|
218
193
|
ctx.beginPath();
|
|
219
194
|
ctx.moveTo(originX - 5, y);
|
|
220
195
|
ctx.lineTo(originX, y);
|
|
221
196
|
ctx.stroke();
|
|
222
197
|
continue;
|
|
223
198
|
}
|
|
224
|
-
// Draw tick mark
|
|
225
199
|
ctx.beginPath();
|
|
226
200
|
ctx.moveTo(originX - 5, y);
|
|
227
201
|
ctx.lineTo(originX, y);
|
|
228
202
|
ctx.stroke();
|
|
229
|
-
// Draw label
|
|
230
203
|
const labelText = value.toFixed(1);
|
|
231
204
|
ctx.fillText(labelText, originX - 10, y);
|
|
232
|
-
lastLabelY = y;
|
|
205
|
+
lastLabelY = y;
|
|
233
206
|
}
|
|
234
207
|
}
|
|
235
208
|
ctx.restore();
|
|
@@ -245,98 +218,76 @@ function drawXAxisTicks(ctx, originX, originY, axisEndX, minValue, maxValue, ste
|
|
|
245
218
|
ctx.textBaseline = 'top';
|
|
246
219
|
const chartWidth = axisEndX - originX;
|
|
247
220
|
if (customValues && customValues.length > 0) {
|
|
248
|
-
// Use custom X-axis values
|
|
249
221
|
if (valueSpacing && valueSpacing > 0) {
|
|
250
|
-
// Use specified spacing - position ticks with exact pixel spacing
|
|
251
222
|
let currentX = originX;
|
|
252
223
|
customValues.forEach((value, index) => {
|
|
253
224
|
if (index === 0) {
|
|
254
225
|
currentX = originX;
|
|
255
226
|
}
|
|
256
227
|
else {
|
|
257
|
-
currentX += valueSpacing;
|
|
228
|
+
currentX += valueSpacing;
|
|
258
229
|
}
|
|
259
|
-
// Clamp to chart area
|
|
260
230
|
if (currentX >= originX && currentX <= axisEndX) {
|
|
261
|
-
// Draw tick mark
|
|
262
231
|
ctx.beginPath();
|
|
263
232
|
ctx.moveTo(currentX, originY);
|
|
264
233
|
ctx.lineTo(currentX, originY + 5);
|
|
265
234
|
ctx.stroke();
|
|
266
|
-
// Draw label
|
|
267
235
|
ctx.fillText(value.toString(), currentX, originY + 10);
|
|
268
236
|
}
|
|
269
237
|
});
|
|
270
238
|
}
|
|
271
239
|
else {
|
|
272
|
-
// Use index-based positioning (original behavior)
|
|
273
|
-
// But check for label overlap and skip labels if they're too close
|
|
274
240
|
const totalValues = customValues.length;
|
|
275
241
|
const divisor = totalValues > 1 ? totalValues - 1 : 1;
|
|
276
242
|
let lastLabelX = -Infinity;
|
|
277
|
-
const minLabelSpacing = 40;
|
|
243
|
+
const minLabelSpacing = 40;
|
|
278
244
|
customValues.forEach((value, index) => {
|
|
279
|
-
// Position based on index in the array, not the numeric value
|
|
280
245
|
const x = originX + (index / divisor) * chartWidth;
|
|
281
246
|
const labelText = value.toString();
|
|
282
247
|
const labelWidth = ctx.measureText(labelText).width;
|
|
283
|
-
// Check if this label would overlap with the previous one
|
|
284
248
|
if (x - lastLabelX < minLabelSpacing && index > 0) {
|
|
285
|
-
// Skip this label to prevent overlap
|
|
286
249
|
return;
|
|
287
250
|
}
|
|
288
|
-
// Draw tick mark
|
|
289
251
|
ctx.beginPath();
|
|
290
252
|
ctx.moveTo(x, originY);
|
|
291
253
|
ctx.lineTo(x, originY + 5);
|
|
292
254
|
ctx.stroke();
|
|
293
|
-
// Draw label
|
|
294
255
|
ctx.fillText(labelText, x, originY + 10);
|
|
295
|
-
lastLabelX = x + labelWidth / 2;
|
|
256
|
+
lastLabelX = x + labelWidth / 2;
|
|
296
257
|
});
|
|
297
258
|
}
|
|
298
259
|
}
|
|
299
260
|
else {
|
|
300
|
-
// Use regular step-based ticks
|
|
301
261
|
const range = maxValue - minValue;
|
|
302
262
|
if (valueSpacing && valueSpacing > 0) {
|
|
303
|
-
// Use specified spacing - only show ticks that fit with spacing
|
|
304
263
|
let currentX = originX;
|
|
305
264
|
let currentValue = minValue;
|
|
306
265
|
while (currentX <= axisEndX && currentValue <= maxValue) {
|
|
307
|
-
// Draw tick mark
|
|
308
266
|
ctx.beginPath();
|
|
309
267
|
ctx.moveTo(currentX, originY);
|
|
310
268
|
ctx.lineTo(currentX, originY + 5);
|
|
311
269
|
ctx.stroke();
|
|
312
|
-
// Draw label
|
|
313
270
|
ctx.fillText(currentValue.toString(), currentX, originY + 10);
|
|
314
271
|
currentX += valueSpacing;
|
|
315
272
|
currentValue += step;
|
|
316
273
|
}
|
|
317
274
|
}
|
|
318
275
|
else {
|
|
319
|
-
// Original behavior - evenly distribute based on value
|
|
320
|
-
// But check for label overlap and skip labels if they're too close
|
|
321
276
|
let lastLabelX = -Infinity;
|
|
322
|
-
const minLabelSpacing = 40;
|
|
277
|
+
const minLabelSpacing = 40;
|
|
323
278
|
for (let value = minValue; value <= maxValue; value += step) {
|
|
324
279
|
const x = originX + ((value - minValue) / range) * chartWidth;
|
|
325
280
|
const labelText = value.toString();
|
|
326
281
|
const labelWidth = ctx.measureText(labelText).width;
|
|
327
|
-
// Check if this label would overlap with the previous one
|
|
328
282
|
if (x - lastLabelX < minLabelSpacing && value > minValue) {
|
|
329
|
-
// Skip this label to prevent overlap
|
|
330
283
|
continue;
|
|
331
284
|
}
|
|
332
|
-
// Draw tick mark
|
|
333
285
|
ctx.beginPath();
|
|
334
286
|
ctx.moveTo(x, originY);
|
|
335
287
|
ctx.lineTo(x, originY + 5);
|
|
336
288
|
ctx.stroke();
|
|
337
|
-
// Draw label
|
|
338
289
|
ctx.fillText(labelText, x, originY + 10);
|
|
339
|
-
lastLabelX = x + labelWidth / 2;
|
|
290
|
+
lastLabelX = x + labelWidth / 2;
|
|
340
291
|
}
|
|
341
292
|
}
|
|
342
293
|
}
|
|
@@ -351,7 +302,6 @@ function calculateLegendDimensions(legend, fontSize, maxWidth, wrapTextEnabled =
|
|
|
351
302
|
const boxSize = 15;
|
|
352
303
|
const spacing = 10;
|
|
353
304
|
const padding = paddingBox;
|
|
354
|
-
// Create a temporary canvas to measure text
|
|
355
305
|
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
356
306
|
const tempCtx = tempCanvas.getContext('2d');
|
|
357
307
|
tempCtx.font = `${fontSize}px Arial`;
|
|
@@ -411,12 +361,10 @@ async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, bac
|
|
|
411
361
|
const spacing = 10;
|
|
412
362
|
const padding = paddingBox ?? 8;
|
|
413
363
|
ctx.font = `${fontSize}px Arial`;
|
|
414
|
-
// Determine colors
|
|
415
364
|
const isDarkBackground = backgroundColor === '#000000' || backgroundColor.toLowerCase() === 'black';
|
|
416
365
|
const effectiveTextColor = textColor ?? (isDarkBackground ? '#FFFFFF' : '#000000');
|
|
417
366
|
const effectiveBgColor = isDarkBackground ? 'rgba(0, 0, 0, 0.8)' : (backgroundColor.startsWith('rgba') || backgroundColor.startsWith('rgb') ? backgroundColor : 'rgba(255, 255, 255, 0.9)');
|
|
418
367
|
const effectiveBorderColor = borderColor ?? (isDarkBackground ? '#FFFFFF' : '#000000');
|
|
419
|
-
// Calculate dimensions with text wrapping support
|
|
420
368
|
const textSpacing = 10;
|
|
421
369
|
const effectiveMaxWidth = maxWidth ? maxWidth - padding * 2 - boxSize - textSpacing : undefined;
|
|
422
370
|
let maxEntryWidth = 0;
|
|
@@ -427,7 +375,7 @@ async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, bac
|
|
|
427
375
|
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
428
376
|
const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
|
|
429
377
|
textWidth = Math.max(...wrappedLines.map(line => ctx.measureText(line).width));
|
|
430
|
-
textHeight = wrappedLines.length * fontSize * 1.2;
|
|
378
|
+
textHeight = wrappedLines.length * fontSize * 1.2;
|
|
431
379
|
}
|
|
432
380
|
else {
|
|
433
381
|
textWidth = ctx.measureText(entry.label).width;
|
|
@@ -439,16 +387,13 @@ async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, bac
|
|
|
439
387
|
});
|
|
440
388
|
const legendWidth = maxWidth ? maxWidth : maxEntryWidth + padding * 2;
|
|
441
389
|
const legendHeight = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? spacing : 0), 0) + padding * 2;
|
|
442
|
-
// Draw legend background (gradient or color)
|
|
443
390
|
ctx.beginPath();
|
|
444
391
|
ctx.rect(legendX, legendY, legendWidth, legendHeight);
|
|
445
392
|
fillWithGradientOrColor(ctx, backgroundGradient, effectiveBgColor, effectiveBgColor, { x: legendX, y: legendY, w: legendWidth, h: legendHeight });
|
|
446
393
|
ctx.fill();
|
|
447
|
-
// Draw legend border
|
|
448
394
|
ctx.strokeStyle = effectiveBorderColor;
|
|
449
395
|
ctx.lineWidth = 1;
|
|
450
396
|
ctx.strokeRect(legendX, legendY, legendWidth, legendHeight);
|
|
451
|
-
// Draw legend entries
|
|
452
397
|
ctx.textAlign = 'left';
|
|
453
398
|
ctx.textBaseline = 'middle';
|
|
454
399
|
ctx.fillStyle = effectiveTextColor;
|
|
@@ -457,16 +402,13 @@ async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, bac
|
|
|
457
402
|
const entry = legend[index];
|
|
458
403
|
const entryHeight = entryHeights[index];
|
|
459
404
|
const centerY = currentY + entryHeight / 2;
|
|
460
|
-
// Draw color box (gradient or color)
|
|
461
405
|
ctx.beginPath();
|
|
462
406
|
ctx.rect(legendX + padding, centerY - boxSize / 2, boxSize, boxSize);
|
|
463
407
|
fillWithGradientOrColor(ctx, entry.gradient, entry.color || '#4A90E2', '#4A90E2', { x: legendX + padding, y: centerY - boxSize / 2, w: boxSize, h: boxSize });
|
|
464
408
|
ctx.fill();
|
|
465
|
-
// Draw box border
|
|
466
409
|
ctx.strokeStyle = effectiveBorderColor;
|
|
467
410
|
ctx.lineWidth = 1;
|
|
468
411
|
ctx.strokeRect(legendX + padding, centerY - boxSize / 2, boxSize, boxSize);
|
|
469
|
-
// Draw label (with wrapping if enabled) using enhanced text
|
|
470
412
|
const textX = legendX + padding + boxSize + textSpacing;
|
|
471
413
|
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
472
414
|
const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
|
|
@@ -493,7 +435,6 @@ async function drawLegend(ctx, legend, position, width, height, padding, fontSiz
|
|
|
493
435
|
const boxSize = 15;
|
|
494
436
|
const spacing = 10;
|
|
495
437
|
const paddingBox = 8;
|
|
496
|
-
// Calculate legend dimensions
|
|
497
438
|
ctx.font = `${fontSize}px Arial`;
|
|
498
439
|
const maxLabelWidth = Math.max(...legend.map(e => ctx.measureText(e.label).width));
|
|
499
440
|
const legendWidth = boxSize + spacing + maxLabelWidth + paddingBox * 2;
|
|
@@ -520,19 +461,15 @@ async function drawLegend(ctx, legend, position, width, height, padding, fontSiz
|
|
|
520
461
|
legendX = width - padding.right - legendWidth - legendSpacing;
|
|
521
462
|
legendY = padding.top + legendSpacing;
|
|
522
463
|
}
|
|
523
|
-
// Determine text color based on background
|
|
524
464
|
const isDarkBackground = backgroundColor === '#000000' || backgroundColor.toLowerCase() === 'black';
|
|
525
465
|
const textColor = isDarkBackground ? '#FFFFFF' : '#000000';
|
|
526
466
|
const bgColor = isDarkBackground ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)';
|
|
527
467
|
const borderColor = isDarkBackground ? '#FFFFFF' : '#000000';
|
|
528
|
-
// Draw legend background
|
|
529
468
|
ctx.fillStyle = bgColor;
|
|
530
469
|
ctx.fillRect(legendX, legendY, legendWidth, legendHeight);
|
|
531
|
-
// Draw legend border
|
|
532
470
|
ctx.strokeStyle = borderColor;
|
|
533
471
|
ctx.lineWidth = 1;
|
|
534
472
|
ctx.strokeRect(legendX, legendY, legendWidth, legendHeight);
|
|
535
|
-
// Draw legend entries
|
|
536
473
|
ctx.font = `${fontSize}px Arial`;
|
|
537
474
|
ctx.textAlign = 'left';
|
|
538
475
|
ctx.textBaseline = 'middle';
|
|
@@ -540,16 +477,13 @@ async function drawLegend(ctx, legend, position, width, height, padding, fontSiz
|
|
|
540
477
|
const entry = legend[index];
|
|
541
478
|
const y = legendY + paddingBox + index * (boxSize + spacing) + boxSize / 2;
|
|
542
479
|
const x = legendX + paddingBox;
|
|
543
|
-
// Draw color box (gradient or color)
|
|
544
480
|
ctx.beginPath();
|
|
545
481
|
ctx.rect(x, y - boxSize / 2, boxSize, boxSize);
|
|
546
482
|
fillWithGradientOrColor(ctx, entry.gradient, entry.color || '#4A90E2', '#4A90E2', { x, y: y - boxSize / 2, w: boxSize, h: boxSize });
|
|
547
483
|
ctx.fill();
|
|
548
|
-
// Draw box border
|
|
549
484
|
ctx.strokeStyle = borderColor;
|
|
550
485
|
ctx.lineWidth = 1;
|
|
551
486
|
ctx.strokeRect(x, y - boxSize / 2, boxSize, boxSize);
|
|
552
|
-
// Draw label
|
|
553
487
|
ctx.fillStyle = textColor;
|
|
554
488
|
ctx.fillText(entry.label, x + boxSize + spacing, y);
|
|
555
489
|
}
|
|
@@ -562,10 +496,9 @@ function drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep,
|
|
|
562
496
|
ctx.save();
|
|
563
497
|
ctx.strokeStyle = gridColor;
|
|
564
498
|
ctx.lineWidth = gridWidth;
|
|
565
|
-
ctx.setLineDash([2, 2]);
|
|
499
|
+
ctx.setLineDash([2, 2]);
|
|
566
500
|
const chartWidth = axisEndX - originX;
|
|
567
501
|
const chartHeight = originY - axisEndY;
|
|
568
|
-
// Draw vertical grid lines (based on X-axis)
|
|
569
502
|
if (xAxisCustomValues && xAxisCustomValues.length > 0) {
|
|
570
503
|
const totalValues = xAxisCustomValues.length;
|
|
571
504
|
const divisor = totalValues > 1 ? totalValues - 1 : 1;
|
|
@@ -587,7 +520,6 @@ function drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep,
|
|
|
587
520
|
ctx.stroke();
|
|
588
521
|
}
|
|
589
522
|
}
|
|
590
|
-
// Draw horizontal grid lines (based on Y-axis)
|
|
591
523
|
if (yAxisCustomValues && yAxisCustomValues.length > 0) {
|
|
592
524
|
const actualMin = Math.min(...yAxisCustomValues);
|
|
593
525
|
const actualMax = Math.max(...yAxisCustomValues);
|
|
@@ -620,15 +552,11 @@ function calculateResponsiveWidth(xAxisRange, options = {}, customValues) {
|
|
|
620
552
|
const paddingLeft = padding.left ?? 100;
|
|
621
553
|
const paddingRight = padding.right ?? 80;
|
|
622
554
|
if (customValues && customValues.length > 0) {
|
|
623
|
-
// Calculate width based on number of custom values
|
|
624
|
-
// Use about 20-25 pixels per tick mark
|
|
625
555
|
const minChartAreaWidth = Math.max(400, customValues.length * 20);
|
|
626
556
|
return paddingLeft + minChartAreaWidth + paddingRight;
|
|
627
557
|
}
|
|
628
|
-
// Calculate width based on X-axis range
|
|
629
|
-
// Use a reasonable scale: about 10-15 pixels per unit on X-axis
|
|
630
558
|
const xRange = xAxisRange.max - xAxisRange.min;
|
|
631
|
-
const minChartAreaWidth = Math.max(400, xRange * 10);
|
|
559
|
+
const minChartAreaWidth = Math.max(400, xRange * 10);
|
|
632
560
|
return paddingLeft + minChartAreaWidth + paddingRight;
|
|
633
561
|
}
|
|
634
562
|
/**
|
|
@@ -648,35 +576,27 @@ function drawAxes(width = 800, height = 600, options = {}) {
|
|
|
648
576
|
const paddingRight = padding.right ?? 80;
|
|
649
577
|
const paddingBottom = padding.bottom ?? 80;
|
|
650
578
|
const paddingLeft = padding.left ?? 100;
|
|
651
|
-
// Create canvas
|
|
652
579
|
const canvas = (0, canvas_1.createCanvas)(width, height);
|
|
653
580
|
const ctx = canvas.getContext('2d');
|
|
654
|
-
// Fill white background
|
|
655
581
|
ctx.fillStyle = backgroundColor;
|
|
656
582
|
ctx.fillRect(0, 0, width, height);
|
|
657
|
-
// Calculate axis positions
|
|
658
583
|
const originX = paddingLeft;
|
|
659
584
|
const originY = height - paddingBottom;
|
|
660
585
|
const axisEndX = width - paddingRight;
|
|
661
586
|
const axisEndY = paddingTop;
|
|
662
|
-
// Set axis style
|
|
663
587
|
ctx.strokeStyle = axisColor;
|
|
664
588
|
ctx.fillStyle = axisColor;
|
|
665
589
|
ctx.lineWidth = axisWidth;
|
|
666
590
|
ctx.lineCap = 'round';
|
|
667
|
-
// Draw Y-axis (vertical line from origin to top)
|
|
668
591
|
ctx.beginPath();
|
|
669
592
|
ctx.moveTo(originX, originY);
|
|
670
593
|
ctx.lineTo(originX, axisEndY);
|
|
671
594
|
ctx.stroke();
|
|
672
|
-
// Draw X-axis (horizontal line from origin to right)
|
|
673
595
|
ctx.beginPath();
|
|
674
596
|
ctx.moveTo(originX, originY);
|
|
675
597
|
ctx.lineTo(axisEndX, originY);
|
|
676
598
|
ctx.stroke();
|
|
677
|
-
// Draw arrow on Y-axis (pointing up)
|
|
678
599
|
drawArrow(ctx, originX, axisEndY, -Math.PI / 2, arrowSize);
|
|
679
|
-
// Draw arrow on X-axis (pointing right)
|
|
680
600
|
drawArrow(ctx, axisEndX, originY, 0, arrowSize);
|
|
681
601
|
return { buffer: canvas.toBuffer('image/png'), ctx, canvas };
|
|
682
602
|
}
|
|
@@ -687,18 +607,14 @@ function drawAxes(width = 800, height = 600, options = {}) {
|
|
|
687
607
|
* @returns Canvas buffer
|
|
688
608
|
*/
|
|
689
609
|
async function createBarChart(data, options = {}) {
|
|
690
|
-
// Extract and map organized config to internal variables
|
|
691
|
-
// Dimensions
|
|
692
610
|
const height = options.dimensions?.height ?? 600;
|
|
693
611
|
const padding = options.dimensions?.padding || {};
|
|
694
|
-
// Appearance
|
|
695
612
|
const backgroundColor = options.appearance?.backgroundColor ?? '#FFFFFF';
|
|
696
613
|
const backgroundGradient = options.appearance?.backgroundGradient;
|
|
697
614
|
const backgroundImage = options.appearance?.backgroundImage;
|
|
698
615
|
const axisColor = options.appearance?.axisColor ?? options.axes?.x?.color ?? options.axes?.y?.color ?? '#000000';
|
|
699
616
|
const axisWidth = options.appearance?.axisWidth ?? options.axes?.x?.width ?? options.axes?.y?.width ?? 2;
|
|
700
617
|
const arrowSize = options.appearance?.arrowSize ?? 10;
|
|
701
|
-
// Labels
|
|
702
618
|
const chartTitle = options.labels?.title?.text;
|
|
703
619
|
const chartTitleFontSize = options.labels?.title?.fontSize ?? 24;
|
|
704
620
|
const showBarLabels = options.labels?.barLabelDefaults?.show ?? true;
|
|
@@ -707,7 +623,6 @@ async function createBarChart(data, options = {}) {
|
|
|
707
623
|
const showValues = options.labels?.valueLabelDefaults?.show ?? true;
|
|
708
624
|
const valueFontSize = options.labels?.valueLabelDefaults?.fontSize ?? 12;
|
|
709
625
|
const valueColor = options.labels?.valueLabelDefaults?.defaultColor ?? '#000000';
|
|
710
|
-
// Axes
|
|
711
626
|
const xAxisLabel = options.axes?.x?.label;
|
|
712
627
|
const yAxisLabel = options.axes?.y?.label;
|
|
713
628
|
const axisLabelColor = options.axes?.x?.labelColor ?? options.axes?.y?.labelColor ?? '#000000';
|
|
@@ -715,23 +630,18 @@ async function createBarChart(data, options = {}) {
|
|
|
715
630
|
const xAxisValues = options.axes?.x?.values;
|
|
716
631
|
const yAxisRange = options.axes?.y?.range;
|
|
717
632
|
const yAxisValues = options.axes?.y?.values;
|
|
718
|
-
const baseline = options.axes?.y?.baseline ?? 0;
|
|
633
|
+
const baseline = options.axes?.y?.baseline ?? 0;
|
|
719
634
|
const tickFontSize = options.axes?.x?.tickFontSize ?? options.axes?.y?.tickFontSize ?? 12;
|
|
720
635
|
const xAxisValueSpacing = options.axes?.x?.valueSpacing;
|
|
721
636
|
const yAxisValueSpacing = options.axes?.y?.valueSpacing;
|
|
722
|
-
// Chart type
|
|
723
637
|
const chartType = options.type ?? 'standard';
|
|
724
|
-
// Waterfall chart options
|
|
725
638
|
const initialValue = options.waterfall?.initialValue ?? 0;
|
|
726
|
-
// Legend
|
|
727
639
|
const showLegend = options.legend?.show ?? false;
|
|
728
640
|
const legend = options.legend?.entries;
|
|
729
|
-
const legendPosition = options.legend?.position ?? 'right';
|
|
730
|
-
// Grid
|
|
641
|
+
const legendPosition = options.legend?.position ?? 'right';
|
|
731
642
|
const showGrid = options.grid?.show ?? false;
|
|
732
643
|
const gridColor = options.grid?.color ?? '#E0E0E0';
|
|
733
644
|
const gridWidth = options.grid?.width ?? 1;
|
|
734
|
-
// Bars
|
|
735
645
|
const minBarWidth = options.bars?.minWidth ?? 20;
|
|
736
646
|
const barSpacing = options.bars?.spacing;
|
|
737
647
|
const groupSpacing = options.bars?.groupSpacing ?? 10;
|
|
@@ -745,11 +655,9 @@ async function createBarChart(data, options = {}) {
|
|
|
745
655
|
const paddingRight = padding.right ?? 80;
|
|
746
656
|
const paddingBottom = padding.bottom ?? 80;
|
|
747
657
|
const paddingLeft = padding.left ?? 100;
|
|
748
|
-
// Determine X-axis range from custom values, options, or data
|
|
749
658
|
let xMin, xMax;
|
|
750
659
|
let xAxisCustomValues = xAxisValues;
|
|
751
660
|
if (xAxisCustomValues && xAxisCustomValues.length > 0) {
|
|
752
|
-
// Use custom X-axis values
|
|
753
661
|
xMin = Math.min(...xAxisCustomValues);
|
|
754
662
|
xMax = Math.max(...xAxisCustomValues);
|
|
755
663
|
}
|
|
@@ -758,7 +666,6 @@ async function createBarChart(data, options = {}) {
|
|
|
758
666
|
xMax = xAxisRange.max;
|
|
759
667
|
}
|
|
760
668
|
else {
|
|
761
|
-
// Auto-calculate from data
|
|
762
669
|
if (data.length === 0) {
|
|
763
670
|
xMin = 0;
|
|
764
671
|
xMax = 100;
|
|
@@ -768,20 +675,17 @@ async function createBarChart(data, options = {}) {
|
|
|
768
675
|
const allXEnds = data.map(d => d.xEnd);
|
|
769
676
|
xMin = Math.min(...allXStarts, ...allXEnds);
|
|
770
677
|
xMax = Math.max(...allXStarts, ...allXEnds);
|
|
771
|
-
// Add some padding
|
|
772
678
|
const xPadding = (xMax - xMin) * 0.1;
|
|
773
679
|
xMin = Math.max(0, xMin - xPadding);
|
|
774
680
|
xMax = xMax + xPadding;
|
|
775
681
|
}
|
|
776
682
|
}
|
|
777
|
-
// Calculate responsive width based on X-axis range or custom values
|
|
778
683
|
let baseWidth = calculateResponsiveWidth({ min: xMin, max: xMax }, options, xAxisCustomValues);
|
|
779
|
-
// Calculate legend dimensions and adjust canvas size based on legend position
|
|
780
684
|
let legendWidth = 0;
|
|
781
685
|
let legendHeight = 0;
|
|
782
686
|
let extraWidth = 0;
|
|
783
687
|
let extraHeight = 0;
|
|
784
|
-
const minLegendSpacing = 10;
|
|
688
|
+
const minLegendSpacing = 10;
|
|
785
689
|
if (showLegend && legend && legend.length > 0) {
|
|
786
690
|
const legendMaxWidth = options.legend?.maxWidth;
|
|
787
691
|
const legendWrapText = options.legend?.wrapText !== false;
|
|
@@ -790,16 +694,12 @@ async function createBarChart(data, options = {}) {
|
|
|
790
694
|
legendWidth = legendDims.width;
|
|
791
695
|
legendHeight = legendDims.height;
|
|
792
696
|
const legendSpacing = options.legend?.spacing ?? 20;
|
|
793
|
-
// Adjust canvas dimensions based on legend position
|
|
794
|
-
// For left position, add extra space for Y-axis labels
|
|
795
697
|
if (legendPosition === 'left') {
|
|
796
|
-
// Estimate Y-axis label width: measure potential large values
|
|
797
698
|
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
798
699
|
const tempCtx = tempCanvas.getContext('2d');
|
|
799
|
-
let estimatedYAxisLabelWidth = 60;
|
|
700
|
+
let estimatedYAxisLabelWidth = 60;
|
|
800
701
|
if (tempCtx) {
|
|
801
702
|
tempCtx.font = `${tickFontSize}px Arial`;
|
|
802
|
-
// Get max value from data to estimate label width
|
|
803
703
|
const allValues = [];
|
|
804
704
|
data.forEach(d => {
|
|
805
705
|
if (d.values && d.values.length > 0) {
|
|
@@ -817,7 +717,6 @@ async function createBarChart(data, options = {}) {
|
|
|
817
717
|
if (allValues.length > 0) {
|
|
818
718
|
const maxValue = Math.max(...allValues);
|
|
819
719
|
const minValue = Math.min(...allValues);
|
|
820
|
-
// Measure potential labels
|
|
821
720
|
const testLabels = [
|
|
822
721
|
maxValue.toFixed(1),
|
|
823
722
|
minValue.toFixed(1),
|
|
@@ -829,7 +728,6 @@ async function createBarChart(data, options = {}) {
|
|
|
829
728
|
estimatedYAxisLabelWidth = Math.max(estimatedYAxisLabelWidth, width);
|
|
830
729
|
});
|
|
831
730
|
}
|
|
832
|
-
// Add padding: 10px (label offset) + 5px (tick) + 15px (spacing) = 30px total
|
|
833
731
|
estimatedYAxisLabelWidth += 30;
|
|
834
732
|
}
|
|
835
733
|
extraWidth = legendWidth + legendSpacing + estimatedYAxisLabelWidth + minLegendSpacing;
|
|
@@ -843,19 +741,15 @@ async function createBarChart(data, options = {}) {
|
|
|
843
741
|
}
|
|
844
742
|
const width = baseWidth + extraWidth;
|
|
845
743
|
const adjustedHeight = height + extraHeight;
|
|
846
|
-
// Create canvas
|
|
847
744
|
const canvas = (0, canvas_1.createCanvas)(width, adjustedHeight);
|
|
848
745
|
const ctx = canvas.getContext('2d');
|
|
849
|
-
// Fill background (gradient, image, or color)
|
|
850
746
|
if (backgroundImage) {
|
|
851
747
|
try {
|
|
852
748
|
const bgImage = await (0, canvas_1.loadImage)(backgroundImage);
|
|
853
|
-
// Draw image to fill entire canvas
|
|
854
749
|
ctx.drawImage(bgImage, 0, 0, width, adjustedHeight);
|
|
855
750
|
}
|
|
856
751
|
catch (error) {
|
|
857
752
|
console.warn(`Failed to load background image: ${backgroundImage}`, error);
|
|
858
|
-
// Fallback to gradient or color if image fails to load
|
|
859
753
|
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
|
|
860
754
|
x: 0, y: 0, w: width, h: adjustedHeight
|
|
861
755
|
});
|
|
@@ -868,10 +762,8 @@ async function createBarChart(data, options = {}) {
|
|
|
868
762
|
});
|
|
869
763
|
ctx.fillRect(0, 0, width, adjustedHeight);
|
|
870
764
|
}
|
|
871
|
-
// Calculate axis positions
|
|
872
765
|
const titleHeight = chartTitle ? chartTitleFontSize + 30 : 0;
|
|
873
766
|
const axisLabelHeight = (xAxisLabel || yAxisLabel) ? axisLabelFontSize + 20 : 0;
|
|
874
|
-
// Adjust chart area based on legend position
|
|
875
767
|
let chartAreaLeft = paddingLeft;
|
|
876
768
|
let chartAreaRight = baseWidth - paddingRight;
|
|
877
769
|
let chartAreaTop = paddingTop + titleHeight;
|
|
@@ -879,13 +771,11 @@ async function createBarChart(data, options = {}) {
|
|
|
879
771
|
if (showLegend && legend && legend.length > 0) {
|
|
880
772
|
const legendSpacing = options.legend?.spacing ?? 20;
|
|
881
773
|
if (legendPosition === 'left') {
|
|
882
|
-
|
|
883
|
-
let actualYAxisLabelWidth = 60; // Default estimate
|
|
774
|
+
let actualYAxisLabelWidth = 60;
|
|
884
775
|
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
885
776
|
const tempCtx = tempCanvas.getContext('2d');
|
|
886
777
|
if (tempCtx) {
|
|
887
778
|
tempCtx.font = `${tickFontSize}px Arial`;
|
|
888
|
-
// Use the calculated min/max values if available, otherwise estimate
|
|
889
779
|
const allValues = [];
|
|
890
780
|
data.forEach(d => {
|
|
891
781
|
if (d.values && d.values.length > 0) {
|
|
@@ -914,10 +804,8 @@ async function createBarChart(data, options = {}) {
|
|
|
914
804
|
actualYAxisLabelWidth = Math.max(actualYAxisLabelWidth, width);
|
|
915
805
|
});
|
|
916
806
|
}
|
|
917
|
-
// Add padding: 10px (label offset) + 5px (tick) + 15px (spacing)
|
|
918
807
|
actualYAxisLabelWidth += 30;
|
|
919
808
|
}
|
|
920
|
-
// Position chart area to leave room for legend + Y-axis labels
|
|
921
809
|
chartAreaLeft = paddingLeft + legendWidth + legendSpacing + actualYAxisLabelWidth;
|
|
922
810
|
chartAreaRight = baseWidth - paddingRight;
|
|
923
811
|
}
|
|
@@ -938,37 +826,27 @@ async function createBarChart(data, options = {}) {
|
|
|
938
826
|
const originY = chartAreaBottom - axisLabelHeight;
|
|
939
827
|
const axisEndX = chartAreaRight;
|
|
940
828
|
const axisEndY = chartAreaTop;
|
|
941
|
-
// Draw chart title if provided
|
|
942
829
|
if (chartTitle) {
|
|
943
830
|
ctx.save();
|
|
944
831
|
ctx.textAlign = 'center';
|
|
945
832
|
ctx.textBaseline = 'top';
|
|
946
|
-
// Title positioned with proper spacing from top
|
|
947
833
|
const titleY = paddingTop + 10;
|
|
948
834
|
const titleX = width / 2;
|
|
949
835
|
await renderEnhancedText(ctx, chartTitle, titleX, titleY, options.labels?.title?.textStyle, chartTitleFontSize, options.labels?.title?.color, options.labels?.title?.gradient);
|
|
950
836
|
ctx.restore();
|
|
951
837
|
}
|
|
952
|
-
// Set axis style
|
|
953
838
|
ctx.strokeStyle = axisColor;
|
|
954
839
|
ctx.fillStyle = axisColor;
|
|
955
840
|
ctx.lineWidth = axisWidth;
|
|
956
841
|
ctx.lineCap = 'round';
|
|
957
|
-
// Draw Y-axis
|
|
958
842
|
ctx.beginPath();
|
|
959
843
|
ctx.moveTo(originX, originY);
|
|
960
844
|
ctx.lineTo(originX, axisEndY);
|
|
961
845
|
ctx.stroke();
|
|
962
|
-
// Draw arrows (X-axis will be drawn after calculating zero line)
|
|
963
846
|
drawArrow(ctx, originX, axisEndY, -Math.PI / 2, arrowSize);
|
|
964
|
-
// Calculate Y-axis value ranges
|
|
965
|
-
// For grouped charts: find max value across all segments
|
|
966
|
-
// For stacked charts: find max sum of values per category
|
|
967
|
-
// For waterfall charts: find cumulative min/max across all bars
|
|
968
847
|
let allValues = [];
|
|
969
848
|
if (chartType === 'grouped' || chartType === 'stacked' || chartType === 'waterfall') {
|
|
970
849
|
if (chartType === 'grouped') {
|
|
971
|
-
// For grouped: find max value across all segments
|
|
972
850
|
data.forEach(d => {
|
|
973
851
|
if (d.values && d.values.length > 0) {
|
|
974
852
|
d.values.forEach(seg => allValues.push(seg.value));
|
|
@@ -979,24 +857,20 @@ async function createBarChart(data, options = {}) {
|
|
|
979
857
|
});
|
|
980
858
|
}
|
|
981
859
|
else if (chartType === 'waterfall') {
|
|
982
|
-
// For waterfall: calculate all cumulative values (initial + each step's cumulative total)
|
|
983
860
|
let cumulativeValue = initialValue;
|
|
984
|
-
allValues.push(initialValue);
|
|
861
|
+
allValues.push(initialValue);
|
|
985
862
|
data.forEach(d => {
|
|
986
863
|
if (d.values && d.values.length > 0) {
|
|
987
|
-
// Sum all segments for this item
|
|
988
864
|
const itemTotal = d.values.reduce((sum, seg) => sum + seg.value, 0);
|
|
989
865
|
cumulativeValue += itemTotal;
|
|
990
866
|
}
|
|
991
867
|
else if (d.value !== undefined) {
|
|
992
868
|
cumulativeValue += d.value;
|
|
993
869
|
}
|
|
994
|
-
// Add each cumulative total to allValues
|
|
995
870
|
allValues.push(cumulativeValue);
|
|
996
871
|
});
|
|
997
872
|
}
|
|
998
873
|
else {
|
|
999
|
-
// For stacked: find max sum per category
|
|
1000
874
|
data.forEach(d => {
|
|
1001
875
|
if (d.values && d.values.length > 0) {
|
|
1002
876
|
const sum = d.values.reduce((acc, seg) => acc + seg.value, 0);
|
|
@@ -1009,7 +883,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1009
883
|
}
|
|
1010
884
|
}
|
|
1011
885
|
else {
|
|
1012
|
-
// Standard chart: use value directly
|
|
1013
886
|
allValues = data.map(d => d.value ?? 0).filter(v => v !== undefined && v !== null);
|
|
1014
887
|
}
|
|
1015
888
|
let minValue, maxValue, yStep;
|
|
@@ -1017,28 +890,21 @@ async function createBarChart(data, options = {}) {
|
|
|
1017
890
|
const hasExplicitYRange = yAxisRange && yAxisRange.min !== undefined && yAxisRange.max !== undefined;
|
|
1018
891
|
const hasExplicitXRange = xAxisRange && xAxisRange.min !== undefined && xAxisRange.max !== undefined;
|
|
1019
892
|
if (yAxisCustomValues && yAxisCustomValues.length > 0) {
|
|
1020
|
-
// Use custom Y-axis values
|
|
1021
893
|
minValue = Math.min(...yAxisCustomValues);
|
|
1022
894
|
maxValue = Math.max(...yAxisCustomValues);
|
|
1023
|
-
yStep = 1;
|
|
895
|
+
yStep = 1;
|
|
1024
896
|
}
|
|
1025
897
|
else if (hasExplicitYRange) {
|
|
1026
|
-
// Use Y-axis range, but for waterfall charts, ensure it includes all cumulative values
|
|
1027
|
-
// TypeScript narrowing: hasExplicitYRange ensures min and max are defined
|
|
1028
898
|
minValue = yAxisRange.min;
|
|
1029
899
|
maxValue = yAxisRange.max;
|
|
1030
|
-
// Ensure baseline is within range
|
|
1031
900
|
const effectiveBaseline = baseline !== undefined ? baseline : 0;
|
|
1032
901
|
minValue = Math.min(minValue, effectiveBaseline);
|
|
1033
902
|
maxValue = Math.max(maxValue, effectiveBaseline);
|
|
1034
|
-
// For waterfall charts, expand range if needed to include all cumulative values
|
|
1035
903
|
if (chartType === 'waterfall' && allValues.length > 0) {
|
|
1036
904
|
const dataMin = Math.min(...allValues);
|
|
1037
905
|
const dataMax = Math.max(...allValues);
|
|
1038
|
-
// Ensure the range includes all data values
|
|
1039
906
|
minValue = Math.min(minValue, dataMin);
|
|
1040
907
|
maxValue = Math.max(maxValue, dataMax);
|
|
1041
|
-
// Add padding, but ensure baseline is always included
|
|
1042
908
|
const range = maxValue - minValue;
|
|
1043
909
|
const padding = range * 0.1;
|
|
1044
910
|
minValue = Math.min(minValue - padding, effectiveBaseline);
|
|
@@ -1047,20 +913,16 @@ async function createBarChart(data, options = {}) {
|
|
|
1047
913
|
yStep = yAxisRange.step ?? Math.ceil((maxValue - minValue) / 10);
|
|
1048
914
|
}
|
|
1049
915
|
else {
|
|
1050
|
-
// Auto-calculate from data
|
|
1051
916
|
if (allValues.length > 0) {
|
|
1052
917
|
minValue = Math.min(...allValues);
|
|
1053
918
|
maxValue = Math.max(...allValues);
|
|
1054
|
-
// Ensure baseline is within range for waterfall charts
|
|
1055
919
|
if (chartType === 'waterfall') {
|
|
1056
920
|
minValue = Math.min(minValue, initialValue);
|
|
1057
921
|
maxValue = Math.max(maxValue, initialValue);
|
|
1058
922
|
}
|
|
1059
|
-
// Add some padding, but ensure baseline is always included in the range
|
|
1060
923
|
const range = maxValue - minValue;
|
|
1061
924
|
const padding = range * 0.1;
|
|
1062
925
|
const effectiveBaseline = baseline !== undefined ? baseline : 0;
|
|
1063
|
-
// Ensure baseline is within the range
|
|
1064
926
|
minValue = Math.min(minValue - padding, effectiveBaseline);
|
|
1065
927
|
maxValue = maxValue + padding;
|
|
1066
928
|
}
|
|
@@ -1070,7 +932,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1070
932
|
}
|
|
1071
933
|
yStep = Math.ceil((maxValue - minValue) / 10);
|
|
1072
934
|
}
|
|
1073
|
-
// Validate data values against explicit axis ranges
|
|
1074
935
|
if (hasExplicitXRange || xAxisCustomValues) {
|
|
1075
936
|
const effectiveXMin = xAxisCustomValues ? Math.min(...xAxisCustomValues) : xAxisRange.min;
|
|
1076
937
|
const effectiveXMax = xAxisCustomValues ? Math.max(...xAxisCustomValues) : xAxisRange.max;
|
|
@@ -1110,7 +971,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1110
971
|
}
|
|
1111
972
|
}
|
|
1112
973
|
else if (chartType === 'waterfall') {
|
|
1113
|
-
// For waterfall, check individual segment values and cumulative totals
|
|
1114
974
|
if (item.values && item.values.length > 0) {
|
|
1115
975
|
item.values.forEach((seg, segIndex) => {
|
|
1116
976
|
if (seg.value < effectiveYMin || seg.value > effectiveYMax) {
|
|
@@ -1129,7 +989,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1129
989
|
}
|
|
1130
990
|
}
|
|
1131
991
|
else {
|
|
1132
|
-
// Standard chart
|
|
1133
992
|
if (item.value !== undefined && (item.value < effectiveYMin || item.value > effectiveYMax)) {
|
|
1134
993
|
throw new Error(`Bar Chart Error: Data value out of Y-axis bounds.\n` +
|
|
1135
994
|
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has value ${item.value}, ` +
|
|
@@ -1138,37 +997,26 @@ async function createBarChart(data, options = {}) {
|
|
|
1138
997
|
}
|
|
1139
998
|
});
|
|
1140
999
|
}
|
|
1141
|
-
// Draw Y-axis ticks and labels (with custom values if provided)
|
|
1142
1000
|
drawYAxisTicks(ctx, originX, originY, axisEndY, minValue, maxValue, yStep, tickFontSize, yAxisCustomValues, yAxisValueSpacing);
|
|
1143
|
-
// Calculate chart area dimensions (needed for baseline calculation)
|
|
1144
1001
|
const chartAreaHeight = originY - axisEndY;
|
|
1145
|
-
// Calculate baseline position (custom baseline value, default is 0)
|
|
1146
|
-
// Position the baseline within the chart area based on minValue, maxValue, and baseline
|
|
1147
1002
|
const baselineY = originY - ((baseline - minValue) / (maxValue - minValue)) * chartAreaHeight;
|
|
1148
|
-
// Draw X-axis at baseline position
|
|
1149
1003
|
const xAxisY = baselineY;
|
|
1150
1004
|
ctx.beginPath();
|
|
1151
1005
|
ctx.moveTo(originX, xAxisY);
|
|
1152
1006
|
ctx.lineTo(axisEndX, xAxisY);
|
|
1153
1007
|
ctx.stroke();
|
|
1154
|
-
// Draw X-axis arrow
|
|
1155
1008
|
drawArrow(ctx, axisEndX, xAxisY, 0, arrowSize);
|
|
1156
|
-
// Calculate X-axis step
|
|
1157
1009
|
const xStep = xAxisRange?.step ?? Math.ceil((xMax - xMin) / 10);
|
|
1158
|
-
// Draw X-axis ticks and labels at baseline position
|
|
1159
1010
|
drawXAxisTicks(ctx, originX, xAxisY, axisEndX, xMin, xMax, xStep, tickFontSize, xAxisCustomValues, xAxisValueSpacing);
|
|
1160
|
-
// Draw grid lines if enabled (before calculating zero line, but will use correct Y position)
|
|
1161
1011
|
if (showGrid) {
|
|
1162
1012
|
drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep, minValue, maxValue, yStep, xAxisCustomValues, yAxisCustomValues, gridColor, gridWidth);
|
|
1163
1013
|
}
|
|
1164
|
-
// Draw X-axis label if provided
|
|
1165
1014
|
if (xAxisLabel) {
|
|
1166
1015
|
ctx.save();
|
|
1167
1016
|
ctx.fillStyle = axisLabelColor;
|
|
1168
1017
|
ctx.font = `${axisLabelFontSize}px Arial`;
|
|
1169
1018
|
ctx.textAlign = 'center';
|
|
1170
1019
|
ctx.textBaseline = 'top';
|
|
1171
|
-
// Position label below X-axis ticks (ticks are at xAxisY + 10, so add more spacing)
|
|
1172
1020
|
ctx.fillText(xAxisLabel, (originX + axisEndX) / 2, xAxisY + 25);
|
|
1173
1021
|
ctx.restore();
|
|
1174
1022
|
}
|
|
@@ -1178,7 +1026,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1178
1026
|
ctx.font = `${axisLabelFontSize}px Arial`;
|
|
1179
1027
|
ctx.textAlign = 'center';
|
|
1180
1028
|
ctx.textBaseline = 'bottom';
|
|
1181
|
-
// Rotate for vertical text
|
|
1182
1029
|
const labelX = originX - 30;
|
|
1183
1030
|
const labelY = (originY + axisEndY) / 2;
|
|
1184
1031
|
ctx.translate(labelX, labelY);
|
|
@@ -1186,7 +1033,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1186
1033
|
ctx.fillText(yAxisLabel, 0, 0);
|
|
1187
1034
|
ctx.restore();
|
|
1188
1035
|
}
|
|
1189
|
-
// Draw legend if provided - positioned based on legendPosition option
|
|
1190
1036
|
if (showLegend && legend && legend.length > 0) {
|
|
1191
1037
|
const legendSpacing = options.legend?.spacing ?? 20;
|
|
1192
1038
|
const legendFontSize = options.legend?.fontSize ?? 16;
|
|
@@ -1196,111 +1042,85 @@ async function createBarChart(data, options = {}) {
|
|
|
1196
1042
|
const legendPadding = options.legend?.padding;
|
|
1197
1043
|
const legendMaxWidth = options.legend?.maxWidth;
|
|
1198
1044
|
const legendWrapText = options.legend?.wrapText !== false;
|
|
1199
|
-
// Calculate legend position based on legendPosition option
|
|
1200
1045
|
let legendX, legendY;
|
|
1201
1046
|
const chartAreaHeight = originY - axisEndY;
|
|
1202
1047
|
const chartAreaWidth = axisEndX - originX;
|
|
1203
1048
|
switch (legendPosition) {
|
|
1204
1049
|
case 'top':
|
|
1205
|
-
legendX = (width - legendWidth) / 2;
|
|
1050
|
+
legendX = (width - legendWidth) / 2;
|
|
1206
1051
|
legendY = paddingTop + titleHeight + minLegendSpacing;
|
|
1207
1052
|
break;
|
|
1208
1053
|
case 'bottom':
|
|
1209
|
-
legendX = (width - legendWidth) / 2;
|
|
1054
|
+
legendX = (width - legendWidth) / 2;
|
|
1210
1055
|
legendY = adjustedHeight - paddingBottom - legendHeight - minLegendSpacing;
|
|
1211
1056
|
break;
|
|
1212
1057
|
case 'left':
|
|
1213
|
-
// Position legend further left to make room for Y-axis labels
|
|
1214
|
-
// Position legend on the left side
|
|
1215
1058
|
legendX = paddingLeft + minLegendSpacing;
|
|
1216
|
-
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2;
|
|
1059
|
+
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2;
|
|
1217
1060
|
break;
|
|
1218
1061
|
case 'right':
|
|
1219
1062
|
default:
|
|
1220
1063
|
legendX = axisEndX + minLegendSpacing;
|
|
1221
|
-
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2;
|
|
1064
|
+
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2;
|
|
1222
1065
|
break;
|
|
1223
1066
|
}
|
|
1224
1067
|
await drawLegendAtPosition(ctx, legend, legendX, legendY, legendFontSize, legendBgColor || backgroundColor, legendTextColor, legendBorderColor, legendPadding, legendMaxWidth, legendWrapText, options.legend?.backgroundGradient, options.legend?.textGradient, options.legend?.textStyle);
|
|
1225
1068
|
}
|
|
1226
|
-
// Calculate chart area dimensions
|
|
1227
1069
|
const chartAreaWidth = axisEndX - originX;
|
|
1228
1070
|
const labelsToDraw = [];
|
|
1229
|
-
// Track value label positions per bar (for adjusting bar label positions)
|
|
1230
1071
|
const valueLabelPositions = new Map();
|
|
1231
|
-
// First pass: Draw all bars (no labels)
|
|
1232
1072
|
data.forEach((item, itemIndex) => {
|
|
1233
|
-
// Calculate bar position and width based on X-axis range
|
|
1234
|
-
// If custom X-axis values are provided, map to those positions
|
|
1235
1073
|
let barXStart, barXEnd;
|
|
1236
1074
|
if (xAxisCustomValues && xAxisCustomValues.length > 0) {
|
|
1237
|
-
// Map to custom X-axis values
|
|
1238
1075
|
const actualMin = Math.min(...xAxisCustomValues);
|
|
1239
1076
|
const actualMax = Math.max(...xAxisCustomValues);
|
|
1240
1077
|
const xRange = actualMax - actualMin;
|
|
1241
|
-
// Find the position of xStart and xEnd in the custom values array
|
|
1242
|
-
// If xStart equals xEnd, it's a single-position bar
|
|
1243
1078
|
const startIndex = xAxisCustomValues.indexOf(item.xStart);
|
|
1244
1079
|
const endIndex = xAxisCustomValues.indexOf(item.xEnd);
|
|
1245
1080
|
if (startIndex !== -1 && endIndex !== -1) {
|
|
1246
|
-
// Both values found in custom array - use index-based positioning
|
|
1247
1081
|
const totalValues = xAxisCustomValues.length;
|
|
1248
1082
|
const divisor = totalValues > 1 ? totalValues - 1 : 1;
|
|
1249
1083
|
barXStart = originX + (startIndex / divisor) * chartAreaWidth;
|
|
1250
1084
|
barXEnd = originX + (endIndex / divisor) * chartAreaWidth;
|
|
1251
1085
|
}
|
|
1252
1086
|
else {
|
|
1253
|
-
// Fallback to range-based positioning
|
|
1254
1087
|
barXStart = originX + ((item.xStart - actualMin) / xRange) * chartAreaWidth;
|
|
1255
1088
|
barXEnd = originX + ((item.xEnd - actualMin) / xRange) * chartAreaWidth;
|
|
1256
1089
|
}
|
|
1257
1090
|
}
|
|
1258
1091
|
else {
|
|
1259
|
-
// Use regular range mapping
|
|
1260
1092
|
const xRange = xMax - xMin;
|
|
1261
1093
|
barXStart = originX + ((item.xStart - xMin) / xRange) * chartAreaWidth;
|
|
1262
1094
|
barXEnd = originX + ((item.xEnd - xMin) / xRange) * chartAreaWidth;
|
|
1263
1095
|
}
|
|
1264
|
-
// If xStart equals xEnd, use a minimum bar width
|
|
1265
1096
|
const groupWidth = Math.max(barXEnd - barXStart, minBarWidth);
|
|
1266
1097
|
if (item.xStart === item.xEnd) {
|
|
1267
|
-
// Center the bar at the position
|
|
1268
1098
|
const centerX = barXStart;
|
|
1269
1099
|
barXStart = centerX - groupWidth / 2;
|
|
1270
1100
|
}
|
|
1271
|
-
// Handle grouped/stacked/waterfall vs standard charts
|
|
1272
1101
|
if ((chartType === 'grouped' || chartType === 'stacked' || chartType === 'waterfall') && item.values && item.values.length > 0) {
|
|
1273
|
-
// Grouped, stacked, or waterfall chart
|
|
1274
1102
|
const segments = item.values;
|
|
1275
1103
|
const numSegments = segments.length;
|
|
1276
1104
|
if (chartType === 'grouped') {
|
|
1277
|
-
// Grouped: bars side-by-side
|
|
1278
1105
|
const segmentWidth = (groupWidth - (groupSpacing * (numSegments - 1))) / numSegments;
|
|
1279
|
-
// Track the highest value label Y position for this grouped bar
|
|
1280
1106
|
let highestValueLabelY = null;
|
|
1281
1107
|
segments.forEach((segment, segIndex) => {
|
|
1282
1108
|
const segXStart = barXStart + (segIndex * (segmentWidth + groupSpacing));
|
|
1283
|
-
// Calculate bar position relative to baseline
|
|
1284
1109
|
let barY, barHeight;
|
|
1285
1110
|
if (segment.value >= baseline) {
|
|
1286
|
-
// Bar extends above baseline
|
|
1287
1111
|
const positiveRatio = (segment.value - baseline) / (maxValue - minValue);
|
|
1288
1112
|
barHeight = positiveRatio * chartAreaHeight;
|
|
1289
1113
|
barY = baselineY - barHeight;
|
|
1290
1114
|
}
|
|
1291
1115
|
else {
|
|
1292
|
-
// Bar extends below baseline
|
|
1293
1116
|
const negativeRatio = (baseline - segment.value) / (maxValue - minValue);
|
|
1294
1117
|
barHeight = negativeRatio * chartAreaHeight;
|
|
1295
1118
|
barY = baselineY;
|
|
1296
1119
|
}
|
|
1297
|
-
// Draw segment bar with gradient, opacity, shadow, and stroke
|
|
1298
1120
|
drawBar(ctx, segXStart, barY, segmentWidth, barHeight, segment.color || item.color || '#4A90E2', segment.gradient || item.gradient, segment.opacity ?? item.opacity ?? globalBarOpacity, segment.shadow || item.shadow, segment.stroke || item.stroke, globalBarShadow, globalBarStroke);
|
|
1299
|
-
// Store value label for later drawing
|
|
1300
1121
|
const shouldShowValue = segment.showValue !== undefined ? segment.showValue : showValues;
|
|
1301
1122
|
if (shouldShowValue) {
|
|
1302
1123
|
const valueLabelY = barY - 5;
|
|
1303
|
-
// Track the highest (smallest Y value = highest on screen) value label
|
|
1304
1124
|
if (segment.value >= baseline && (highestValueLabelY === null || valueLabelY < highestValueLabelY)) {
|
|
1305
1125
|
highestValueLabelY = valueLabelY;
|
|
1306
1126
|
}
|
|
@@ -1316,20 +1136,16 @@ async function createBarChart(data, options = {}) {
|
|
|
1316
1136
|
});
|
|
1317
1137
|
}
|
|
1318
1138
|
});
|
|
1319
|
-
// Store the highest value label position for this grouped bar (for adjusting bar label position)
|
|
1320
1139
|
if (highestValueLabelY !== null) {
|
|
1321
1140
|
valueLabelPositions.set(data.indexOf(item), { y: highestValueLabelY, fontSize: valueFontSize, baseline: 'bottom' });
|
|
1322
1141
|
}
|
|
1323
1142
|
}
|
|
1324
1143
|
else if (chartType === 'waterfall') {
|
|
1325
|
-
// Waterfall: each bar starts from cumulative total of previous bars
|
|
1326
|
-
// Calculate cumulative value up to this point
|
|
1327
1144
|
let cumulativeValue = initialValue;
|
|
1328
1145
|
const currentIndex = data.indexOf(item);
|
|
1329
1146
|
for (let i = 0; i < currentIndex; i++) {
|
|
1330
1147
|
const prevItem = data[i];
|
|
1331
1148
|
if (prevItem.values && prevItem.values.length > 0) {
|
|
1332
|
-
// Sum all segments for previous item
|
|
1333
1149
|
const prevTotal = prevItem.values.reduce((sum, seg) => sum + seg.value, 0);
|
|
1334
1150
|
cumulativeValue += prevTotal;
|
|
1335
1151
|
}
|
|
@@ -1337,9 +1153,7 @@ async function createBarChart(data, options = {}) {
|
|
|
1337
1153
|
cumulativeValue += prevItem.value;
|
|
1338
1154
|
}
|
|
1339
1155
|
}
|
|
1340
|
-
// Calculate baseline Y position for this cumulative value
|
|
1341
1156
|
const cumulativeBaselineY = originY - ((cumulativeValue - minValue) / (maxValue - minValue)) * chartAreaHeight;
|
|
1342
|
-
// Separate positive and negative segments
|
|
1343
1157
|
const positiveSegments = [];
|
|
1344
1158
|
const negativeSegments = [];
|
|
1345
1159
|
segments.forEach(seg => {
|
|
@@ -1350,17 +1164,14 @@ async function createBarChart(data, options = {}) {
|
|
|
1350
1164
|
negativeSegments.push(seg);
|
|
1351
1165
|
}
|
|
1352
1166
|
});
|
|
1353
|
-
// Draw positive segments (stacked upward from cumulative baseline)
|
|
1354
1167
|
let accumulatedPositiveHeight = 0;
|
|
1355
1168
|
positiveSegments.forEach((segment) => {
|
|
1356
1169
|
const positiveRatio = segment.value / (maxValue - minValue);
|
|
1357
1170
|
const segmentHeight = positiveRatio * chartAreaHeight;
|
|
1358
1171
|
const barY = cumulativeBaselineY - accumulatedPositiveHeight - segmentHeight;
|
|
1359
|
-
// Ensure bar stays within chart area bounds
|
|
1360
1172
|
const clampedBarY = Math.max(axisEndY, barY);
|
|
1361
1173
|
const clampedBarHeight = Math.min(segmentHeight, cumulativeBaselineY - accumulatedPositiveHeight - clampedBarY);
|
|
1362
1174
|
if (clampedBarHeight > 0) {
|
|
1363
|
-
// Ensure bar doesn't exceed X-axis bounds
|
|
1364
1175
|
const clampedBarXStart = Math.max(originX, Math.min(barXStart, axisEndX));
|
|
1365
1176
|
const clampedGroupWidth = Math.min(groupWidth, axisEndX - clampedBarXStart);
|
|
1366
1177
|
if (clampedGroupWidth > 0) {
|
|
@@ -1382,17 +1193,14 @@ async function createBarChart(data, options = {}) {
|
|
|
1382
1193
|
}
|
|
1383
1194
|
accumulatedPositiveHeight += segmentHeight;
|
|
1384
1195
|
});
|
|
1385
|
-
// Draw negative segments (stacked downward from cumulative baseline)
|
|
1386
1196
|
let accumulatedNegativeHeight = 0;
|
|
1387
1197
|
negativeSegments.forEach((segment) => {
|
|
1388
1198
|
const negativeRatio = Math.abs(segment.value) / (maxValue - minValue);
|
|
1389
1199
|
const segmentHeight = negativeRatio * chartAreaHeight;
|
|
1390
1200
|
const barY = cumulativeBaselineY + accumulatedNegativeHeight;
|
|
1391
|
-
// Ensure bar stays within chart area bounds
|
|
1392
1201
|
const clampedBarY = Math.max(barY, axisEndY);
|
|
1393
1202
|
const clampedBarHeight = Math.min(segmentHeight, originY - clampedBarY);
|
|
1394
1203
|
if (clampedBarHeight > 0) {
|
|
1395
|
-
// Ensure bar doesn't exceed X-axis bounds
|
|
1396
1204
|
const clampedBarXStart = Math.max(originX, Math.min(barXStart, axisEndX));
|
|
1397
1205
|
const clampedGroupWidth = Math.min(groupWidth, axisEndX - clampedBarXStart);
|
|
1398
1206
|
if (clampedGroupWidth > 0) {
|
|
@@ -1416,8 +1224,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1416
1224
|
});
|
|
1417
1225
|
}
|
|
1418
1226
|
else {
|
|
1419
|
-
// Stacked: bars on top of each other
|
|
1420
|
-
// For stacked with negatives, we need to separate positive and negative segments
|
|
1421
1227
|
const positiveSegments = [];
|
|
1422
1228
|
const negativeSegments = [];
|
|
1423
1229
|
segments.forEach(seg => {
|
|
@@ -1428,7 +1234,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1428
1234
|
negativeSegments.push(seg);
|
|
1429
1235
|
}
|
|
1430
1236
|
});
|
|
1431
|
-
// Draw positive segments (stacked upward from baseline)
|
|
1432
1237
|
let accumulatedPositiveHeight = 0;
|
|
1433
1238
|
positiveSegments.forEach((segment) => {
|
|
1434
1239
|
const positiveRatio = (segment.value - baseline) / (maxValue - minValue);
|
|
@@ -1450,7 +1255,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1450
1255
|
}
|
|
1451
1256
|
accumulatedPositiveHeight += segmentHeight;
|
|
1452
1257
|
});
|
|
1453
|
-
// Draw negative segments (stacked downward from baseline)
|
|
1454
1258
|
let accumulatedNegativeHeight = 0;
|
|
1455
1259
|
negativeSegments.forEach((segment) => {
|
|
1456
1260
|
const negativeRatio = (baseline - segment.value) / (maxValue - minValue);
|
|
@@ -1472,13 +1276,11 @@ async function createBarChart(data, options = {}) {
|
|
|
1472
1276
|
}
|
|
1473
1277
|
accumulatedNegativeHeight += segmentHeight;
|
|
1474
1278
|
});
|
|
1475
|
-
// Store total value label for later drawing
|
|
1476
1279
|
const totalValue = segments.reduce((sum, seg) => sum + seg.value, 0);
|
|
1477
1280
|
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1478
1281
|
if (shouldShowValue) {
|
|
1479
1282
|
const totalValueY = totalValue >= baseline ? baselineY - accumulatedPositiveHeight - 5 : baselineY + accumulatedNegativeHeight + 5;
|
|
1480
1283
|
const totalValueBaseline = totalValue >= baseline ? 'bottom' : 'top';
|
|
1481
|
-
// Store value label position for this bar (for adjusting bar label position)
|
|
1482
1284
|
if (totalValue >= baseline) {
|
|
1483
1285
|
valueLabelPositions.set(data.indexOf(item), { y: totalValueY, fontSize: valueFontSize, baseline: totalValueBaseline });
|
|
1484
1286
|
}
|
|
@@ -1496,22 +1298,17 @@ async function createBarChart(data, options = {}) {
|
|
|
1496
1298
|
}
|
|
1497
1299
|
}
|
|
1498
1300
|
else if (chartType === 'lollipop') {
|
|
1499
|
-
// Lollipop chart: line with dot at end
|
|
1500
1301
|
const barCenterX = barXStart + groupWidth / 2;
|
|
1501
1302
|
const value = item.value ?? baseline;
|
|
1502
|
-
// Calculate value Y position
|
|
1503
1303
|
let valueY;
|
|
1504
1304
|
if (value >= baseline) {
|
|
1505
|
-
// Value above baseline
|
|
1506
1305
|
const positiveRatio = (value - baseline) / (maxValue - minValue);
|
|
1507
1306
|
valueY = baselineY - positiveRatio * chartAreaHeight;
|
|
1508
1307
|
}
|
|
1509
1308
|
else {
|
|
1510
|
-
// Value below baseline
|
|
1511
1309
|
const negativeRatio = (baseline - value) / (maxValue - minValue);
|
|
1512
1310
|
valueY = baselineY + negativeRatio * chartAreaHeight;
|
|
1513
1311
|
}
|
|
1514
|
-
// Draw line from baseline to value position
|
|
1515
1312
|
ctx.save();
|
|
1516
1313
|
ctx.strokeStyle = item.color || '#4A90E2';
|
|
1517
1314
|
ctx.lineWidth = lollipopLineWidth;
|
|
@@ -1519,13 +1316,11 @@ async function createBarChart(data, options = {}) {
|
|
|
1519
1316
|
ctx.moveTo(barCenterX, baselineY);
|
|
1520
1317
|
ctx.lineTo(barCenterX, valueY);
|
|
1521
1318
|
ctx.stroke();
|
|
1522
|
-
// Draw dot/circle at value position with opacity, shadow, and stroke
|
|
1523
1319
|
ctx.save();
|
|
1524
1320
|
const dotOpacity = item.opacity ?? globalBarOpacity;
|
|
1525
1321
|
if (dotOpacity !== undefined) {
|
|
1526
1322
|
ctx.globalAlpha = dotOpacity;
|
|
1527
1323
|
}
|
|
1528
|
-
// Apply shadow
|
|
1529
1324
|
const dotShadow = item.shadow || globalBarShadow;
|
|
1530
1325
|
if (dotShadow) {
|
|
1531
1326
|
ctx.shadowColor = dotShadow.color || 'rgba(0,0,0,0.3)';
|
|
@@ -1537,14 +1332,12 @@ async function createBarChart(data, options = {}) {
|
|
|
1537
1332
|
ctx.arc(barCenterX, valueY, lollipopDotSize / 2, 0, Math.PI * 2);
|
|
1538
1333
|
fillWithGradientOrColor(ctx, item.gradient, item.color || '#4A90E2', '#4A90E2', { x: barCenterX - lollipopDotSize / 2, y: valueY - lollipopDotSize / 2, w: lollipopDotSize, h: lollipopDotSize });
|
|
1539
1334
|
ctx.fill();
|
|
1540
|
-
// Reset shadow before stroke
|
|
1541
1335
|
if (dotShadow) {
|
|
1542
1336
|
ctx.shadowColor = 'transparent';
|
|
1543
1337
|
ctx.shadowOffsetX = 0;
|
|
1544
1338
|
ctx.shadowOffsetY = 0;
|
|
1545
1339
|
ctx.shadowBlur = 0;
|
|
1546
1340
|
}
|
|
1547
|
-
// Draw dot border/stroke
|
|
1548
1341
|
const dotStroke = item.stroke || globalBarStroke;
|
|
1549
1342
|
if (dotStroke && dotStroke.width && dotStroke.width > 0) {
|
|
1550
1343
|
ctx.beginPath();
|
|
@@ -1564,13 +1357,11 @@ async function createBarChart(data, options = {}) {
|
|
|
1564
1357
|
ctx.stroke();
|
|
1565
1358
|
}
|
|
1566
1359
|
else {
|
|
1567
|
-
// Default border for better visibility if no stroke specified
|
|
1568
1360
|
ctx.strokeStyle = item.color || '#4A90E2';
|
|
1569
1361
|
ctx.lineWidth = 1;
|
|
1570
1362
|
ctx.stroke();
|
|
1571
1363
|
}
|
|
1572
1364
|
ctx.restore();
|
|
1573
|
-
// Store value label for later drawing
|
|
1574
1365
|
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1575
1366
|
if (shouldShowValue) {
|
|
1576
1367
|
labelsToDraw.push({
|
|
@@ -1586,32 +1377,25 @@ async function createBarChart(data, options = {}) {
|
|
|
1586
1377
|
}
|
|
1587
1378
|
}
|
|
1588
1379
|
else {
|
|
1589
|
-
// Standard chart: single bar
|
|
1590
1380
|
const barWidth = groupWidth;
|
|
1591
1381
|
const value = item.value ?? baseline;
|
|
1592
|
-
// Calculate bar height and position based on value relative to baseline
|
|
1593
1382
|
let barHeight;
|
|
1594
1383
|
let barY;
|
|
1595
1384
|
if (value >= baseline) {
|
|
1596
|
-
// Value above baseline: bar goes up from baseline
|
|
1597
1385
|
const positiveRatio = (value - baseline) / (maxValue - minValue);
|
|
1598
1386
|
barHeight = positiveRatio * chartAreaHeight;
|
|
1599
1387
|
barY = baselineY - barHeight;
|
|
1600
1388
|
}
|
|
1601
1389
|
else {
|
|
1602
|
-
// Value below baseline: bar goes down from baseline
|
|
1603
1390
|
const negativeRatio = (baseline - value) / (maxValue - minValue);
|
|
1604
1391
|
barHeight = negativeRatio * chartAreaHeight;
|
|
1605
1392
|
barY = baselineY;
|
|
1606
1393
|
}
|
|
1607
|
-
// Draw bar with gradient, opacity, shadow, and stroke
|
|
1608
1394
|
drawBar(ctx, barXStart, barY, barWidth, barHeight, item.color || '#4A90E2', item.gradient, item.opacity ?? globalBarOpacity, item.shadow, item.stroke, globalBarShadow, globalBarStroke);
|
|
1609
|
-
// Store value label for later drawing
|
|
1610
1395
|
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1611
1396
|
if (shouldShowValue) {
|
|
1612
1397
|
const valueLabelY = value >= baseline ? barY - 5 : barY + barHeight + 5;
|
|
1613
1398
|
const valueLabelBaseline = value >= baseline ? 'bottom' : 'top';
|
|
1614
|
-
// Store value label position for this bar (for adjusting bar label position)
|
|
1615
1399
|
if (value >= baseline) {
|
|
1616
1400
|
valueLabelPositions.set(data.indexOf(item), { y: valueLabelY, fontSize: valueFontSize, baseline: valueLabelBaseline });
|
|
1617
1401
|
}
|
|
@@ -1627,7 +1411,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1627
1411
|
});
|
|
1628
1412
|
}
|
|
1629
1413
|
}
|
|
1630
|
-
// Store bar label information for later drawing
|
|
1631
1414
|
if (showBarLabels) {
|
|
1632
1415
|
ctx.save();
|
|
1633
1416
|
ctx.fillStyle = item.labelColor || '#000000';
|
|
@@ -1635,33 +1418,26 @@ async function createBarChart(data, options = {}) {
|
|
|
1635
1418
|
let labelX, labelY;
|
|
1636
1419
|
let textAlign = 'center';
|
|
1637
1420
|
let textBaseline = 'middle';
|
|
1638
|
-
// Calculate bar center - use groupWidth for all chart types
|
|
1639
1421
|
const barCenterX = barXStart + groupWidth / 2;
|
|
1640
|
-
// For grouped/stacked, calculate appropriate center Y
|
|
1641
1422
|
let barCenterY;
|
|
1642
1423
|
if ((chartType === 'grouped' || chartType === 'stacked') && item.values && item.values.length > 0) {
|
|
1643
1424
|
if (chartType === 'stacked') {
|
|
1644
|
-
// For stacked, use the total height
|
|
1645
1425
|
const totalValue = item.values.reduce((sum, seg) => sum + seg.value, 0);
|
|
1646
1426
|
const totalHeight = ((totalValue - minValue) / (maxValue - minValue)) * chartAreaHeight;
|
|
1647
1427
|
barCenterY = originY - totalHeight / 2;
|
|
1648
1428
|
}
|
|
1649
1429
|
else {
|
|
1650
|
-
// For grouped, use the max value height
|
|
1651
1430
|
const maxSegValue = Math.max(...item.values.map(seg => seg.value));
|
|
1652
1431
|
const maxHeight = ((maxSegValue - minValue) / (maxValue - minValue)) * chartAreaHeight;
|
|
1653
1432
|
barCenterY = originY - maxHeight / 2;
|
|
1654
1433
|
}
|
|
1655
1434
|
}
|
|
1656
1435
|
else {
|
|
1657
|
-
// Standard chart
|
|
1658
1436
|
const value = item.value ?? 0;
|
|
1659
1437
|
const barHeight = ((value - minValue) / (maxValue - minValue)) * chartAreaHeight;
|
|
1660
1438
|
barCenterY = originY - barHeight / 2;
|
|
1661
1439
|
}
|
|
1662
|
-
// Use individual bar label position if provided, otherwise use global setting
|
|
1663
1440
|
const currentLabelPosition = item.labelPosition ?? barLabelPosition;
|
|
1664
|
-
// Calculate top Y position for label
|
|
1665
1441
|
let topBarY;
|
|
1666
1442
|
if ((chartType === 'grouped' || chartType === 'stacked') && item.values && item.values.length > 0) {
|
|
1667
1443
|
if (chartType === 'stacked') {
|
|
@@ -1683,13 +1459,9 @@ async function createBarChart(data, options = {}) {
|
|
|
1683
1459
|
switch (currentLabelPosition) {
|
|
1684
1460
|
case 'top':
|
|
1685
1461
|
labelX = barCenterX;
|
|
1686
|
-
// Check if there's a value label at the top - if so, position bar label below it
|
|
1687
1462
|
const valueLabelInfo = valueLabelPositions.get(data.indexOf(item));
|
|
1688
1463
|
if (valueLabelInfo && valueLabelInfo.baseline === 'bottom') {
|
|
1689
|
-
|
|
1690
|
-
// Value label uses 'bottom' baseline, so its top is at valueLabelInfo.y
|
|
1691
|
-
// Bar label uses 'bottom' baseline, so position it below the value label
|
|
1692
|
-
const spacing = 5; // Gap between value and bar label
|
|
1464
|
+
const spacing = 5;
|
|
1693
1465
|
labelY = valueLabelInfo.y - valueLabelInfo.fontSize - spacing;
|
|
1694
1466
|
}
|
|
1695
1467
|
else {
|
|
@@ -1721,9 +1493,7 @@ async function createBarChart(data, options = {}) {
|
|
|
1721
1493
|
labelY = barCenterY;
|
|
1722
1494
|
textAlign = 'center';
|
|
1723
1495
|
textBaseline = 'middle';
|
|
1724
|
-
// Use white or black text based on bar color for better visibility
|
|
1725
1496
|
const barColor = item.color || '#4A90E2';
|
|
1726
|
-
// Simple brightness check - if bar is dark, use white text
|
|
1727
1497
|
const isDark = barColor === '#000000' || barColor.toLowerCase().includes('dark') ||
|
|
1728
1498
|
(barColor.startsWith('#') && parseInt(barColor.slice(1, 3), 16) < 128);
|
|
1729
1499
|
ctx.fillStyle = isDark ? '#FFFFFF' : (item.labelColor || '#000000');
|
|
@@ -1734,7 +1504,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1734
1504
|
textAlign = 'center';
|
|
1735
1505
|
textBaseline = 'top';
|
|
1736
1506
|
}
|
|
1737
|
-
// Calculate label color (for 'inside' position, check if bar is dark)
|
|
1738
1507
|
let labelColor = item.labelColor || '#000000';
|
|
1739
1508
|
if (currentLabelPosition === 'inside') {
|
|
1740
1509
|
const barColor = item.color || '#4A90E2';
|
|
@@ -1742,7 +1511,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1742
1511
|
(barColor.startsWith('#') && parseInt(barColor.slice(1, 3), 16) < 128);
|
|
1743
1512
|
labelColor = isDark ? '#FFFFFF' : (item.labelColor || '#000000');
|
|
1744
1513
|
}
|
|
1745
|
-
// Store bar label for later drawing
|
|
1746
1514
|
labelsToDraw.push({
|
|
1747
1515
|
type: 'bar',
|
|
1748
1516
|
text: item.label,
|
|
@@ -1755,12 +1523,10 @@ async function createBarChart(data, options = {}) {
|
|
|
1755
1523
|
});
|
|
1756
1524
|
}
|
|
1757
1525
|
});
|
|
1758
|
-
// Second pass: Draw all labels (values and bar labels) on top of everything
|
|
1759
1526
|
for (const label of labelsToDraw) {
|
|
1760
1527
|
ctx.save();
|
|
1761
1528
|
ctx.textAlign = label.align;
|
|
1762
1529
|
ctx.textBaseline = label.baseline;
|
|
1763
|
-
// Determine text style and gradient based on label type
|
|
1764
1530
|
let textStyle;
|
|
1765
1531
|
let textGradient;
|
|
1766
1532
|
if (label.type === 'bar') {
|
|
@@ -1776,7 +1542,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1776
1542
|
}
|
|
1777
1543
|
return canvas.toBuffer('image/png');
|
|
1778
1544
|
}
|
|
1779
|
-
// Example usage with organized, categorized configuration:
|
|
1780
1545
|
(async () => {
|
|
1781
1546
|
const chart = await createBarChart([
|
|
1782
1547
|
{
|
|
@@ -1813,9 +1578,7 @@ async function createBarChart(data, options = {}) {
|
|
|
1813
1578
|
showValue: true
|
|
1814
1579
|
}
|
|
1815
1580
|
], {
|
|
1816
|
-
|
|
1817
|
-
type: 'standard', // 'standard' | 'grouped' | 'stacked' | 'horizontal'
|
|
1818
|
-
// Dimensions
|
|
1581
|
+
type: 'standard',
|
|
1819
1582
|
dimensions: {
|
|
1820
1583
|
height: 600,
|
|
1821
1584
|
padding: {
|
|
@@ -1825,34 +1588,28 @@ async function createBarChart(data, options = {}) {
|
|
|
1825
1588
|
left: 100
|
|
1826
1589
|
}
|
|
1827
1590
|
},
|
|
1828
|
-
// Appearance
|
|
1829
1591
|
appearance: {
|
|
1830
1592
|
backgroundColor: 'white',
|
|
1831
|
-
// backgroundImage: './path/to/background.png', // Optional
|
|
1832
1593
|
axisColor: '#000000',
|
|
1833
1594
|
axisWidth: 2,
|
|
1834
1595
|
arrowSize: 10
|
|
1835
1596
|
},
|
|
1836
|
-
// Axes Configuration
|
|
1837
1597
|
axes: {
|
|
1838
1598
|
x: {
|
|
1839
1599
|
label: 'Day',
|
|
1840
1600
|
labelColor: 'black',
|
|
1841
1601
|
values: [24, 25, 26, 27, 28, 29, 30, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
|
|
1842
|
-
// OR use range: { min: 0, max: 100, step: 20 }
|
|
1843
1602
|
tickFontSize: 10,
|
|
1844
|
-
valueSpacing: 5
|
|
1603
|
+
valueSpacing: 5
|
|
1845
1604
|
},
|
|
1846
1605
|
y: {
|
|
1847
1606
|
label: 'Count',
|
|
1848
1607
|
labelColor: 'black',
|
|
1849
1608
|
values: [0, 2, 4, 6, 8, 10, 12, 14],
|
|
1850
|
-
// OR use range: { min: 0, max: 14, step: 2 }
|
|
1851
1609
|
tickFontSize: 10,
|
|
1852
|
-
valueSpacing: 3
|
|
1610
|
+
valueSpacing: 3
|
|
1853
1611
|
}
|
|
1854
1612
|
},
|
|
1855
|
-
// Labels & Text
|
|
1856
1613
|
labels: {
|
|
1857
1614
|
title: {
|
|
1858
1615
|
text: 'Joined Members',
|
|
@@ -1860,18 +1617,17 @@ async function createBarChart(data, options = {}) {
|
|
|
1860
1617
|
color: '#000000'
|
|
1861
1618
|
},
|
|
1862
1619
|
barLabelDefaults: {
|
|
1863
|
-
show: true,
|
|
1864
|
-
defaultPosition: 'bottom',
|
|
1620
|
+
show: true,
|
|
1621
|
+
defaultPosition: 'bottom',
|
|
1865
1622
|
fontSize: 12,
|
|
1866
|
-
defaultColor: '#000000'
|
|
1623
|
+
defaultColor: '#000000'
|
|
1867
1624
|
},
|
|
1868
1625
|
valueLabelDefaults: {
|
|
1869
|
-
show: true,
|
|
1626
|
+
show: true,
|
|
1870
1627
|
fontSize: 11,
|
|
1871
|
-
defaultColor: '#000000'
|
|
1628
|
+
defaultColor: '#000000'
|
|
1872
1629
|
}
|
|
1873
1630
|
},
|
|
1874
|
-
// Legend (always positioned at top)
|
|
1875
1631
|
legend: {
|
|
1876
1632
|
show: true,
|
|
1877
1633
|
entries: [
|
|
@@ -1879,7 +1635,6 @@ async function createBarChart(data, options = {}) {
|
|
|
1879
1635
|
{ color: '#4A90E2', label: 'Bots' }
|
|
1880
1636
|
]
|
|
1881
1637
|
},
|
|
1882
|
-
// Grid
|
|
1883
1638
|
grid: {
|
|
1884
1639
|
show: true,
|
|
1885
1640
|
color: '#E0E0E0',
|