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
|
@@ -8,7 +8,6 @@ const imageProperties_1 = require("../Image/imageProperties");
|
|
|
8
8
|
*/
|
|
9
9
|
async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textGradient) {
|
|
10
10
|
ctx.save();
|
|
11
|
-
// Preserve text alignment settings
|
|
12
11
|
const savedTextAlign = ctx.textAlign;
|
|
13
12
|
const savedTextBaseline = ctx.textBaseline;
|
|
14
13
|
const effectiveFontSize = fontSize || style?.fontSize || 16;
|
|
@@ -18,30 +17,24 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
18
17
|
fontString += 'bold ';
|
|
19
18
|
if (style?.italic)
|
|
20
19
|
fontString += 'italic ';
|
|
21
|
-
// Use font family without quotes for better rendering (quotes can cause rendering issues)
|
|
22
20
|
fontString += `${effectiveFontSize}px ${fontFamily}`;
|
|
23
21
|
ctx.font = fontString;
|
|
24
|
-
// Reset letter and word spacing to prevent stretched text
|
|
25
22
|
ctx.letterSpacing = '0px';
|
|
26
23
|
ctx.wordSpacing = '0px';
|
|
27
|
-
// Restore text alignment to ensure correct positioning
|
|
28
24
|
ctx.textAlign = savedTextAlign;
|
|
29
25
|
ctx.textBaseline = savedTextBaseline;
|
|
30
|
-
// Register custom font if provided
|
|
31
26
|
if (style?.fontPath && style?.fontName) {
|
|
32
27
|
try {
|
|
33
28
|
const { GlobalFonts } = await import('@napi-rs/canvas');
|
|
34
29
|
const path = await import('path');
|
|
35
30
|
const fullPath = path.join(process.cwd(), style.fontPath);
|
|
36
31
|
GlobalFonts.registerFromPath(fullPath, style.fontName);
|
|
37
|
-
// Replace font family in font string (handle both quoted and unquoted formats)
|
|
38
32
|
ctx.font = fontString.replace(fontFamily, style.fontName);
|
|
39
33
|
}
|
|
40
34
|
catch (error) {
|
|
41
35
|
console.warn(`Failed to register font: ${style.fontPath}`, error);
|
|
42
36
|
}
|
|
43
37
|
}
|
|
44
|
-
// Apply shadow
|
|
45
38
|
if (style?.shadow) {
|
|
46
39
|
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.5)';
|
|
47
40
|
ctx.shadowOffsetX = style.shadow.offsetX || 2;
|
|
@@ -51,7 +44,6 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
51
44
|
ctx.globalAlpha = style.shadow.opacity;
|
|
52
45
|
}
|
|
53
46
|
}
|
|
54
|
-
// Set fill style (gradient or color)
|
|
55
47
|
if (textGradient) {
|
|
56
48
|
const metrics = ctx.measureText(text);
|
|
57
49
|
ctx.fillStyle = (0, imageProperties_1.createGradientFill)(ctx, textGradient, {
|
|
@@ -61,9 +53,7 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
61
53
|
else if (color) {
|
|
62
54
|
ctx.fillStyle = color;
|
|
63
55
|
}
|
|
64
|
-
// Draw text
|
|
65
56
|
ctx.fillText(text, x, y);
|
|
66
|
-
// Apply stroke
|
|
67
57
|
if (style?.stroke) {
|
|
68
58
|
ctx.strokeStyle = style.stroke.color || '#000000';
|
|
69
59
|
ctx.lineWidth = style.stroke.width || 1;
|
|
@@ -75,7 +65,6 @@ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textG
|
|
|
75
65
|
}
|
|
76
66
|
ctx.strokeText(text, x, y);
|
|
77
67
|
}
|
|
78
|
-
// Reset shadow and alpha
|
|
79
68
|
ctx.shadowColor = 'transparent';
|
|
80
69
|
ctx.shadowOffsetX = 0;
|
|
81
70
|
ctx.shadowOffsetY = 0;
|
|
@@ -120,18 +109,15 @@ function drawXAxisTicks(ctx, originX, originY, axisEndX, minValue, maxValue, ste
|
|
|
120
109
|
ctx.textBaseline = 'top';
|
|
121
110
|
const chartWidth = axisEndX - originX;
|
|
122
111
|
if (customValues && customValues.length > 0) {
|
|
123
|
-
// Position labels based on their actual values, not pixel spacing
|
|
124
112
|
const actualMin = Math.min(...customValues);
|
|
125
113
|
const actualMax = Math.max(...customValues);
|
|
126
|
-
const range = actualMax - actualMin || 1;
|
|
114
|
+
const range = actualMax - actualMin || 1;
|
|
127
115
|
let lastLabelX = -Infinity;
|
|
128
|
-
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40;
|
|
116
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40;
|
|
129
117
|
customValues.forEach((value) => {
|
|
130
118
|
const x = originX + ((value - actualMin) / range) * chartWidth;
|
|
131
119
|
const labelText = value.toString();
|
|
132
|
-
// Check if this label would overlap with the previous one
|
|
133
120
|
if (x - lastLabelX < minLabelSpacing && value > actualMin) {
|
|
134
|
-
// Skip this label to prevent overlap (but still draw tick mark)
|
|
135
121
|
ctx.beginPath();
|
|
136
122
|
ctx.moveTo(x, originY);
|
|
137
123
|
ctx.lineTo(x, originY + 5);
|
|
@@ -142,45 +128,36 @@ function drawXAxisTicks(ctx, originX, originY, axisEndX, minValue, maxValue, ste
|
|
|
142
128
|
ctx.moveTo(x, originY);
|
|
143
129
|
ctx.lineTo(x, originY + 5);
|
|
144
130
|
ctx.stroke();
|
|
145
|
-
// Measure label to ensure it doesn't overflow, add minimum 2px spacing
|
|
146
131
|
const labelMetrics = ctx.measureText(labelText);
|
|
147
|
-
const labelY = originY + 10;
|
|
132
|
+
const labelY = originY + 10;
|
|
148
133
|
ctx.fillText(labelText, x, labelY);
|
|
149
|
-
lastLabelX = x;
|
|
134
|
+
lastLabelX = x;
|
|
150
135
|
});
|
|
151
136
|
}
|
|
152
137
|
else {
|
|
153
|
-
|
|
154
|
-
const range = maxValue - minValue || 1; // Avoid division by zero
|
|
155
|
-
// Calculate all tick positions first
|
|
138
|
+
const range = maxValue - minValue || 1;
|
|
156
139
|
const tickValues = [];
|
|
157
140
|
for (let value = minValue; value <= maxValue; value += step) {
|
|
158
141
|
tickValues.push(value);
|
|
159
142
|
}
|
|
160
|
-
// Draw ticks, but skip labels if they're too close together
|
|
161
143
|
let lastLabelX = -Infinity;
|
|
162
|
-
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40;
|
|
144
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40;
|
|
163
145
|
for (const value of tickValues) {
|
|
164
146
|
const x = originX + ((value - minValue) / range) * chartWidth;
|
|
165
147
|
const labelText = value.toString();
|
|
166
|
-
// Check if this label center is too close to the previous label center
|
|
167
148
|
if (x - lastLabelX < minLabelSpacing && value > minValue) {
|
|
168
|
-
// Skip this label to prevent overlap - but still draw the tick mark
|
|
169
149
|
ctx.beginPath();
|
|
170
150
|
ctx.moveTo(x, originY);
|
|
171
151
|
ctx.lineTo(x, originY + 5);
|
|
172
152
|
ctx.stroke();
|
|
173
153
|
continue;
|
|
174
154
|
}
|
|
175
|
-
// Draw tick mark
|
|
176
155
|
ctx.beginPath();
|
|
177
156
|
ctx.moveTo(x, originY);
|
|
178
157
|
ctx.lineTo(x, originY + 5);
|
|
179
158
|
ctx.stroke();
|
|
180
|
-
|
|
181
|
-
const labelY = originY + 10; // 10px spacing from axis line
|
|
159
|
+
const labelY = originY + 10;
|
|
182
160
|
ctx.fillText(labelText, x, labelY);
|
|
183
|
-
// Update last label position (center of the label)
|
|
184
161
|
lastLabelX = x;
|
|
185
162
|
}
|
|
186
163
|
}
|
|
@@ -218,7 +195,6 @@ function drawYAxisTicks(ctx, originX, originY, axisEndY, minValue, maxValue, ste
|
|
|
218
195
|
});
|
|
219
196
|
}
|
|
220
197
|
else {
|
|
221
|
-
// Position based on value range
|
|
222
198
|
const range = maxValue - minValue || 1;
|
|
223
199
|
customValues.forEach((value) => {
|
|
224
200
|
const y = originY - ((value - minValue) / range) * chartHeight;
|
|
@@ -231,16 +207,13 @@ function drawYAxisTicks(ctx, originX, originY, axisEndY, minValue, maxValue, ste
|
|
|
231
207
|
}
|
|
232
208
|
}
|
|
233
209
|
else {
|
|
234
|
-
// Range-based positioning
|
|
235
210
|
const range = maxValue - minValue || 1;
|
|
236
211
|
let lastLabelY = Infinity;
|
|
237
|
-
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30;
|
|
212
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30;
|
|
238
213
|
for (let value = minValue; value <= maxValue; value += step) {
|
|
239
214
|
const y = originY - ((value - minValue) / range) * chartHeight;
|
|
240
215
|
const labelText = value.toString();
|
|
241
|
-
// Check if this label would overlap with the previous one
|
|
242
216
|
if (lastLabelY - y < minLabelSpacing && value > minValue) {
|
|
243
|
-
// Skip this label to prevent overlap - but still draw the tick mark
|
|
244
217
|
ctx.beginPath();
|
|
245
218
|
ctx.moveTo(originX - 5, y);
|
|
246
219
|
ctx.lineTo(originX, y);
|
|
@@ -252,7 +225,7 @@ function drawYAxisTicks(ctx, originX, originY, axisEndY, minValue, maxValue, ste
|
|
|
252
225
|
ctx.lineTo(originX, y);
|
|
253
226
|
ctx.stroke();
|
|
254
227
|
ctx.fillText(labelText, originX - 10, y);
|
|
255
|
-
lastLabelY = y;
|
|
228
|
+
lastLabelY = y;
|
|
256
229
|
}
|
|
257
230
|
}
|
|
258
231
|
ctx.restore();
|
|
@@ -267,7 +240,6 @@ function drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep,
|
|
|
267
240
|
ctx.setLineDash([2, 2]);
|
|
268
241
|
const chartWidth = axisEndX - originX;
|
|
269
242
|
const chartHeight = originY - axisEndY;
|
|
270
|
-
// Draw vertical grid lines (based on X-axis values)
|
|
271
243
|
if (xAxisCustomValues && xAxisCustomValues.length > 0) {
|
|
272
244
|
const actualMin = Math.min(...xAxisCustomValues);
|
|
273
245
|
const actualMax = Math.max(...xAxisCustomValues);
|
|
@@ -290,7 +262,6 @@ function drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep,
|
|
|
290
262
|
ctx.stroke();
|
|
291
263
|
}
|
|
292
264
|
}
|
|
293
|
-
// Draw horizontal grid lines (based on Y-axis values/range)
|
|
294
265
|
if (yAxisCustomValues && yAxisCustomValues.length > 0) {
|
|
295
266
|
const actualMin = Math.min(...yAxisCustomValues);
|
|
296
267
|
const actualMax = Math.max(...yAxisCustomValues);
|
|
@@ -345,7 +316,6 @@ function calculateLegendDimensions(legend, fontSize, maxWidth, wrapTextEnabled =
|
|
|
345
316
|
const boxSize = 15;
|
|
346
317
|
const spacing = 10;
|
|
347
318
|
const padding = paddingBox;
|
|
348
|
-
// Create a temporary canvas to measure text
|
|
349
319
|
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
350
320
|
const tempCtx = tempCanvas.getContext('2d');
|
|
351
321
|
tempCtx.font = `${fontSize}px Arial`;
|
|
@@ -384,12 +354,10 @@ async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, bac
|
|
|
384
354
|
const spacing = 10;
|
|
385
355
|
const padding = paddingBox ?? 8;
|
|
386
356
|
ctx.font = `${fontSize}px Arial`;
|
|
387
|
-
// Determine colors
|
|
388
357
|
const isDarkBackground = backgroundColor === '#000000' || backgroundColor.toLowerCase() === 'black';
|
|
389
358
|
const effectiveTextColor = textColor ?? (isDarkBackground ? '#FFFFFF' : '#000000');
|
|
390
359
|
const effectiveBgColor = isDarkBackground ? 'rgba(0, 0, 0, 0.8)' : (backgroundColor.startsWith('rgba') || backgroundColor.startsWith('rgb') ? backgroundColor : 'rgba(255, 255, 255, 0.9)');
|
|
391
360
|
const effectiveBorderColor = borderColor ?? (isDarkBackground ? '#FFFFFF' : '#000000');
|
|
392
|
-
// Calculate dimensions with text wrapping support
|
|
393
361
|
const textSpacing = 10;
|
|
394
362
|
const effectiveMaxWidth = maxWidth ? maxWidth - padding * 2 - boxSize - textSpacing : undefined;
|
|
395
363
|
let maxEntryWidth = 0;
|
|
@@ -412,7 +380,6 @@ async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, bac
|
|
|
412
380
|
});
|
|
413
381
|
const legendWidth = maxWidth ? maxWidth : maxEntryWidth + padding * 2;
|
|
414
382
|
const legendHeight = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? spacing : 0), 0) + padding * 2;
|
|
415
|
-
// Draw legend background (gradient or color)
|
|
416
383
|
ctx.beginPath();
|
|
417
384
|
ctx.rect(legendX, legendY, legendWidth, legendHeight);
|
|
418
385
|
fillWithGradientOrColor(ctx, backgroundGradient, effectiveBgColor, effectiveBgColor, { x: legendX, y: legendY, w: legendWidth, h: legendHeight });
|
|
@@ -427,7 +394,6 @@ async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, bac
|
|
|
427
394
|
const entry = legend[index];
|
|
428
395
|
const entryHeight = entryHeights[index];
|
|
429
396
|
const centerY = currentY + entryHeight / 2;
|
|
430
|
-
// Draw color box (gradient or color)
|
|
431
397
|
ctx.beginPath();
|
|
432
398
|
ctx.rect(legendX + padding, centerY - boxSize / 2, boxSize, boxSize);
|
|
433
399
|
fillWithGradientOrColor(ctx, entry.gradient, entry.color || '#4A90E2', '#4A90E2', { x: legendX + padding, y: centerY - boxSize / 2, w: boxSize, h: boxSize });
|
|
@@ -522,25 +488,21 @@ function calculateResponsiveHeight(dataLength, options = {}) {
|
|
|
522
488
|
const paddingTop = padding.top ?? 60;
|
|
523
489
|
const paddingBottom = padding.bottom ?? 80;
|
|
524
490
|
const minBarHeight = options.bars?.minHeight ?? 40;
|
|
525
|
-
const barSpacing = options.bars?.spacing ?? 15;
|
|
526
|
-
// Calculate minimum height needed: (number of bars * bar height) + (spacing between bars)
|
|
527
|
-
// Each bar needs minBarHeight, and between each pair of bars we need barSpacing
|
|
491
|
+
const barSpacing = options.bars?.spacing ?? 15;
|
|
528
492
|
const chartAreaHeight = dataLength * minBarHeight + (dataLength - 1) * barSpacing;
|
|
529
|
-
// Add title height if needed (with margin below title)
|
|
530
493
|
const titleHeight = options.labels?.title?.text ? (options.labels.title.fontSize ?? 24) + 20 : 0;
|
|
531
|
-
const titleMargin = options.labels?.title?.text ? 20 : 0;
|
|
532
|
-
// Calculate x-axis label area height (tick labels + gap + axis label)
|
|
494
|
+
const titleMargin = options.labels?.title?.text ? 20 : 0;
|
|
533
495
|
const tickFontSize = options.axes?.x?.tickFontSize ?? options.axes?.y?.tickFontSize ?? 12;
|
|
534
496
|
const axisLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
|
|
535
497
|
let xAxisLabelAreaHeight = 0;
|
|
536
498
|
if (options.axes?.x?.label || options.axes?.x?.values || options.axes?.x?.range) {
|
|
537
|
-
const tickLabelHeight = tickFontSize + 10;
|
|
538
|
-
const xAxisLabelTextHeight = options.axes?.x?.label ? axisLabelFontSize + 2 : 0;
|
|
539
|
-
xAxisLabelAreaHeight = tickLabelHeight + (options.axes?.x?.label ? 8 : 0) + xAxisLabelTextHeight;
|
|
499
|
+
const tickLabelHeight = tickFontSize + 10;
|
|
500
|
+
const xAxisLabelTextHeight = options.axes?.x?.label ? axisLabelFontSize + 2 : 0;
|
|
501
|
+
xAxisLabelAreaHeight = tickLabelHeight + (options.axes?.x?.label ? 8 : 0) + xAxisLabelTextHeight;
|
|
540
502
|
}
|
|
541
503
|
const yAxisLabelHeight = options.axes?.y?.label ? axisLabelFontSize + 20 : 0;
|
|
542
504
|
const axisLabelHeight = Math.max(xAxisLabelAreaHeight, yAxisLabelHeight);
|
|
543
|
-
const minBottomGap = 2;
|
|
505
|
+
const minBottomGap = 2;
|
|
544
506
|
return paddingTop + titleHeight + titleMargin + chartAreaHeight + axisLabelHeight + minBottomGap + paddingBottom;
|
|
545
507
|
}
|
|
546
508
|
/**
|
|
@@ -550,17 +512,14 @@ function calculateResponsiveHeight(dataLength, options = {}) {
|
|
|
550
512
|
* @returns Canvas buffer
|
|
551
513
|
*/
|
|
552
514
|
async function createHorizontalBarChart(data, options = {}) {
|
|
553
|
-
// Extract and map organized config to internal variables
|
|
554
515
|
let width = options.dimensions?.width ?? 800;
|
|
555
516
|
const padding = options.dimensions?.padding || {};
|
|
556
|
-
// Appearance
|
|
557
517
|
const backgroundColor = options.appearance?.backgroundColor ?? '#FFFFFF';
|
|
558
518
|
const backgroundGradient = options.appearance?.backgroundGradient;
|
|
559
519
|
const backgroundImage = options.appearance?.backgroundImage;
|
|
560
520
|
const axisColor = options.appearance?.axisColor ?? options.axes?.x?.color ?? options.axes?.y?.color ?? '#000000';
|
|
561
521
|
const axisWidth = options.appearance?.axisWidth ?? options.axes?.x?.width ?? options.axes?.y?.width ?? 2;
|
|
562
522
|
const arrowSize = options.appearance?.arrowSize ?? 10;
|
|
563
|
-
// Labels
|
|
564
523
|
const chartTitle = options.labels?.title?.text;
|
|
565
524
|
const chartTitleFontSize = options.labels?.title?.fontSize ?? 24;
|
|
566
525
|
const showBarLabels = options.labels?.barLabelDefaults?.show ?? true;
|
|
@@ -569,48 +528,41 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
569
528
|
const showValues = options.labels?.valueLabelDefaults?.show ?? true;
|
|
570
529
|
const valueFontSize = options.labels?.valueLabelDefaults?.fontSize ?? 12;
|
|
571
530
|
const valueColor = options.labels?.valueLabelDefaults?.defaultColor ?? '#000000';
|
|
572
|
-
// Axes
|
|
573
531
|
const xAxisLabel = options.axes?.x?.label;
|
|
574
532
|
const yAxisLabel = options.axes?.y?.label;
|
|
575
533
|
const axisLabelColor = options.axes?.x?.labelColor ?? options.axes?.y?.labelColor ?? '#000000';
|
|
576
534
|
const xAxisRange = options.axes?.x?.range;
|
|
577
535
|
const xAxisValues = options.axes?.x?.values;
|
|
578
|
-
const baseline = options.axes?.x?.baseline ?? 0;
|
|
536
|
+
const baseline = options.axes?.x?.baseline ?? 0;
|
|
579
537
|
const yAxisValues = options.axes?.y?.values;
|
|
580
538
|
const tickFontSize = options.axes?.x?.tickFontSize ?? options.axes?.y?.tickFontSize ?? 12;
|
|
581
539
|
const xAxisTickColor = options.axes?.x?.tickColor ?? '#000000';
|
|
582
540
|
const yAxisTickColor = options.axes?.y?.tickColor ?? '#000000';
|
|
583
541
|
const xAxisValueSpacing = options.axes?.x?.valueSpacing;
|
|
584
542
|
const yAxisValueSpacing = options.axes?.y?.valueSpacing;
|
|
585
|
-
// Legend
|
|
586
543
|
const showLegend = options.legend?.show ?? false;
|
|
587
544
|
const legend = options.legend?.entries;
|
|
588
|
-
const legendPosition = options.legend?.position ?? 'right';
|
|
589
|
-
// Grid
|
|
545
|
+
const legendPosition = options.legend?.position ?? 'right';
|
|
590
546
|
const showGrid = options.grid?.show ?? false;
|
|
591
547
|
const gridColor = options.grid?.color ?? '#E0E0E0';
|
|
592
548
|
const gridWidth = options.grid?.width ?? 1;
|
|
593
|
-
// Chart type
|
|
594
549
|
const chartType = options.type ?? 'standard';
|
|
595
|
-
// Bars
|
|
596
550
|
const minBarHeight = options.bars?.minHeight ?? 30;
|
|
597
551
|
const barSpacing = options.bars?.spacing;
|
|
598
552
|
const groupSpacing = options.bars?.groupSpacing ?? 10;
|
|
599
553
|
const segmentSpacing = options.bars?.segmentSpacing ?? 2;
|
|
600
|
-
const lollipopLineWidth = options.bars?.lineWidth ?? 2;
|
|
601
|
-
const lollipopDotSize = options.bars?.dotSize ?? 8;
|
|
554
|
+
const lollipopLineWidth = options.bars?.lineWidth ?? 2;
|
|
555
|
+
const lollipopDotSize = options.bars?.dotSize ?? 8;
|
|
602
556
|
const paddingTop = padding.top ?? 60;
|
|
603
557
|
const paddingRight = padding.right ?? 80;
|
|
604
558
|
const paddingBottom = padding.bottom ?? 80;
|
|
605
559
|
const paddingLeft = padding.left ?? 100;
|
|
606
|
-
// Calculate responsive height based on number of bars
|
|
607
560
|
let baseHeight = calculateResponsiveHeight(data.length, options);
|
|
608
|
-
// Calculate legend dimensions and adjust canvas size based on legend position
|
|
609
561
|
let legendWidth = 0;
|
|
610
562
|
let legendHeight = 0;
|
|
611
563
|
let extraWidth = 0;
|
|
612
564
|
let extraHeight = 0;
|
|
613
|
-
const minLegendSpacing = 10;
|
|
565
|
+
const minLegendSpacing = 10;
|
|
614
566
|
if (showLegend && legend && legend.length > 0) {
|
|
615
567
|
const legendMaxWidth = options.legend?.maxWidth;
|
|
616
568
|
const legendWrapText = options.legend?.wrapText !== false;
|
|
@@ -619,36 +571,27 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
619
571
|
legendWidth = legendDims.width;
|
|
620
572
|
legendHeight = legendDims.height;
|
|
621
573
|
const legendSpacing = options.legend?.spacing ?? 20;
|
|
622
|
-
// Adjust canvas dimensions based on legend position
|
|
623
|
-
// For left position, add extra space for Y-axis labels and bar labels
|
|
624
574
|
if (legendPosition === 'left') {
|
|
625
|
-
// Estimate Y-axis label width: measure potential category labels or numeric values
|
|
626
575
|
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
627
576
|
const tempCtx = tempCanvas.getContext('2d');
|
|
628
|
-
let estimatedYAxisLabelWidth = 80;
|
|
577
|
+
let estimatedYAxisLabelWidth = 80;
|
|
629
578
|
if (tempCtx) {
|
|
630
|
-
// Check if bar labels are on the left (they act as Y-axis labels)
|
|
631
579
|
const barLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
|
|
632
580
|
const showBarLabels = options.labels?.barLabelDefaults?.show ?? true;
|
|
633
581
|
const barLabelPosition = options.labels?.barLabelDefaults?.defaultPosition ?? 'left';
|
|
634
582
|
const hasLeftLabels = showBarLabels && (barLabelPosition === 'left' ||
|
|
635
583
|
data.some(item => (item.labelPosition ?? barLabelPosition) === 'left'));
|
|
636
584
|
if (hasLeftLabels) {
|
|
637
|
-
// Measure category labels (bar labels) which are typically longer
|
|
638
585
|
tempCtx.font = `${barLabelFontSize}px Arial`;
|
|
639
586
|
data.forEach(d => {
|
|
640
587
|
const labelWidth = tempCtx.measureText(d.label).width;
|
|
641
588
|
estimatedYAxisLabelWidth = Math.max(estimatedYAxisLabelWidth, labelWidth);
|
|
642
589
|
});
|
|
643
|
-
// Add padding: 5px (label offset from originX) + 10px (spacing) = 15px total
|
|
644
590
|
estimatedYAxisLabelWidth += 15;
|
|
645
591
|
}
|
|
646
592
|
else {
|
|
647
|
-
// No left labels, but might have Y-axis numeric ticks
|
|
648
593
|
tempCtx.font = `${tickFontSize}px Arial`;
|
|
649
|
-
|
|
650
|
-
estimatedYAxisLabelWidth = 60; // Default for numeric values
|
|
651
|
-
// Add padding: 10px (label offset) + 5px (tick) + 15px (spacing) = 30px total
|
|
594
|
+
estimatedYAxisLabelWidth = 60;
|
|
652
595
|
estimatedYAxisLabelWidth += 30;
|
|
653
596
|
}
|
|
654
597
|
}
|
|
@@ -661,15 +604,9 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
661
604
|
extraHeight = legendHeight + legendSpacing + minLegendSpacing;
|
|
662
605
|
}
|
|
663
606
|
}
|
|
664
|
-
// adjustedWidth and adjustedHeight are already calculated above
|
|
665
|
-
// Determine X-axis (value axis) range
|
|
666
|
-
// For grouped charts: find max value across all segments
|
|
667
|
-
// For stacked charts: find max sum of values per category
|
|
668
|
-
// For lollipop charts: same as standard (single value per bar)
|
|
669
607
|
let allValues = [];
|
|
670
608
|
if (chartType === 'grouped' || chartType === 'stacked' || chartType === 'lollipop') {
|
|
671
609
|
if (chartType === 'grouped') {
|
|
672
|
-
// For grouped: find max value across all segments
|
|
673
610
|
data.forEach(d => {
|
|
674
611
|
if (d.values && d.values.length > 0) {
|
|
675
612
|
d.values.forEach(seg => allValues.push(seg.value));
|
|
@@ -680,7 +617,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
680
617
|
});
|
|
681
618
|
}
|
|
682
619
|
else {
|
|
683
|
-
// For stacked: find max sum per category
|
|
684
620
|
data.forEach(d => {
|
|
685
621
|
if (d.values && d.values.length > 0) {
|
|
686
622
|
const sum = d.values.reduce((acc, seg) => acc + seg.value, 0);
|
|
@@ -693,20 +629,17 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
693
629
|
}
|
|
694
630
|
}
|
|
695
631
|
else {
|
|
696
|
-
// Standard chart: use value directly
|
|
697
632
|
allValues = data.map(d => d.value ?? 0).filter(v => v !== undefined && v !== null);
|
|
698
633
|
}
|
|
699
634
|
let xMin, xMax;
|
|
700
635
|
let xAxisCustomValues = xAxisValues;
|
|
701
636
|
const hasExplicitXRange = xAxisRange && xAxisRange.min !== undefined && xAxisRange.max !== undefined;
|
|
702
|
-
// Check if any bars have xStart/xEnd (value ranges)
|
|
703
637
|
const hasValueRanges = data.some(d => d.xStart !== undefined || d.xEnd !== undefined);
|
|
704
638
|
if (hasValueRanges) {
|
|
705
639
|
const allXStarts = data.map(d => d.xStart ?? d.value ?? 0).filter(v => v !== undefined);
|
|
706
640
|
const allXEnds = data.map(d => d.xEnd ?? d.value ?? 0).filter(v => v !== undefined);
|
|
707
641
|
xMin = Math.min(...allXStarts, ...allXEnds);
|
|
708
642
|
xMax = Math.max(...allXStarts, ...allXEnds);
|
|
709
|
-
// Add some padding
|
|
710
643
|
const xPadding = (xMax - xMin) * 0.1;
|
|
711
644
|
xMin = Math.max(0, xMin - xPadding);
|
|
712
645
|
xMax = xMax + xPadding;
|
|
@@ -718,7 +651,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
718
651
|
else if (hasExplicitXRange) {
|
|
719
652
|
xMin = xAxisRange.min;
|
|
720
653
|
xMax = xAxisRange.max;
|
|
721
|
-
// Ensure baseline is within range
|
|
722
654
|
const effectiveBaseline = baseline !== undefined ? baseline : 0;
|
|
723
655
|
xMin = Math.min(xMin, effectiveBaseline);
|
|
724
656
|
xMax = Math.max(xMax, effectiveBaseline);
|
|
@@ -728,11 +660,9 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
728
660
|
xMax = Math.max(...allValues, 1);
|
|
729
661
|
const xPadding = (xMax - xMin) * 0.1;
|
|
730
662
|
const effectiveBaseline = baseline !== undefined ? baseline : 0;
|
|
731
|
-
// Ensure baseline is always included in the range
|
|
732
663
|
xMin = Math.min(Math.max(0, xMin - xPadding), effectiveBaseline);
|
|
733
664
|
xMax = xMax + xPadding;
|
|
734
665
|
}
|
|
735
|
-
// Determine Y-axis (category axis) range - similar to X-axis in standard chart
|
|
736
666
|
const yAxisRange = options.axes?.y?.range;
|
|
737
667
|
let yMin, yMax;
|
|
738
668
|
let yAxisCustomValues = yAxisValues;
|
|
@@ -746,22 +676,18 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
746
676
|
yMax = yAxisRange.max;
|
|
747
677
|
}
|
|
748
678
|
else {
|
|
749
|
-
// Auto-calculate from data indices (0 to data.length - 1)
|
|
750
679
|
yMin = 0;
|
|
751
680
|
yMax = data.length - 1;
|
|
752
681
|
}
|
|
753
|
-
// Validate data values against explicit axis ranges
|
|
754
682
|
if (hasExplicitXRange || xAxisCustomValues) {
|
|
755
683
|
const effectiveXMin = xAxisCustomValues ? Math.min(...xAxisCustomValues) : xAxisRange.min;
|
|
756
684
|
const effectiveXMax = xAxisCustomValues ? Math.max(...xAxisCustomValues) : xAxisRange.max;
|
|
757
685
|
data.forEach((item, itemIndex) => {
|
|
758
|
-
// Check value (X-axis for horizontal bars)
|
|
759
686
|
if (item.value !== undefined && (item.value < effectiveXMin || item.value > effectiveXMax)) {
|
|
760
687
|
throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
|
|
761
688
|
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has value ${item.value}, ` +
|
|
762
689
|
`which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
|
|
763
690
|
}
|
|
764
|
-
// Check xStart and xEnd if they exist
|
|
765
691
|
if (item.xStart !== undefined && (item.xStart < effectiveXMin || item.xStart > effectiveXMax)) {
|
|
766
692
|
throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
|
|
767
693
|
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has xStart value ${item.xStart}, ` +
|
|
@@ -772,7 +698,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
772
698
|
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has xEnd value ${item.xEnd}, ` +
|
|
773
699
|
`which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
|
|
774
700
|
}
|
|
775
|
-
// Check grouped/stacked values
|
|
776
701
|
if (item.values && item.values.length > 0) {
|
|
777
702
|
item.values.forEach((seg, segIndex) => {
|
|
778
703
|
if (seg.value < effectiveXMin || seg.value > effectiveXMax) {
|
|
@@ -788,7 +713,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
788
713
|
const effectiveYMin = yAxisCustomValues ? Math.min(...yAxisCustomValues) : yAxisRange.min;
|
|
789
714
|
const effectiveYMax = yAxisCustomValues ? Math.max(...yAxisCustomValues) : yAxisRange.max;
|
|
790
715
|
data.forEach((item, itemIndex) => {
|
|
791
|
-
// Check yStart and yEnd (Y-axis for horizontal bars)
|
|
792
716
|
if (item.yStart !== undefined && (item.yStart < effectiveYMin || item.yStart > effectiveYMax)) {
|
|
793
717
|
throw new Error(`Horizontal Bar Chart Error: Data value out of Y-axis bounds.\n` +
|
|
794
718
|
`Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has yStart value ${item.yStart}, ` +
|
|
@@ -801,14 +725,10 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
801
725
|
}
|
|
802
726
|
});
|
|
803
727
|
}
|
|
804
|
-
// Legend dimensions already calculated above, no need to recalculate
|
|
805
|
-
// Calculate adjusted dimensions (needed before creating canvas)
|
|
806
728
|
const adjustedWidth = width + extraWidth;
|
|
807
729
|
const adjustedHeight = baseHeight + extraHeight;
|
|
808
|
-
// Create canvas
|
|
809
730
|
const canvas = (0, canvas_1.createCanvas)(adjustedWidth, adjustedHeight);
|
|
810
731
|
const ctx = canvas.getContext('2d');
|
|
811
|
-
// Fill background (gradient, image, or color)
|
|
812
732
|
if (backgroundImage) {
|
|
813
733
|
try {
|
|
814
734
|
const bgImage = await (0, canvas_1.loadImage)(backgroundImage);
|
|
@@ -816,7 +736,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
816
736
|
}
|
|
817
737
|
catch (error) {
|
|
818
738
|
console.warn(`Failed to load background image: ${backgroundImage}`, error);
|
|
819
|
-
// Fallback to gradient or color if image fails to load
|
|
820
739
|
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
|
|
821
740
|
x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
|
|
822
741
|
});
|
|
@@ -829,165 +748,114 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
829
748
|
});
|
|
830
749
|
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
831
750
|
}
|
|
832
|
-
// Calculate axis positions with proper spacing
|
|
833
|
-
// Title height: fontSize + spacing below title (minimum 20px gap)
|
|
834
751
|
const titleHeight = chartTitle ? chartTitleFontSize + 20 : 0;
|
|
835
|
-
// Calculate actual height needed for x-axis labels (tick labels + axis label)
|
|
836
752
|
let xAxisLabelAreaHeight = 0;
|
|
837
753
|
if (xAxisLabel || (xAxisCustomValues && xAxisCustomValues.length > 0) || xAxisRange) {
|
|
838
|
-
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
const xAxisLabelTextHeight = xAxisLabel ? axisLabelFontSize + 2 : 0; // 2px minimum gap
|
|
842
|
-
// Total height needed: tick labels + gap + axis label
|
|
843
|
-
xAxisLabelAreaHeight = tickLabelHeight + (xAxisLabel ? 8 : 0) + xAxisLabelTextHeight; // 8px gap between tick labels and axis label
|
|
754
|
+
const tickLabelHeight = tickFontSize + 10;
|
|
755
|
+
const xAxisLabelTextHeight = xAxisLabel ? axisLabelFontSize + 2 : 0;
|
|
756
|
+
xAxisLabelAreaHeight = tickLabelHeight + (xAxisLabel ? 8 : 0) + xAxisLabelTextHeight;
|
|
844
757
|
}
|
|
845
|
-
// Y-axis label height (if present)
|
|
846
758
|
const yAxisLabelHeight = yAxisLabel ? axisLabelFontSize + 20 : 0;
|
|
847
759
|
const axisLabelHeight = Math.max(xAxisLabelAreaHeight, yAxisLabelHeight);
|
|
848
|
-
// Adjust chart area based on legend position
|
|
849
|
-
// Note: adjustedWidth and adjustedHeight are already calculated above (before canvas creation)
|
|
850
|
-
// Chart area should use the ORIGINAL width/height, not adjusted, so legend space is added to canvas, not taken from chart
|
|
851
760
|
let chartAreaLeft = paddingLeft;
|
|
852
|
-
let chartAreaRight = width - paddingRight;
|
|
853
|
-
|
|
854
|
-
const titleMargin = chartTitle ? 20 : 0; // 20px gap between title and chart
|
|
761
|
+
let chartAreaRight = width - paddingRight;
|
|
762
|
+
const titleMargin = chartTitle ? 20 : 0;
|
|
855
763
|
let chartAreaTop = paddingTop + titleHeight + titleMargin;
|
|
856
|
-
let chartAreaBottom = baseHeight - paddingBottom;
|
|
764
|
+
let chartAreaBottom = baseHeight - paddingBottom;
|
|
857
765
|
if (showLegend && legend && legend.length > 0) {
|
|
858
766
|
const legendSpacing = options.legend?.spacing ?? 20;
|
|
859
767
|
if (legendPosition === 'left') {
|
|
860
|
-
|
|
861
|
-
let actualYAxisLabelWidth = 80; // Default estimate
|
|
768
|
+
let actualYAxisLabelWidth = 80;
|
|
862
769
|
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
863
770
|
const tempCtx = tempCanvas.getContext('2d');
|
|
864
771
|
if (tempCtx) {
|
|
865
|
-
// Check if bar labels are positioned on the left (they act as Y-axis labels)
|
|
866
772
|
const barLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
|
|
867
773
|
tempCtx.font = `${barLabelFontSize}px Arial`;
|
|
868
|
-
// Check if bar labels are on the left side
|
|
869
774
|
const hasLeftLabels = barLabelPosition === 'left' ||
|
|
870
775
|
data.some(item => (item.labelPosition ?? barLabelPosition) === 'left');
|
|
871
776
|
if (hasLeftLabels && showBarLabels) {
|
|
872
|
-
// Measure category labels (bar labels) - these are the Y-axis labels
|
|
873
777
|
data.forEach(d => {
|
|
874
778
|
const labelWidth = tempCtx.measureText(d.label).width;
|
|
875
779
|
actualYAxisLabelWidth = Math.max(actualYAxisLabelWidth, labelWidth);
|
|
876
780
|
});
|
|
877
|
-
// Add padding: 5px (label offset from originX) + 10px (spacing) = 15px total
|
|
878
781
|
actualYAxisLabelWidth += 15;
|
|
879
782
|
}
|
|
880
783
|
else {
|
|
881
|
-
// No left labels, but might have Y-axis numeric ticks
|
|
882
784
|
tempCtx.font = `${tickFontSize}px Arial`;
|
|
883
|
-
|
|
884
|
-
actualYAxisLabelWidth = 60; // Default for numeric values
|
|
885
|
-
// Add padding: 10px (label offset) + 5px (tick) + 15px (spacing) = 30px total
|
|
785
|
+
actualYAxisLabelWidth = 60;
|
|
886
786
|
actualYAxisLabelWidth += 30;
|
|
887
787
|
}
|
|
888
788
|
}
|
|
889
|
-
// For left position, we need to shift chart area to the right to make room for legend
|
|
890
|
-
// But the chart area width itself should remain the same (using original width)
|
|
891
789
|
chartAreaLeft = paddingLeft + legendWidth + legendSpacing + actualYAxisLabelWidth;
|
|
892
|
-
chartAreaRight = width - paddingRight;
|
|
790
|
+
chartAreaRight = width - paddingRight;
|
|
893
791
|
}
|
|
894
792
|
else if (legendPosition === 'right') {
|
|
895
|
-
// For right position, chart area stays at original position and size
|
|
896
|
-
// Legend will be positioned to the right of chartAreaRight, in the extraWidth space
|
|
897
793
|
chartAreaLeft = paddingLeft;
|
|
898
|
-
chartAreaRight = width - paddingRight;
|
|
794
|
+
chartAreaRight = width - paddingRight;
|
|
899
795
|
}
|
|
900
796
|
else if (legendPosition === 'top') {
|
|
901
|
-
// For top position, shift chart area down but keep same height
|
|
902
797
|
chartAreaTop = paddingTop + titleHeight + legendHeight + legendSpacing + minLegendSpacing;
|
|
903
|
-
chartAreaBottom = baseHeight - paddingBottom;
|
|
798
|
+
chartAreaBottom = baseHeight - paddingBottom;
|
|
904
799
|
}
|
|
905
800
|
else if (legendPosition === 'bottom') {
|
|
906
|
-
// For bottom position, chart area stays at original position
|
|
907
|
-
// Legend will be positioned below in the extraHeight space
|
|
908
801
|
chartAreaTop = paddingTop + titleHeight;
|
|
909
|
-
chartAreaBottom = baseHeight - paddingBottom;
|
|
802
|
+
chartAreaBottom = baseHeight - paddingBottom;
|
|
910
803
|
}
|
|
911
804
|
}
|
|
912
805
|
const originX = chartAreaLeft;
|
|
913
|
-
// Use baseHeight (not adjustedHeight) for originY calculation - chart area should be based on original dimensions
|
|
914
|
-
// The adjustedHeight is only for canvas size expansion, not chart area positioning
|
|
915
|
-
// This ensures the chart size stays constant and legend space is added to canvas, not taken from chart
|
|
916
|
-
// Ensure at least 2px gap at bottom for labels
|
|
917
806
|
const minBottomGap = 2;
|
|
918
807
|
const originY = baseHeight - paddingBottom - xAxisLabelAreaHeight - minBottomGap;
|
|
919
808
|
const axisEndY = chartAreaTop;
|
|
920
809
|
const axisEndX = chartAreaRight;
|
|
921
|
-
// Draw chart title
|
|
922
810
|
if (chartTitle) {
|
|
923
811
|
ctx.save();
|
|
924
812
|
ctx.textAlign = 'center';
|
|
925
813
|
ctx.textBaseline = 'top';
|
|
926
|
-
// Title positioned with proper spacing from top
|
|
927
814
|
const titleY = paddingTop + 10;
|
|
928
815
|
const titleX = adjustedWidth / 2;
|
|
929
816
|
await renderEnhancedText(ctx, chartTitle, titleX, titleY, options.labels?.title?.textStyle, chartTitleFontSize, options.labels?.title?.color, options.labels?.title?.gradient);
|
|
930
817
|
ctx.restore();
|
|
931
818
|
}
|
|
932
|
-
// Set axis style
|
|
933
819
|
ctx.strokeStyle = axisColor;
|
|
934
820
|
ctx.fillStyle = axisColor;
|
|
935
821
|
ctx.lineWidth = axisWidth;
|
|
936
822
|
ctx.lineCap = 'round';
|
|
937
|
-
// X-axis will be drawn after calculating zero line
|
|
938
|
-
// Draw Y-axis (vertical - category axis)
|
|
939
823
|
ctx.beginPath();
|
|
940
824
|
ctx.moveTo(originX, originY);
|
|
941
825
|
ctx.lineTo(originX, axisEndY);
|
|
942
826
|
ctx.stroke();
|
|
943
|
-
|
|
944
|
-
drawArrow(ctx, originX, axisEndY, -Math.PI / 2, arrowSize); // Y-axis arrow (up)
|
|
945
|
-
// Calculate X-axis step
|
|
827
|
+
drawArrow(ctx, originX, axisEndY, -Math.PI / 2, arrowSize);
|
|
946
828
|
const xStep = xAxisRange?.step ?? Math.ceil((xMax - xMin) / 10);
|
|
947
|
-
// Calculate Y-axis step
|
|
948
829
|
const yStep = yAxisRange?.step ?? 1;
|
|
949
|
-
// Calculate chart area dimensions (needed for baseline calculation)
|
|
950
830
|
const chartAreaWidth = axisEndX - originX;
|
|
951
|
-
// Calculate baseline position for X-axis (custom baseline value, default is 0)
|
|
952
831
|
const baselineX = originX + ((baseline - xMin) / (xMax - xMin)) * chartAreaWidth;
|
|
953
|
-
// Draw X-axis at baseline position (horizontal line at originY)
|
|
954
832
|
ctx.beginPath();
|
|
955
833
|
ctx.moveTo(originX, originY);
|
|
956
834
|
ctx.lineTo(axisEndX, originY);
|
|
957
835
|
ctx.stroke();
|
|
958
|
-
// Draw X-axis arrow
|
|
959
836
|
drawArrow(ctx, axisEndX, originY, 0, arrowSize);
|
|
960
|
-
// Draw X-axis ticks and labels at baseline position
|
|
961
837
|
drawXAxisTicks(ctx, originX, originY, axisEndX, xMin, xMax, xStep, tickFontSize, xAxisCustomValues, xAxisValueSpacing, xAxisTickColor);
|
|
962
|
-
// Draw Y-axis ticks and labels only if yAxisValues or yAxisRange is explicitly provided
|
|
963
|
-
// For horizontal bar charts, we typically only want bar labels, not numeric y-axis values
|
|
964
838
|
const shouldDrawYAxisTicks = (yAxisValues && yAxisValues.length > 0) || (options.axes?.y?.range && options.axes.y.range.min !== undefined && options.axes.y.range.max !== undefined);
|
|
965
839
|
if (shouldDrawYAxisTicks) {
|
|
966
840
|
drawYAxisTicks(ctx, originX, originY, axisEndY, yMin, yMax, yStep, tickFontSize, yAxisCustomValues, yAxisValueSpacing, yAxisTickColor);
|
|
967
841
|
}
|
|
968
|
-
// Draw axis labels
|
|
969
842
|
if (xAxisLabel) {
|
|
970
843
|
ctx.save();
|
|
971
844
|
ctx.fillStyle = axisLabelColor;
|
|
972
845
|
ctx.font = `${axisLabelFontSize}px Arial`;
|
|
973
846
|
ctx.textAlign = 'center';
|
|
974
847
|
ctx.textBaseline = 'top';
|
|
975
|
-
// Position x-axis label with proper spacing: tick labels end at originY + tickFontSize + 10
|
|
976
|
-
// Add 8px gap between tick labels and axis label, then position axis label
|
|
977
848
|
const tickLabelBottom = originY + tickFontSize + 10;
|
|
978
|
-
const xAxisLabelY = tickLabelBottom + 8;
|
|
849
|
+
const xAxisLabelY = tickLabelBottom + 8;
|
|
979
850
|
ctx.fillText(xAxisLabel, (originX + axisEndX) / 2, xAxisLabelY);
|
|
980
851
|
ctx.restore();
|
|
981
852
|
}
|
|
982
853
|
if (yAxisLabel) {
|
|
983
|
-
// Check if bar labels are on the left side - if so, position Y-axis label further left
|
|
984
854
|
let maxBarLabelWidth = 0;
|
|
985
855
|
if (showBarLabels) {
|
|
986
|
-
// Check if default position or any bar has labels on the left
|
|
987
856
|
const hasLeftLabels = barLabelPosition === 'left' ||
|
|
988
857
|
data.some(item => (item.labelPosition ?? barLabelPosition) === 'left');
|
|
989
858
|
if (hasLeftLabels) {
|
|
990
|
-
// Calculate maximum width of bar labels
|
|
991
859
|
ctx.save();
|
|
992
860
|
ctx.font = `${axisLabelFontSize}px Arial`;
|
|
993
861
|
data.forEach(item => {
|
|
@@ -1005,8 +873,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1005
873
|
ctx.font = `${axisLabelFontSize}px Arial`;
|
|
1006
874
|
ctx.textAlign = 'center';
|
|
1007
875
|
ctx.textBaseline = 'bottom';
|
|
1008
|
-
// Position Y-axis label further left if bar labels are on the left
|
|
1009
|
-
// Add extra spacing (20px) after the bar labels
|
|
1010
876
|
const labelX = originX - maxBarLabelWidth - 20 - 30;
|
|
1011
877
|
const labelY = (originY + axisEndY) / 2;
|
|
1012
878
|
ctx.translate(labelX, labelY);
|
|
@@ -1014,11 +880,9 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1014
880
|
ctx.fillText(yAxisLabel, 0, 0);
|
|
1015
881
|
ctx.restore();
|
|
1016
882
|
}
|
|
1017
|
-
// Draw grid lines if enabled
|
|
1018
883
|
if (showGrid) {
|
|
1019
884
|
drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep, yMin, yMax, yStep, yAxisCustomValues, xAxisCustomValues, gridColor, gridWidth);
|
|
1020
885
|
}
|
|
1021
|
-
// Draw legend if provided - positioned based on legendPosition option
|
|
1022
886
|
if (showLegend && legend && legend.length > 0) {
|
|
1023
887
|
const legendSpacing = options.legend?.spacing ?? 20;
|
|
1024
888
|
const legendFontSize = options.legend?.fontSize ?? 16;
|
|
@@ -1028,78 +892,57 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1028
892
|
const legendPadding = options.legend?.padding;
|
|
1029
893
|
const legendMaxWidth = options.legend?.maxWidth;
|
|
1030
894
|
const legendWrapText = options.legend?.wrapText !== false;
|
|
1031
|
-
// Calculate legend position based on legendPosition option
|
|
1032
895
|
let legendX, legendY;
|
|
1033
896
|
const chartAreaHeight = originY - axisEndY;
|
|
1034
897
|
const chartAreaWidth = axisEndX - originX;
|
|
1035
898
|
switch (legendPosition) {
|
|
1036
899
|
case 'top':
|
|
1037
|
-
legendX = (adjustedWidth - legendWidth) / 2;
|
|
900
|
+
legendX = (adjustedWidth - legendWidth) / 2;
|
|
1038
901
|
legendY = paddingTop + titleHeight + minLegendSpacing;
|
|
1039
902
|
break;
|
|
1040
903
|
case 'bottom':
|
|
1041
|
-
legendX = (adjustedWidth - legendWidth) / 2;
|
|
904
|
+
legendX = (adjustedWidth - legendWidth) / 2;
|
|
1042
905
|
legendY = adjustedHeight - paddingBottom - legendHeight - minLegendSpacing;
|
|
1043
906
|
break;
|
|
1044
907
|
case 'left':
|
|
1045
|
-
// Position legend at the very left edge to make maximum room for Y-axis labels
|
|
1046
|
-
// The chart area already accounts for legend width + label width, so position legend at leftmost
|
|
1047
908
|
legendX = paddingLeft;
|
|
1048
|
-
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2;
|
|
909
|
+
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2;
|
|
1049
910
|
break;
|
|
1050
911
|
case 'right':
|
|
1051
912
|
default:
|
|
1052
913
|
legendX = axisEndX + minLegendSpacing;
|
|
1053
|
-
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2;
|
|
914
|
+
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2;
|
|
1054
915
|
break;
|
|
1055
916
|
}
|
|
1056
917
|
await drawLegendAtPosition(ctx, legend, legendX, legendY, legendFontSize, legendBgColor || backgroundColor, legendTextColor, legendBorderColor, legendPadding, legendMaxWidth, legendWrapText, options.legend?.backgroundGradient, options.legend?.textGradient, options.legend?.textStyle);
|
|
1057
918
|
}
|
|
1058
|
-
// Calculate chart area dimensions (Y-axis area for bars)
|
|
1059
|
-
// chartAreaWidth and baselineX already calculated above when drawing X-axis
|
|
1060
919
|
const chartAreaHeight = originY - axisEndY;
|
|
1061
|
-
// Calculate bar dimensions to fit within Y-axis bounds (between axisEndY and originY)
|
|
1062
920
|
const calculatedBarSpacing = barSpacing ?? 15;
|
|
1063
|
-
|
|
1064
|
-
const totalSpacing = data.length * calculatedBarSpacing; // Include spacing before first bar
|
|
921
|
+
const totalSpacing = data.length * calculatedBarSpacing;
|
|
1065
922
|
const availableHeight = chartAreaHeight - totalSpacing;
|
|
1066
923
|
const calculatedBarHeight = Math.max(minBarHeight, availableHeight / data.length);
|
|
1067
|
-
// Verify all bars will fit
|
|
1068
924
|
const totalBarsHeight = data.length * calculatedBarHeight + totalSpacing;
|
|
1069
925
|
if (totalBarsHeight > chartAreaHeight) {
|
|
1070
|
-
// Adjust bar height if needed to fit all bars
|
|
1071
926
|
const adjustedBarHeight = Math.max(minBarHeight, (chartAreaHeight - totalSpacing) / data.length);
|
|
1072
927
|
if (adjustedBarHeight < calculatedBarHeight) {
|
|
1073
928
|
console.warn(`Bar height adjusted from ${calculatedBarHeight} to ${adjustedBarHeight} to fit all ${data.length} bars`);
|
|
1074
929
|
}
|
|
1075
930
|
}
|
|
1076
931
|
const labelsToDraw = [];
|
|
1077
|
-
// Track value label positions per bar (for adjusting bar label positions)
|
|
1078
932
|
const valueLabelPositions = new Map();
|
|
1079
|
-
// First pass: Draw all bars (no labels)
|
|
1080
933
|
data.forEach((item, index) => {
|
|
1081
|
-
// Calculate bar Y position - start from axisEndY (top) and space bars downward
|
|
1082
|
-
// First bar starts at axisEndY, each subsequent bar: previous position + bar height + spacing
|
|
1083
934
|
const barY = axisEndY + (index * (calculatedBarHeight + calculatedBarSpacing));
|
|
1084
935
|
const barCenterY = barY + calculatedBarHeight / 2;
|
|
1085
|
-
// Ensure bar stays within Y-axis bounds (between axisEndY and originY)
|
|
1086
936
|
if (barY + calculatedBarHeight > originY) {
|
|
1087
|
-
// Bar would exceed Y-axis bottom - skip it to prevent overflow
|
|
1088
|
-
// This should not happen if chartAreaHeight is calculated correctly, but check anyway
|
|
1089
937
|
console.warn(`Bar at index ${index} (${item.label}) would exceed chart bounds. Skipping.`);
|
|
1090
938
|
return;
|
|
1091
939
|
}
|
|
1092
|
-
// Calculate bar position and dimensions for label positioning (used for all chart types)
|
|
1093
940
|
let barX, barEndX, barLength;
|
|
1094
|
-
// Handle grouped/stacked/lollipop vs standard charts
|
|
1095
941
|
if ((chartType === 'grouped' || chartType === 'stacked' || chartType === 'lollipop') && item.values && item.values.length > 0) {
|
|
1096
|
-
// Grouped or stacked chart
|
|
1097
942
|
const segments = item.values;
|
|
1098
943
|
const numSegments = segments.length;
|
|
1099
944
|
if (chartType === 'grouped') {
|
|
1100
|
-
// Grouped: bars side-by-side (vertically stacked in horizontal chart)
|
|
1101
945
|
const segmentHeight = (calculatedBarHeight - (groupSpacing * (numSegments - 1))) / numSegments;
|
|
1102
|
-
// Calculate overall bar bounds for label positioning (use max segment)
|
|
1103
946
|
const maxSegment = segments.reduce((max, seg) => seg.value > max.value ? seg : max, segments[0]);
|
|
1104
947
|
if (item.xStart !== undefined || item.xEnd !== undefined) {
|
|
1105
948
|
const startValue = item.xStart ?? xMin;
|
|
@@ -1110,7 +953,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1110
953
|
barEndX = originX + endRatio * chartAreaWidth;
|
|
1111
954
|
}
|
|
1112
955
|
else {
|
|
1113
|
-
// Calculate based on positive/negative
|
|
1114
956
|
if (maxSegment.value >= 0) {
|
|
1115
957
|
const positiveRatio = (maxSegment.value - 0) / (xMax - xMin);
|
|
1116
958
|
barX = baselineX;
|
|
@@ -1126,7 +968,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1126
968
|
segments.forEach((segment, segIndex) => {
|
|
1127
969
|
const segY = barY + (segIndex * (segmentHeight + groupSpacing));
|
|
1128
970
|
const segCenterY = segY + segmentHeight / 2;
|
|
1129
|
-
// Calculate segment bar position and length
|
|
1130
971
|
let segBarX, segBarEndX;
|
|
1131
972
|
if (item.xStart !== undefined || item.xEnd !== undefined) {
|
|
1132
973
|
const startValue = item.xStart ?? xMin;
|
|
@@ -1137,7 +978,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1137
978
|
segBarEndX = originX + endRatio * chartAreaWidth;
|
|
1138
979
|
}
|
|
1139
980
|
else {
|
|
1140
|
-
// Calculate bar position based on positive/negative value
|
|
1141
981
|
if (segment.value >= baseline) {
|
|
1142
982
|
const positiveRatio = (segment.value - baseline) / (xMax - xMin);
|
|
1143
983
|
segBarX = baselineX;
|
|
@@ -1150,12 +990,10 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1150
990
|
}
|
|
1151
991
|
}
|
|
1152
992
|
const segBarLength = Math.abs(segBarEndX - segBarX);
|
|
1153
|
-
// Draw segment bar with gradient or color
|
|
1154
993
|
ctx.beginPath();
|
|
1155
994
|
ctx.rect(segBarX, segY, segBarLength, segmentHeight);
|
|
1156
995
|
fillWithGradientOrColor(ctx, segment.gradient || item.gradient, segment.color || item.color || '#4A90E2', '#4A90E2', { x: segBarX, y: segY, w: segBarLength, h: segmentHeight });
|
|
1157
996
|
ctx.fill();
|
|
1158
|
-
// Store value label for later drawing
|
|
1159
997
|
const shouldShowValue = segment.showValue !== undefined ? segment.showValue : showValues;
|
|
1160
998
|
if (shouldShowValue) {
|
|
1161
999
|
labelsToDraw.push({
|
|
@@ -1172,10 +1010,8 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1172
1010
|
});
|
|
1173
1011
|
}
|
|
1174
1012
|
else {
|
|
1175
|
-
// Stacked: bars on top of each other (horizontally stacked in horizontal chart)
|
|
1176
1013
|
let accumulatedLength = 0;
|
|
1177
1014
|
segments.forEach((segment, segIndex) => {
|
|
1178
|
-
// For stacked, separate positive and negative segments
|
|
1179
1015
|
let segmentLength;
|
|
1180
1016
|
let segBarX;
|
|
1181
1017
|
if (segment.value >= baseline) {
|
|
@@ -1188,10 +1024,8 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1188
1024
|
segmentLength = negativeRatio * chartAreaWidth;
|
|
1189
1025
|
segBarX = baselineX - accumulatedLength - segmentLength;
|
|
1190
1026
|
}
|
|
1191
|
-
// Draw segment bar
|
|
1192
1027
|
ctx.fillStyle = segment.color || item.color || '#4A90E2';
|
|
1193
1028
|
ctx.fillRect(segBarX, barY, segmentLength, calculatedBarHeight);
|
|
1194
|
-
// Store value label for later drawing
|
|
1195
1029
|
const shouldShowValue = segment.showValue !== undefined ? segment.showValue : showValues;
|
|
1196
1030
|
if (shouldShowValue && segmentLength > valueFontSize + 10) {
|
|
1197
1031
|
labelsToDraw.push({
|
|
@@ -1207,15 +1041,12 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1207
1041
|
}
|
|
1208
1042
|
accumulatedLength += segmentLength;
|
|
1209
1043
|
});
|
|
1210
|
-
// Calculate overall bar bounds for label positioning
|
|
1211
1044
|
barX = originX;
|
|
1212
1045
|
barEndX = originX + accumulatedLength;
|
|
1213
1046
|
barLength = accumulatedLength;
|
|
1214
|
-
// Store total value label for later drawing
|
|
1215
1047
|
const totalValue = segments.reduce((sum, seg) => sum + seg.value, 0);
|
|
1216
1048
|
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1217
1049
|
if (shouldShowValue) {
|
|
1218
|
-
// Calculate total position
|
|
1219
1050
|
const totalPositive = segments.filter(s => s.value >= 0).reduce((sum, s) => sum + s.value, 0);
|
|
1220
1051
|
const totalNegative = segments.filter(s => s.value < 0).reduce((sum, s) => sum + Math.abs(s.value), 0);
|
|
1221
1052
|
const totalPositiveLength = (totalPositive / (xMax - xMin)) * chartAreaWidth;
|
|
@@ -1237,21 +1068,16 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1237
1068
|
}
|
|
1238
1069
|
}
|
|
1239
1070
|
else if (chartType === 'lollipop') {
|
|
1240
|
-
// Lollipop chart: line with dot at end (horizontal)
|
|
1241
1071
|
const value = item.value ?? baseline;
|
|
1242
|
-
// Calculate value X position
|
|
1243
1072
|
let valueX;
|
|
1244
1073
|
if (value >= baseline) {
|
|
1245
|
-
// Value to the right of baseline
|
|
1246
1074
|
const positiveRatio = (value - baseline) / (xMax - xMin);
|
|
1247
1075
|
valueX = baselineX + positiveRatio * chartAreaWidth;
|
|
1248
1076
|
}
|
|
1249
1077
|
else {
|
|
1250
|
-
// Value to the left of baseline
|
|
1251
1078
|
const negativeRatio = (baseline - value) / (xMax - xMin);
|
|
1252
1079
|
valueX = baselineX - negativeRatio * chartAreaWidth;
|
|
1253
1080
|
}
|
|
1254
|
-
// Draw horizontal line from baseline to value position
|
|
1255
1081
|
ctx.save();
|
|
1256
1082
|
ctx.strokeStyle = item.color || '#4A90E2';
|
|
1257
1083
|
ctx.lineWidth = lollipopLineWidth;
|
|
@@ -1259,20 +1085,16 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1259
1085
|
ctx.moveTo(baselineX, barCenterY);
|
|
1260
1086
|
ctx.lineTo(valueX, barCenterY);
|
|
1261
1087
|
ctx.stroke();
|
|
1262
|
-
// Draw dot/circle at value position
|
|
1263
1088
|
ctx.fillStyle = item.color || '#4A90E2';
|
|
1264
1089
|
ctx.beginPath();
|
|
1265
1090
|
ctx.arc(valueX, barCenterY, lollipopDotSize / 2, 0, Math.PI * 2);
|
|
1266
1091
|
ctx.fill();
|
|
1267
|
-
// Draw dot border for better visibility
|
|
1268
1092
|
ctx.strokeStyle = item.color || '#4A90E2';
|
|
1269
1093
|
ctx.lineWidth = 1;
|
|
1270
1094
|
ctx.stroke();
|
|
1271
1095
|
ctx.restore();
|
|
1272
|
-
// Store value label for later drawing
|
|
1273
1096
|
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1274
1097
|
if (shouldShowValue) {
|
|
1275
|
-
// Store value label position for this bar (for adjusting bar label position)
|
|
1276
1098
|
if (value >= baseline) {
|
|
1277
1099
|
valueLabelPositions.set(index, { x: valueX + lollipopDotSize / 2 + 5, fontSize: valueFontSize, align: 'left' });
|
|
1278
1100
|
}
|
|
@@ -1287,15 +1109,11 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1287
1109
|
fontSize: valueFontSize
|
|
1288
1110
|
});
|
|
1289
1111
|
}
|
|
1290
|
-
// Set bar bounds for label positioning
|
|
1291
1112
|
barX = baselineX;
|
|
1292
1113
|
barEndX = valueX;
|
|
1293
1114
|
barLength = Math.abs(barEndX - barX);
|
|
1294
1115
|
}
|
|
1295
1116
|
else {
|
|
1296
|
-
// Standard chart: single bar
|
|
1297
|
-
// Calculate bar position and length
|
|
1298
|
-
// If xStart/xEnd are provided, use them for bar range; otherwise use value
|
|
1299
1117
|
if (item.xStart !== undefined || item.xEnd !== undefined) {
|
|
1300
1118
|
const startValue = item.xStart ?? xMin;
|
|
1301
1119
|
const endValue = item.xEnd ?? (item.value ?? 0);
|
|
@@ -1305,7 +1123,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1305
1123
|
barEndX = originX + endRatio * chartAreaWidth;
|
|
1306
1124
|
}
|
|
1307
1125
|
else {
|
|
1308
|
-
// Use value as end position, handle relative to baseline
|
|
1309
1126
|
const value = item.value ?? baseline;
|
|
1310
1127
|
if (value >= baseline) {
|
|
1311
1128
|
const positiveRatio = (value - baseline) / (xMax - xMin);
|
|
@@ -1319,18 +1136,15 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1319
1136
|
}
|
|
1320
1137
|
}
|
|
1321
1138
|
barLength = barEndX - barX;
|
|
1322
|
-
// Draw horizontal bar
|
|
1323
1139
|
ctx.beginPath();
|
|
1324
1140
|
ctx.rect(barX, barY, barLength, calculatedBarHeight);
|
|
1325
1141
|
fillWithGradientOrColor(ctx, item.gradient, item.color || '#4A90E2', '#4A90E2', { x: barX, y: barY, w: barLength, h: calculatedBarHeight });
|
|
1326
1142
|
ctx.fill();
|
|
1327
|
-
// Store value label for later drawing
|
|
1328
1143
|
const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
|
|
1329
1144
|
if (shouldShowValue) {
|
|
1330
1145
|
const value = item.value ?? baseline;
|
|
1331
1146
|
const valueLabelX = value >= baseline ? barEndX + 5 : barX - 5;
|
|
1332
1147
|
const valueLabelAlign = value >= baseline ? 'left' : 'right';
|
|
1333
|
-
// Store value label position for this bar (for adjusting bar label position)
|
|
1334
1148
|
if (value >= baseline) {
|
|
1335
1149
|
valueLabelPositions.set(index, { x: valueLabelX, fontSize: valueFontSize, align: valueLabelAlign });
|
|
1336
1150
|
}
|
|
@@ -1346,7 +1160,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1346
1160
|
});
|
|
1347
1161
|
}
|
|
1348
1162
|
}
|
|
1349
|
-
// Store bar label information for later drawing
|
|
1350
1163
|
if (showBarLabels) {
|
|
1351
1164
|
let labelX, labelY;
|
|
1352
1165
|
let textAlign = 'right';
|
|
@@ -1362,16 +1175,13 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1362
1175
|
case 'right':
|
|
1363
1176
|
labelX = barEndX + 5;
|
|
1364
1177
|
labelY = barCenterY;
|
|
1365
|
-
// Check if there's a value label at the right - if so, position bar label to the right of it
|
|
1366
1178
|
const valueLabelInfo = valueLabelPositions.get(index);
|
|
1367
1179
|
if (valueLabelInfo && valueLabelInfo.align === 'left') {
|
|
1368
|
-
// Value label is at right, so position bar label to the right of it
|
|
1369
|
-
// Calculate spacing: value label width + gap
|
|
1370
1180
|
ctx.save();
|
|
1371
1181
|
ctx.font = `${valueLabelInfo.fontSize}px Arial`;
|
|
1372
1182
|
const valueLabelWidth = ctx.measureText((item.value ?? baseline).toString()).width;
|
|
1373
1183
|
ctx.restore();
|
|
1374
|
-
const spacing = 5;
|
|
1184
|
+
const spacing = 5;
|
|
1375
1185
|
labelX = valueLabelInfo.x + valueLabelWidth + spacing;
|
|
1376
1186
|
}
|
|
1377
1187
|
else {
|
|
@@ -1382,11 +1192,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1382
1192
|
break;
|
|
1383
1193
|
case 'top':
|
|
1384
1194
|
labelX = barX + barLength / 2;
|
|
1385
|
-
// Check if there's a value label - for horizontal charts, value labels are at the end (right side)
|
|
1386
|
-
// So 'top' bar label won't conflict with value labels (they're on different axes)
|
|
1387
|
-
// But we still need to check if value is shown and adjust if needed
|
|
1388
|
-
// For horizontal charts, 'top' means above the bar, value labels are at the end
|
|
1389
|
-
// So no conflict, but if we want to be safe, we can check
|
|
1390
1195
|
labelY = barY - 5;
|
|
1391
1196
|
textAlign = 'center';
|
|
1392
1197
|
textBaseline = 'bottom';
|
|
@@ -1409,8 +1214,6 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1409
1214
|
textAlign = 'right';
|
|
1410
1215
|
textBaseline = 'middle';
|
|
1411
1216
|
}
|
|
1412
|
-
// Calculate label color (for 'inside' position, check if bar is dark)
|
|
1413
|
-
// Use item.labelColor if provided, otherwise fall back to barLabelDefaults.defaultColor
|
|
1414
1217
|
const defaultBarLabelColor = options.labels?.barLabelDefaults?.defaultColor ?? '#000000';
|
|
1415
1218
|
let labelColor = item.labelColor || defaultBarLabelColor;
|
|
1416
1219
|
if (currentLabelPosition === 'inside') {
|
|
@@ -1431,12 +1234,10 @@ async function createHorizontalBarChart(data, options = {}) {
|
|
|
1431
1234
|
});
|
|
1432
1235
|
}
|
|
1433
1236
|
});
|
|
1434
|
-
// Second pass: Draw all labels (values and bar labels) on top of everything
|
|
1435
1237
|
for (const label of labelsToDraw) {
|
|
1436
1238
|
ctx.save();
|
|
1437
1239
|
ctx.textAlign = label.align;
|
|
1438
1240
|
ctx.textBaseline = label.baseline;
|
|
1439
|
-
// Determine text style and gradient based on label type
|
|
1440
1241
|
let textStyle;
|
|
1441
1242
|
let textGradient;
|
|
1442
1243
|
if (label.type === 'bar') {
|