apexify.js 5.1.1 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +240 -0
- package/README.md +244 -1101
- package/dist/cjs/Canvas/ApexPainter.d.ts +183 -204
- package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/cjs/Canvas/ApexPainter.js +524 -1282
- package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
- package/dist/cjs/Canvas/extended/CanvasCreator.d.ts +33 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.js +223 -0
- package/dist/cjs/Canvas/extended/CanvasCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/ChartCreator.d.ts +26 -0
- package/dist/cjs/Canvas/extended/ChartCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/ChartCreator.js +50 -0
- package/dist/cjs/Canvas/extended/ChartCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/GIFCreator.d.ts +43 -0
- package/dist/cjs/Canvas/extended/GIFCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/GIFCreator.js +157 -0
- package/dist/cjs/Canvas/extended/GIFCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/ImageCreator.d.ts +83 -0
- package/dist/cjs/Canvas/extended/ImageCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/ImageCreator.js +479 -0
- package/dist/cjs/Canvas/extended/ImageCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/TextCreator.d.ts +35 -0
- package/dist/cjs/Canvas/extended/TextCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/TextCreator.js +98 -0
- package/dist/cjs/Canvas/extended/TextCreator.js.map +1 -0
- package/dist/cjs/Canvas/extended/VideoCreator.d.ts +370 -0
- package/dist/cjs/Canvas/extended/VideoCreator.d.ts.map +1 -0
- package/dist/cjs/Canvas/extended/VideoCreator.js +478 -0
- package/dist/cjs/Canvas/extended/VideoCreator.js.map +1 -0
- package/dist/cjs/Canvas/utils/Background/bg.d.ts +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Background/bg.js +43 -7
- package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/cjs/Canvas/utils/Charts/barchart.d.ts +230 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.js +1891 -0
- package/dist/cjs/Canvas/utils/Charts/barchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.js +368 -0
- package/dist/cjs/Canvas/utils/Charts/comparisonchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts +181 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
- package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/index.d.ts +45 -0
- package/dist/cjs/Canvas/utils/Charts/index.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/index.js +17 -0
- package/dist/cjs/Canvas/utils/Charts/index.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.d.ts +216 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.js +1761 -0
- package/dist/cjs/Canvas/utils/Charts/linechart.js.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.d.ts +167 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.js +794 -0
- package/dist/cjs/Canvas/utils/Charts/piechart.js.map +1 -0
- package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/batchOperations.js +3 -4
- package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/general functions.js +62 -33
- package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/General/imageStitching.js +3 -6
- package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageMasking.js +5 -12
- package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +4 -4
- package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Image/imageProperties.js +44 -9
- package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
- package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/Texts/textProperties.js +48 -5
- package/dist/cjs/Canvas/utils/Texts/textProperties.js.map +1 -1
- package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts +489 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.js +1835 -0
- package/dist/cjs/Canvas/utils/Video/videoHelpers.js.map +1 -0
- package/dist/cjs/Canvas/utils/errorUtils.d.ts +15 -0
- package/dist/cjs/Canvas/utils/errorUtils.d.ts.map +1 -0
- package/dist/cjs/Canvas/utils/errorUtils.js +26 -0
- package/dist/cjs/Canvas/utils/errorUtils.js.map +1 -0
- package/dist/cjs/Canvas/utils/types.d.ts +17 -178
- package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/types.js.map +1 -1
- package/dist/cjs/Canvas/utils/utils.d.ts +4 -3
- package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/cjs/Canvas/utils/utils.js +40 -6
- package/dist/cjs/Canvas/utils/utils.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -8
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -45
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/Canvas/ApexPainter.d.ts +183 -204
- package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
- package/dist/esm/Canvas/ApexPainter.js +524 -1282
- package/dist/esm/Canvas/ApexPainter.js.map +1 -1
- package/dist/esm/Canvas/extended/CanvasCreator.d.ts +33 -0
- package/dist/esm/Canvas/extended/CanvasCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/CanvasCreator.js +223 -0
- package/dist/esm/Canvas/extended/CanvasCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/ChartCreator.d.ts +26 -0
- package/dist/esm/Canvas/extended/ChartCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/ChartCreator.js +50 -0
- package/dist/esm/Canvas/extended/ChartCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/GIFCreator.d.ts +43 -0
- package/dist/esm/Canvas/extended/GIFCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/GIFCreator.js +157 -0
- package/dist/esm/Canvas/extended/GIFCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/ImageCreator.d.ts +83 -0
- package/dist/esm/Canvas/extended/ImageCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/ImageCreator.js +479 -0
- package/dist/esm/Canvas/extended/ImageCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/TextCreator.d.ts +35 -0
- package/dist/esm/Canvas/extended/TextCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/TextCreator.js +98 -0
- package/dist/esm/Canvas/extended/TextCreator.js.map +1 -0
- package/dist/esm/Canvas/extended/VideoCreator.d.ts +370 -0
- package/dist/esm/Canvas/extended/VideoCreator.d.ts.map +1 -0
- package/dist/esm/Canvas/extended/VideoCreator.js +478 -0
- package/dist/esm/Canvas/extended/VideoCreator.js.map +1 -0
- package/dist/esm/Canvas/utils/Background/bg.d.ts +1 -1
- package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Background/bg.js +43 -7
- package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
- package/dist/esm/Canvas/utils/Charts/barchart.d.ts +230 -0
- package/dist/esm/Canvas/utils/Charts/barchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/barchart.js +1891 -0
- package/dist/esm/Canvas/utils/Charts/barchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.js +368 -0
- package/dist/esm/Canvas/utils/Charts/comparisonchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts +181 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
- package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/index.d.ts +45 -0
- package/dist/esm/Canvas/utils/Charts/index.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/index.js +17 -0
- package/dist/esm/Canvas/utils/Charts/index.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/linechart.d.ts +216 -0
- package/dist/esm/Canvas/utils/Charts/linechart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/linechart.js +1761 -0
- package/dist/esm/Canvas/utils/Charts/linechart.js.map +1 -0
- package/dist/esm/Canvas/utils/Charts/piechart.d.ts +167 -0
- package/dist/esm/Canvas/utils/Charts/piechart.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Charts/piechart.js +794 -0
- package/dist/esm/Canvas/utils/Charts/piechart.js.map +1 -0
- package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/batchOperations.js +3 -4
- package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -1
- package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/general functions.js +62 -33
- package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
- package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/General/imageStitching.js +3 -6
- package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageMasking.js +5 -12
- package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +4 -4
- package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Image/imageProperties.js +44 -9
- package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
- package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.d.ts +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/Texts/textProperties.js +48 -5
- package/dist/esm/Canvas/utils/Texts/textProperties.js.map +1 -1
- package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts +489 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.js +1835 -0
- package/dist/esm/Canvas/utils/Video/videoHelpers.js.map +1 -0
- package/dist/esm/Canvas/utils/errorUtils.d.ts +15 -0
- package/dist/esm/Canvas/utils/errorUtils.d.ts.map +1 -0
- package/dist/esm/Canvas/utils/errorUtils.js +26 -0
- package/dist/esm/Canvas/utils/errorUtils.js.map +1 -0
- package/dist/esm/Canvas/utils/types.d.ts +17 -178
- package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/types.js.map +1 -1
- package/dist/esm/Canvas/utils/utils.d.ts +4 -3
- package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
- package/dist/esm/Canvas/utils/utils.js +40 -6
- package/dist/esm/Canvas/utils/utils.js.map +1 -1
- package/dist/esm/index.d.ts +1 -8
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +14 -45
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +235 -198
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts +0 -13
- package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +0 -1
- package/dist/cjs/Canvas/utils/Charts/charts.js +0 -466
- package/dist/cjs/Canvas/utils/Charts/charts.js.map +0 -1
- package/dist/esm/Canvas/utils/Charts/charts.d.ts +0 -13
- package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +0 -1
- package/dist/esm/Canvas/utils/Charts/charts.js +0 -466
- package/dist/esm/Canvas/utils/Charts/charts.js.map +0 -1
- package/lib/Canvas/ApexPainter.ts +0 -5414
- package/lib/Canvas/utils/Background/bg.ts +0 -285
- package/lib/Canvas/utils/Charts/charts.ts +0 -548
- package/lib/Canvas/utils/Custom/advancedLines.ts +0 -387
- package/lib/Canvas/utils/Custom/customLines.ts +0 -206
- package/lib/Canvas/utils/General/batchOperations.ts +0 -103
- package/lib/Canvas/utils/General/conversion.ts +0 -34
- package/lib/Canvas/utils/General/general functions.ts +0 -726
- package/lib/Canvas/utils/General/imageCompression.ts +0 -316
- package/lib/Canvas/utils/General/imageStitching.ts +0 -252
- package/lib/Canvas/utils/Image/imageEffects.ts +0 -175
- package/lib/Canvas/utils/Image/imageFilters.ts +0 -356
- package/lib/Canvas/utils/Image/imageMasking.ts +0 -335
- package/lib/Canvas/utils/Image/imageProperties.ts +0 -587
- package/lib/Canvas/utils/Image/professionalImageFilters.ts +0 -391
- package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +0 -229
- package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +0 -455
- package/lib/Canvas/utils/Shapes/shapes.ts +0 -528
- package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +0 -716
- package/lib/Canvas/utils/Texts/textPathRenderer.ts +0 -320
- package/lib/Canvas/utils/Texts/textProperties.ts +0 -231
- package/lib/Canvas/utils/types.ts +0 -983
- package/lib/Canvas/utils/utils.ts +0 -135
- package/lib/index.ts +0 -81
- package/lib/utils.ts +0 -5
|
@@ -0,0 +1,1761 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLineChart = createLineChart;
|
|
4
|
+
const canvas_1 = require("@napi-rs/canvas");
|
|
5
|
+
const imageProperties_1 = require("../Image/imageProperties");
|
|
6
|
+
/**
|
|
7
|
+
* Helper function to render enhanced text with custom fonts, gradients, shadows, strokes
|
|
8
|
+
*/
|
|
9
|
+
async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textGradient) {
|
|
10
|
+
ctx.save();
|
|
11
|
+
// Preserve text alignment settings
|
|
12
|
+
const savedTextAlign = ctx.textAlign;
|
|
13
|
+
const savedTextBaseline = ctx.textBaseline;
|
|
14
|
+
const effectiveFontSize = fontSize || style?.fontSize || 16;
|
|
15
|
+
const fontFamily = style?.fontFamily || style?.fontName || 'Arial';
|
|
16
|
+
let fontString = '';
|
|
17
|
+
if (style?.bold)
|
|
18
|
+
fontString += 'bold ';
|
|
19
|
+
if (style?.italic)
|
|
20
|
+
fontString += 'italic ';
|
|
21
|
+
fontString += `${effectiveFontSize}px "${fontFamily}"`;
|
|
22
|
+
ctx.font = fontString;
|
|
23
|
+
// Restore text alignment to ensure correct positioning
|
|
24
|
+
ctx.textAlign = savedTextAlign;
|
|
25
|
+
ctx.textBaseline = savedTextBaseline;
|
|
26
|
+
// Register custom font if provided
|
|
27
|
+
if (style?.fontPath && style?.fontName) {
|
|
28
|
+
try {
|
|
29
|
+
const { GlobalFonts } = await import('@napi-rs/canvas');
|
|
30
|
+
const path = await import('path');
|
|
31
|
+
const fullPath = path.join(process.cwd(), style.fontPath);
|
|
32
|
+
GlobalFonts.registerFromPath(fullPath, style.fontName);
|
|
33
|
+
ctx.font = fontString.replace(`"${fontFamily}"`, `"${style.fontName}"`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.warn(`Failed to register font: ${style.fontPath}`, error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Apply shadow
|
|
40
|
+
if (style?.shadow) {
|
|
41
|
+
ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.5)';
|
|
42
|
+
ctx.shadowOffsetX = style.shadow.offsetX || 2;
|
|
43
|
+
ctx.shadowOffsetY = style.shadow.offsetY || 2;
|
|
44
|
+
ctx.shadowBlur = style.shadow.blur || 4;
|
|
45
|
+
if (style.shadow.opacity !== undefined) {
|
|
46
|
+
ctx.globalAlpha = style.shadow.opacity;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Set fill style (gradient or color)
|
|
50
|
+
if (textGradient) {
|
|
51
|
+
const metrics = ctx.measureText(text);
|
|
52
|
+
ctx.fillStyle = (0, imageProperties_1.createGradientFill)(ctx, textGradient, {
|
|
53
|
+
x, y, w: metrics.width, h: effectiveFontSize
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else if (color) {
|
|
57
|
+
ctx.fillStyle = color;
|
|
58
|
+
}
|
|
59
|
+
// Draw text
|
|
60
|
+
ctx.fillText(text, x, y);
|
|
61
|
+
// Apply stroke
|
|
62
|
+
if (style?.stroke) {
|
|
63
|
+
ctx.strokeStyle = style.stroke.color || '#000000';
|
|
64
|
+
ctx.lineWidth = style.stroke.width || 1;
|
|
65
|
+
if (style.stroke.gradient) {
|
|
66
|
+
const metrics = ctx.measureText(text);
|
|
67
|
+
ctx.strokeStyle = (0, imageProperties_1.createGradientFill)(ctx, style.stroke.gradient, {
|
|
68
|
+
x, y, w: metrics.width, h: effectiveFontSize
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
ctx.strokeText(text, x, y);
|
|
72
|
+
}
|
|
73
|
+
// Reset shadow and alpha
|
|
74
|
+
ctx.shadowColor = 'transparent';
|
|
75
|
+
ctx.shadowOffsetX = 0;
|
|
76
|
+
ctx.shadowOffsetY = 0;
|
|
77
|
+
ctx.shadowBlur = 0;
|
|
78
|
+
ctx.globalAlpha = 1;
|
|
79
|
+
ctx.restore();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Helper function to fill a shape with gradient or color
|
|
83
|
+
*/
|
|
84
|
+
function fillWithGradientOrColor(ctx, gradient, color, defaultColor = '#000000', rect) {
|
|
85
|
+
if (gradient && rect) {
|
|
86
|
+
ctx.fillStyle = (0, imageProperties_1.createGradientFill)(ctx, gradient, rect);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
ctx.fillStyle = color || defaultColor;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Formats a date/timestamp value according to the format string
|
|
94
|
+
*/
|
|
95
|
+
function formatDate(value, format) {
|
|
96
|
+
const date = new Date(value);
|
|
97
|
+
const year = date.getFullYear();
|
|
98
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
99
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
100
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
101
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
102
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
103
|
+
return format
|
|
104
|
+
.replace(/YYYY/g, String(year))
|
|
105
|
+
.replace(/MM/g, month)
|
|
106
|
+
.replace(/DD/g, day)
|
|
107
|
+
.replace(/HH/g, hours)
|
|
108
|
+
.replace(/mm/g, minutes)
|
|
109
|
+
.replace(/ss/g, seconds);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Converts a linear value to logarithmic scale position
|
|
113
|
+
*/
|
|
114
|
+
function logScale(value, min, max) {
|
|
115
|
+
if (value <= 0)
|
|
116
|
+
return 0;
|
|
117
|
+
const logMin = Math.log10(min);
|
|
118
|
+
const logMax = Math.log10(max);
|
|
119
|
+
const logValue = Math.log10(value);
|
|
120
|
+
return (logValue - logMin) / (logMax - logMin);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Converts a logarithmic scale position back to linear value
|
|
124
|
+
*/
|
|
125
|
+
function logScaleInverse(position, min, max) {
|
|
126
|
+
const logMin = Math.log10(min);
|
|
127
|
+
const logMax = Math.log10(max);
|
|
128
|
+
const logValue = logMin + position * (logMax - logMin);
|
|
129
|
+
return Math.pow(10, logValue);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Helper function to draw an arrow
|
|
133
|
+
*/
|
|
134
|
+
function drawArrow(ctx, x, y, angle, size) {
|
|
135
|
+
ctx.save();
|
|
136
|
+
ctx.translate(x, y);
|
|
137
|
+
ctx.rotate(angle);
|
|
138
|
+
ctx.beginPath();
|
|
139
|
+
ctx.moveTo(0, 0);
|
|
140
|
+
ctx.lineTo(-size, -size / 2);
|
|
141
|
+
ctx.lineTo(-size, size / 2);
|
|
142
|
+
ctx.closePath();
|
|
143
|
+
ctx.fill();
|
|
144
|
+
ctx.restore();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Draws Y-axis ticks and labels with custom values support
|
|
148
|
+
*/
|
|
149
|
+
function drawYAxisTicks(ctx, originX, originY, axisEndY, minValue, maxValue, step, tickFontSize, customValues, valueSpacing, scale = 'linear', dateFormat, isDateTime = false) {
|
|
150
|
+
ctx.save();
|
|
151
|
+
ctx.fillStyle = '#000000';
|
|
152
|
+
ctx.font = `${tickFontSize}px Arial`;
|
|
153
|
+
ctx.textAlign = 'right';
|
|
154
|
+
ctx.textBaseline = 'middle';
|
|
155
|
+
const chartHeight = originY - axisEndY;
|
|
156
|
+
if (customValues && customValues.length > 0) {
|
|
157
|
+
const actualMin = Math.min(...customValues);
|
|
158
|
+
const actualMax = Math.max(...customValues);
|
|
159
|
+
const range = actualMax - actualMin;
|
|
160
|
+
let lastLabelY = Infinity;
|
|
161
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30;
|
|
162
|
+
customValues.forEach((value) => {
|
|
163
|
+
const y = originY - ((value - actualMin) / range) * chartHeight;
|
|
164
|
+
if (Math.abs(y - lastLabelY) < minLabelSpacing) {
|
|
165
|
+
ctx.beginPath();
|
|
166
|
+
ctx.moveTo(originX - 5, y);
|
|
167
|
+
ctx.lineTo(originX, y);
|
|
168
|
+
ctx.stroke();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
ctx.beginPath();
|
|
172
|
+
ctx.moveTo(originX - 5, y);
|
|
173
|
+
ctx.lineTo(originX, y);
|
|
174
|
+
ctx.stroke();
|
|
175
|
+
let labelText;
|
|
176
|
+
if (isDateTime && dateFormat) {
|
|
177
|
+
labelText = formatDate(value, dateFormat);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
labelText = value.toFixed(1);
|
|
181
|
+
}
|
|
182
|
+
ctx.fillText(labelText, originX - 10, y);
|
|
183
|
+
lastLabelY = y;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
let lastLabelY = Infinity;
|
|
188
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : (tickFontSize + 5);
|
|
189
|
+
if (scale === 'log' && minValue > 0 && maxValue > 0) {
|
|
190
|
+
// Logarithmic scale: generate ticks at powers of 10
|
|
191
|
+
const logMin = Math.floor(Math.log10(minValue));
|
|
192
|
+
const logMax = Math.ceil(Math.log10(maxValue));
|
|
193
|
+
for (let power = logMin; power <= logMax; power++) {
|
|
194
|
+
const value = Math.pow(10, power);
|
|
195
|
+
if (value < minValue || value > maxValue)
|
|
196
|
+
continue;
|
|
197
|
+
const logPos = logScale(value, minValue, maxValue);
|
|
198
|
+
const y = originY - logPos * chartHeight;
|
|
199
|
+
if (lastLabelY - y < minLabelSpacing && power !== logMin) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
ctx.beginPath();
|
|
203
|
+
ctx.moveTo(originX - 5, y);
|
|
204
|
+
ctx.lineTo(originX, y);
|
|
205
|
+
ctx.stroke();
|
|
206
|
+
let labelText;
|
|
207
|
+
if (isDateTime && dateFormat) {
|
|
208
|
+
labelText = formatDate(value, dateFormat);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
labelText = value.toFixed(1);
|
|
212
|
+
}
|
|
213
|
+
ctx.fillText(labelText, originX - 10, y);
|
|
214
|
+
lastLabelY = y;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Linear scale
|
|
219
|
+
const range = maxValue - minValue;
|
|
220
|
+
for (let value = minValue; value <= maxValue; value += step) {
|
|
221
|
+
const y = originY - ((value - minValue) / range) * chartHeight;
|
|
222
|
+
if (lastLabelY - y < minLabelSpacing && value !== minValue) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
ctx.beginPath();
|
|
226
|
+
ctx.moveTo(originX - 5, y);
|
|
227
|
+
ctx.lineTo(originX, y);
|
|
228
|
+
ctx.stroke();
|
|
229
|
+
let labelText;
|
|
230
|
+
if (isDateTime && dateFormat) {
|
|
231
|
+
labelText = formatDate(value, dateFormat);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
labelText = value.toFixed(1);
|
|
235
|
+
}
|
|
236
|
+
ctx.fillText(labelText, originX - 10, y);
|
|
237
|
+
lastLabelY = y;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
ctx.restore();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Draws X-axis ticks and labels with custom values
|
|
245
|
+
*/
|
|
246
|
+
function drawXAxisTicks(ctx, originX, originY, axisEndX, minValue, maxValue, step, tickFontSize, customValues, valueSpacing, scale = 'linear', dateFormat, isDateTime = false) {
|
|
247
|
+
ctx.save();
|
|
248
|
+
ctx.fillStyle = '#000000';
|
|
249
|
+
ctx.font = `${tickFontSize}px Arial`;
|
|
250
|
+
ctx.textAlign = 'center';
|
|
251
|
+
ctx.textBaseline = 'top';
|
|
252
|
+
const chartWidth = axisEndX - originX;
|
|
253
|
+
if (customValues && customValues.length > 0) {
|
|
254
|
+
if (valueSpacing && valueSpacing > 0) {
|
|
255
|
+
let currentX = originX;
|
|
256
|
+
customValues.forEach((value, index) => {
|
|
257
|
+
if (index === 0) {
|
|
258
|
+
currentX = originX;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
currentX += valueSpacing;
|
|
262
|
+
}
|
|
263
|
+
if (currentX >= originX && currentX <= axisEndX) {
|
|
264
|
+
ctx.beginPath();
|
|
265
|
+
ctx.moveTo(currentX, originY);
|
|
266
|
+
ctx.lineTo(currentX, originY + 5);
|
|
267
|
+
ctx.stroke();
|
|
268
|
+
let labelText;
|
|
269
|
+
if (isDateTime && dateFormat) {
|
|
270
|
+
labelText = formatDate(value, dateFormat);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
labelText = value.toString();
|
|
274
|
+
}
|
|
275
|
+
ctx.fillText(labelText, currentX, originY + 10);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const numValues = customValues.length;
|
|
281
|
+
let lastLabelX = -Infinity;
|
|
282
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40;
|
|
283
|
+
customValues.forEach((value, index) => {
|
|
284
|
+
const x = originX + (index / (numValues - 1)) * chartWidth;
|
|
285
|
+
if (x - lastLabelX < minLabelSpacing && index > 0) {
|
|
286
|
+
ctx.beginPath();
|
|
287
|
+
ctx.moveTo(x, originY);
|
|
288
|
+
ctx.lineTo(x, originY + 5);
|
|
289
|
+
ctx.stroke();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
ctx.beginPath();
|
|
293
|
+
ctx.moveTo(x, originY);
|
|
294
|
+
ctx.lineTo(x, originY + 5);
|
|
295
|
+
ctx.stroke();
|
|
296
|
+
let labelText;
|
|
297
|
+
if (isDateTime && dateFormat) {
|
|
298
|
+
labelText = formatDate(value, dateFormat);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
labelText = value.toString();
|
|
302
|
+
}
|
|
303
|
+
ctx.fillText(labelText, x, originY + 10);
|
|
304
|
+
lastLabelX = x;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const range = maxValue - minValue;
|
|
310
|
+
let lastLabelX = -Infinity;
|
|
311
|
+
const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40;
|
|
312
|
+
for (let value = minValue; value <= maxValue; value += step) {
|
|
313
|
+
const x = originX + ((value - minValue) / range) * chartWidth;
|
|
314
|
+
if (x - lastLabelX < minLabelSpacing && value !== minValue) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
ctx.beginPath();
|
|
318
|
+
ctx.moveTo(x, originY);
|
|
319
|
+
ctx.lineTo(x, originY + 5);
|
|
320
|
+
ctx.stroke();
|
|
321
|
+
let labelText;
|
|
322
|
+
if (isDateTime && dateFormat) {
|
|
323
|
+
labelText = formatDate(value, dateFormat);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
labelText = value.toFixed(1);
|
|
327
|
+
}
|
|
328
|
+
ctx.fillText(labelText, x, originY + 10);
|
|
329
|
+
lastLabelX = x;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
ctx.restore();
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Draws grid lines
|
|
336
|
+
*/
|
|
337
|
+
function drawGrid(ctx, originX, originY, axisEndX, axisEndY, minValue, maxValue, step, gridColor, gridWidth, isVertical, customValues) {
|
|
338
|
+
ctx.save();
|
|
339
|
+
ctx.strokeStyle = gridColor;
|
|
340
|
+
ctx.lineWidth = gridWidth;
|
|
341
|
+
ctx.setLineDash([5, 5]);
|
|
342
|
+
if (isVertical) {
|
|
343
|
+
// Vertical grid lines (X-axis ticks)
|
|
344
|
+
if (customValues && customValues.length > 0) {
|
|
345
|
+
const chartWidth = axisEndX - originX;
|
|
346
|
+
const numValues = customValues.length;
|
|
347
|
+
customValues.forEach((value, index) => {
|
|
348
|
+
const x = originX + (index / (numValues - 1)) * chartWidth;
|
|
349
|
+
ctx.beginPath();
|
|
350
|
+
ctx.moveTo(x, axisEndY);
|
|
351
|
+
ctx.lineTo(x, originY);
|
|
352
|
+
ctx.stroke();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
const range = maxValue - minValue;
|
|
357
|
+
for (let value = minValue; value <= maxValue; value += step) {
|
|
358
|
+
const x = originX + ((value - minValue) / range) * (axisEndX - originX);
|
|
359
|
+
ctx.beginPath();
|
|
360
|
+
ctx.moveTo(x, axisEndY);
|
|
361
|
+
ctx.lineTo(x, originY);
|
|
362
|
+
ctx.stroke();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
// Horizontal grid lines (Y-axis ticks)
|
|
368
|
+
if (customValues && customValues.length > 0) {
|
|
369
|
+
const chartHeight = originY - axisEndY;
|
|
370
|
+
const actualMin = Math.min(...customValues);
|
|
371
|
+
const actualMax = Math.max(...customValues);
|
|
372
|
+
const range = actualMax - actualMin;
|
|
373
|
+
customValues.forEach((value) => {
|
|
374
|
+
const y = originY - ((value - actualMin) / range) * chartHeight;
|
|
375
|
+
ctx.beginPath();
|
|
376
|
+
ctx.moveTo(originX, y);
|
|
377
|
+
ctx.lineTo(axisEndX, y);
|
|
378
|
+
ctx.stroke();
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const range = maxValue - minValue;
|
|
383
|
+
const chartHeight = originY - axisEndY;
|
|
384
|
+
for (let value = minValue; value <= maxValue; value += step) {
|
|
385
|
+
const y = originY - ((value - minValue) / range) * chartHeight;
|
|
386
|
+
ctx.beginPath();
|
|
387
|
+
ctx.moveTo(originX, y);
|
|
388
|
+
ctx.lineTo(axisEndX, y);
|
|
389
|
+
ctx.stroke();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
ctx.restore();
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Draws an error bar at a point
|
|
397
|
+
*/
|
|
398
|
+
function drawErrorBar(ctx, x, y, positive, negative, color, width, capSize, chartAreaHeight, yMin, yMax) {
|
|
399
|
+
ctx.save();
|
|
400
|
+
ctx.strokeStyle = color;
|
|
401
|
+
ctx.lineWidth = width;
|
|
402
|
+
// Convert error values to pixels
|
|
403
|
+
const positivePixels = (positive / (yMax - yMin)) * chartAreaHeight;
|
|
404
|
+
const negativePixels = (negative / (yMax - yMin)) * chartAreaHeight;
|
|
405
|
+
const topY = y - positivePixels;
|
|
406
|
+
const bottomY = y + negativePixels;
|
|
407
|
+
// Draw vertical line
|
|
408
|
+
ctx.beginPath();
|
|
409
|
+
ctx.moveTo(x, topY);
|
|
410
|
+
ctx.lineTo(x, bottomY);
|
|
411
|
+
ctx.stroke();
|
|
412
|
+
// Draw top cap
|
|
413
|
+
ctx.beginPath();
|
|
414
|
+
ctx.moveTo(x - capSize / 2, topY);
|
|
415
|
+
ctx.lineTo(x + capSize / 2, topY);
|
|
416
|
+
ctx.stroke();
|
|
417
|
+
// Draw bottom cap
|
|
418
|
+
ctx.beginPath();
|
|
419
|
+
ctx.moveTo(x - capSize / 2, bottomY);
|
|
420
|
+
ctx.lineTo(x + capSize / 2, bottomY);
|
|
421
|
+
ctx.stroke();
|
|
422
|
+
ctx.restore();
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Draws a marker at a point
|
|
426
|
+
*/
|
|
427
|
+
function drawMarker(ctx, x, y, type, size, color, filled = true) {
|
|
428
|
+
ctx.save();
|
|
429
|
+
ctx.fillStyle = color;
|
|
430
|
+
ctx.strokeStyle = color;
|
|
431
|
+
ctx.lineWidth = 2;
|
|
432
|
+
switch (type) {
|
|
433
|
+
case 'circle':
|
|
434
|
+
ctx.beginPath();
|
|
435
|
+
ctx.arc(x, y, size / 2, 0, Math.PI * 2);
|
|
436
|
+
if (filled) {
|
|
437
|
+
ctx.fill();
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
ctx.stroke();
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
case 'square':
|
|
444
|
+
if (filled) {
|
|
445
|
+
ctx.fillRect(x - size / 2, y - size / 2, size, size);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
case 'triangle':
|
|
452
|
+
ctx.beginPath();
|
|
453
|
+
ctx.moveTo(x, y - size / 2);
|
|
454
|
+
ctx.lineTo(x - size / 2, y + size / 2);
|
|
455
|
+
ctx.lineTo(x + size / 2, y + size / 2);
|
|
456
|
+
ctx.closePath();
|
|
457
|
+
if (filled) {
|
|
458
|
+
ctx.fill();
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
ctx.stroke();
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
case 'diamond':
|
|
465
|
+
ctx.beginPath();
|
|
466
|
+
ctx.moveTo(x, y - size / 2);
|
|
467
|
+
ctx.lineTo(x + size / 2, y);
|
|
468
|
+
ctx.lineTo(x, y + size / 2);
|
|
469
|
+
ctx.lineTo(x - size / 2, y);
|
|
470
|
+
ctx.closePath();
|
|
471
|
+
if (filled) {
|
|
472
|
+
ctx.fill();
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
ctx.stroke();
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
case 'cross':
|
|
479
|
+
// Cross is always stroked, not filled
|
|
480
|
+
ctx.lineWidth = 2;
|
|
481
|
+
ctx.beginPath();
|
|
482
|
+
ctx.moveTo(x - size / 2, y - size / 2);
|
|
483
|
+
ctx.lineTo(x + size / 2, y + size / 2);
|
|
484
|
+
ctx.moveTo(x + size / 2, y - size / 2);
|
|
485
|
+
ctx.lineTo(x - size / 2, y + size / 2);
|
|
486
|
+
ctx.stroke();
|
|
487
|
+
break;
|
|
488
|
+
case 'none':
|
|
489
|
+
// Do nothing
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
ctx.restore();
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Applies line style to context
|
|
496
|
+
*/
|
|
497
|
+
function applyLineStyle(ctx, style) {
|
|
498
|
+
switch (style) {
|
|
499
|
+
case 'solid':
|
|
500
|
+
ctx.setLineDash([]);
|
|
501
|
+
break;
|
|
502
|
+
case 'dashed':
|
|
503
|
+
ctx.setLineDash([10, 5]);
|
|
504
|
+
break;
|
|
505
|
+
case 'dotted':
|
|
506
|
+
ctx.setLineDash([2, 5]);
|
|
507
|
+
break;
|
|
508
|
+
case 'dashdot':
|
|
509
|
+
ctx.setLineDash([10, 5, 2, 5]);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Calculates Bezier control points for smooth curve
|
|
515
|
+
*/
|
|
516
|
+
function calculateBezierControlPoints(points, tension = 0.5) {
|
|
517
|
+
const controlPoints = [];
|
|
518
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
519
|
+
const p0 = i > 0 ? points[i - 1] : points[i];
|
|
520
|
+
const p1 = points[i];
|
|
521
|
+
const p2 = points[i + 1];
|
|
522
|
+
const p3 = i < points.length - 2 ? points[i + 2] : points[i + 1];
|
|
523
|
+
const cp1x = p1.x + (p2.x - p0.x) * tension;
|
|
524
|
+
const cp1y = p1.y + (p2.y - p0.y) * tension;
|
|
525
|
+
const cp2x = p2.x - (p3.x - p1.x) * tension;
|
|
526
|
+
const cp2y = p2.y - (p3.y - p1.y) * tension;
|
|
527
|
+
controlPoints.push({ cp1x, cp1y, cp2x, cp2y });
|
|
528
|
+
}
|
|
529
|
+
return controlPoints;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Calculates cubic spline interpolation points
|
|
533
|
+
* Uses natural cubic spline interpolation
|
|
534
|
+
*/
|
|
535
|
+
function calculateSplinePoints(points) {
|
|
536
|
+
if (points.length < 2)
|
|
537
|
+
return points;
|
|
538
|
+
if (points.length === 2) {
|
|
539
|
+
// Just return the two points
|
|
540
|
+
return points;
|
|
541
|
+
}
|
|
542
|
+
const n = points.length;
|
|
543
|
+
const h = [];
|
|
544
|
+
const alpha = [];
|
|
545
|
+
const l = [];
|
|
546
|
+
const mu = [];
|
|
547
|
+
const z = [];
|
|
548
|
+
const c = [];
|
|
549
|
+
const b = [];
|
|
550
|
+
const d = [];
|
|
551
|
+
// Calculate h (differences in x)
|
|
552
|
+
for (let i = 0; i < n - 1; i++) {
|
|
553
|
+
h.push(points[i + 1].x - points[i].x);
|
|
554
|
+
}
|
|
555
|
+
// Calculate alpha (for natural spline, alpha[0] = alpha[n-1] = 0)
|
|
556
|
+
alpha[0] = 0;
|
|
557
|
+
for (let i = 1; i < n - 1; i++) {
|
|
558
|
+
alpha[i] = (3 / h[i]) * (points[i + 1].y - points[i].y) - (3 / h[i - 1]) * (points[i].y - points[i - 1].y);
|
|
559
|
+
}
|
|
560
|
+
alpha[n - 1] = 0;
|
|
561
|
+
// Solve tridiagonal system
|
|
562
|
+
l[0] = 1;
|
|
563
|
+
mu[0] = 0;
|
|
564
|
+
z[0] = 0;
|
|
565
|
+
for (let i = 1; i < n - 1; i++) {
|
|
566
|
+
l[i] = 2 * (points[i + 1].x - points[i - 1].x) - h[i - 1] * mu[i - 1];
|
|
567
|
+
mu[i] = h[i] / l[i];
|
|
568
|
+
z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
|
|
569
|
+
}
|
|
570
|
+
l[n - 1] = 1;
|
|
571
|
+
z[n - 1] = 0;
|
|
572
|
+
c[n - 1] = 0;
|
|
573
|
+
// Back substitution
|
|
574
|
+
for (let j = n - 2; j >= 0; j--) {
|
|
575
|
+
c[j] = z[j] - mu[j] * c[j + 1];
|
|
576
|
+
b[j] = (points[j + 1].y - points[j].y) / h[j] - h[j] * (c[j + 1] + 2 * c[j]) / 3;
|
|
577
|
+
d[j] = (c[j + 1] - c[j]) / (3 * h[j]);
|
|
578
|
+
}
|
|
579
|
+
// Generate interpolated points
|
|
580
|
+
const splinePoints = [];
|
|
581
|
+
const numPointsPerSegment = 20; // Number of interpolated points between each pair
|
|
582
|
+
for (let i = 0; i < n - 1; i++) {
|
|
583
|
+
const x0 = points[i].x;
|
|
584
|
+
const y0 = points[i].y;
|
|
585
|
+
const a = y0;
|
|
586
|
+
const b_coeff = b[i];
|
|
587
|
+
const c_coeff = c[i];
|
|
588
|
+
const d_coeff = d[i];
|
|
589
|
+
for (let j = 0; j <= numPointsPerSegment; j++) {
|
|
590
|
+
const t = j / numPointsPerSegment;
|
|
591
|
+
const x = x0 + t * h[i];
|
|
592
|
+
const dx = x - x0;
|
|
593
|
+
const y = a + b_coeff * dx + c_coeff * dx * dx + d_coeff * dx * dx * dx;
|
|
594
|
+
splinePoints.push({ x, y });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return splinePoints;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Calculates linear regression (y = mx + b)
|
|
601
|
+
*/
|
|
602
|
+
function calculateLinearRegression(points) {
|
|
603
|
+
const n = points.length;
|
|
604
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
|
|
605
|
+
points.forEach(p => {
|
|
606
|
+
sumX += p.x;
|
|
607
|
+
sumY += p.y;
|
|
608
|
+
sumXY += p.x * p.y;
|
|
609
|
+
sumXX += p.x * p.x;
|
|
610
|
+
});
|
|
611
|
+
const m = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
612
|
+
const b = (sumY - m * sumX) / n;
|
|
613
|
+
return { m, b };
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Calculates polynomial regression (y = a0 + a1*x + a2*x^2 + ... + an*x^n)
|
|
617
|
+
*/
|
|
618
|
+
function calculatePolynomialRegression(points, degree = 2) {
|
|
619
|
+
const n = points.length;
|
|
620
|
+
const m = degree + 1;
|
|
621
|
+
// Build the Vandermonde matrix
|
|
622
|
+
const X = points.map(p => {
|
|
623
|
+
const row = [];
|
|
624
|
+
for (let i = 0; i <= degree; i++) {
|
|
625
|
+
row.push(Math.pow(p.x, i));
|
|
626
|
+
}
|
|
627
|
+
return row;
|
|
628
|
+
});
|
|
629
|
+
// Calculate X^T * X
|
|
630
|
+
const XTX = [];
|
|
631
|
+
for (let i = 0; i <= degree; i++) {
|
|
632
|
+
XTX[i] = [];
|
|
633
|
+
for (let j = 0; j <= degree; j++) {
|
|
634
|
+
let sum = 0;
|
|
635
|
+
for (let k = 0; k < n; k++) {
|
|
636
|
+
sum += X[k][i] * X[k][j];
|
|
637
|
+
}
|
|
638
|
+
XTX[i][j] = sum;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Calculate X^T * y
|
|
642
|
+
const XTy = [];
|
|
643
|
+
for (let i = 0; i <= degree; i++) {
|
|
644
|
+
let sum = 0;
|
|
645
|
+
for (let k = 0; k < n; k++) {
|
|
646
|
+
sum += X[k][i] * points[k].y;
|
|
647
|
+
}
|
|
648
|
+
XTy[i] = sum;
|
|
649
|
+
}
|
|
650
|
+
// Solve using Gaussian elimination
|
|
651
|
+
const coefficients = new Array(m).fill(0);
|
|
652
|
+
for (let i = 0; i <= degree; i++) {
|
|
653
|
+
// Find pivot
|
|
654
|
+
let maxRow = i;
|
|
655
|
+
for (let k = i + 1; k <= degree; k++) {
|
|
656
|
+
if (Math.abs(XTX[k][i]) > Math.abs(XTX[maxRow][i])) {
|
|
657
|
+
maxRow = k;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Swap rows
|
|
661
|
+
[XTX[i], XTX[maxRow]] = [XTX[maxRow], XTX[i]];
|
|
662
|
+
[XTy[i], XTy[maxRow]] = [XTy[maxRow], XTy[i]];
|
|
663
|
+
// Eliminate
|
|
664
|
+
for (let k = i + 1; k <= degree; k++) {
|
|
665
|
+
const factor = XTX[k][i] / XTX[i][i];
|
|
666
|
+
for (let j = i; j <= degree; j++) {
|
|
667
|
+
XTX[k][j] -= factor * XTX[i][j];
|
|
668
|
+
}
|
|
669
|
+
XTy[k] -= factor * XTy[i];
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// Back substitution
|
|
673
|
+
for (let i = degree; i >= 0; i--) {
|
|
674
|
+
coefficients[i] = XTy[i];
|
|
675
|
+
for (let j = i + 1; j <= degree; j++) {
|
|
676
|
+
coefficients[i] -= XTX[i][j] * coefficients[j];
|
|
677
|
+
}
|
|
678
|
+
coefficients[i] /= XTX[i][i];
|
|
679
|
+
}
|
|
680
|
+
return coefficients;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Calculates exponential regression (y = a * e^(b*x))
|
|
684
|
+
*/
|
|
685
|
+
function calculateExponentialRegression(points) {
|
|
686
|
+
// Transform to linear: ln(y) = ln(a) + b*x
|
|
687
|
+
const transformedPoints = points
|
|
688
|
+
.filter(p => p.y > 0)
|
|
689
|
+
.map(p => ({ x: p.x, y: Math.log(p.y) }));
|
|
690
|
+
if (transformedPoints.length < 2) {
|
|
691
|
+
return { a: 1, b: 0 };
|
|
692
|
+
}
|
|
693
|
+
const linear = calculateLinearRegression(transformedPoints);
|
|
694
|
+
return { a: Math.exp(linear.b), b: linear.m };
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Calculates logarithmic regression (y = a + b*ln(x))
|
|
698
|
+
*/
|
|
699
|
+
function calculateLogarithmicRegression(points) {
|
|
700
|
+
// Transform to linear: y = a + b*ln(x)
|
|
701
|
+
const transformedPoints = points
|
|
702
|
+
.filter(p => p.x > 0)
|
|
703
|
+
.map(p => ({ x: Math.log(p.x), y: p.y }));
|
|
704
|
+
if (transformedPoints.length < 2) {
|
|
705
|
+
return { a: 0, b: 0 };
|
|
706
|
+
}
|
|
707
|
+
const linear = calculateLinearRegression(transformedPoints);
|
|
708
|
+
return { a: linear.b, b: linear.m };
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Generates correlation line points based on regression type
|
|
712
|
+
*/
|
|
713
|
+
function generateCorrelationPoints(points, correlationType, xMin, xMax, degree) {
|
|
714
|
+
if (correlationType === 'none' || points.length < 2) {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
const numPoints = 100; // Number of points to generate for smooth line
|
|
718
|
+
const correlationPoints = [];
|
|
719
|
+
switch (correlationType) {
|
|
720
|
+
case 'linear': {
|
|
721
|
+
const { m, b } = calculateLinearRegression(points);
|
|
722
|
+
for (let i = 0; i <= numPoints; i++) {
|
|
723
|
+
const x = xMin + (i / numPoints) * (xMax - xMin);
|
|
724
|
+
const y = m * x + b;
|
|
725
|
+
correlationPoints.push({ x, y });
|
|
726
|
+
}
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
case 'polynomial': {
|
|
730
|
+
const polyDegree = degree ?? 2;
|
|
731
|
+
const coefficients = calculatePolynomialRegression(points, polyDegree);
|
|
732
|
+
for (let i = 0; i <= numPoints; i++) {
|
|
733
|
+
const x = xMin + (i / numPoints) * (xMax - xMin);
|
|
734
|
+
let y = 0;
|
|
735
|
+
for (let j = 0; j < coefficients.length; j++) {
|
|
736
|
+
y += coefficients[j] * Math.pow(x, j);
|
|
737
|
+
}
|
|
738
|
+
correlationPoints.push({ x, y });
|
|
739
|
+
}
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
case 'exponential': {
|
|
743
|
+
const { a, b } = calculateExponentialRegression(points);
|
|
744
|
+
for (let i = 0; i <= numPoints; i++) {
|
|
745
|
+
const x = xMin + (i / numPoints) * (xMax - xMin);
|
|
746
|
+
const y = a * Math.exp(b * x);
|
|
747
|
+
correlationPoints.push({ x, y });
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
case 'logarithmic': {
|
|
752
|
+
const { a, b } = calculateLogarithmicRegression(points);
|
|
753
|
+
for (let i = 0; i <= numPoints; i++) {
|
|
754
|
+
const x = xMin + (i / numPoints) * (xMax - xMin);
|
|
755
|
+
if (x > 0) {
|
|
756
|
+
const y = a + b * Math.log(x);
|
|
757
|
+
correlationPoints.push({ x, y });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return correlationPoints;
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Wraps text to fit within a maximum width
|
|
767
|
+
*/
|
|
768
|
+
function wrapText(ctx, text, maxWidth) {
|
|
769
|
+
const words = text.split(' ');
|
|
770
|
+
const lines = [];
|
|
771
|
+
let currentLine = words[0];
|
|
772
|
+
for (let i = 1; i < words.length; i++) {
|
|
773
|
+
const word = words[i];
|
|
774
|
+
const width = ctx.measureText(currentLine + ' ' + word).width;
|
|
775
|
+
if (width < maxWidth) {
|
|
776
|
+
currentLine += ' ' + word;
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
lines.push(currentLine);
|
|
780
|
+
currentLine = word;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
lines.push(currentLine);
|
|
784
|
+
return lines;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Calculates legend dimensions
|
|
788
|
+
*/
|
|
789
|
+
function calculateLegendDimensions(entries, spacing, fontSize = 12, maxWidth, wrapTextEnabled = true, padding) {
|
|
790
|
+
if (!entries || entries.length === 0) {
|
|
791
|
+
return { width: 0, height: 0 };
|
|
792
|
+
}
|
|
793
|
+
const boxSize = 15;
|
|
794
|
+
const entrySpacing = spacing || 15;
|
|
795
|
+
const paddingBox = padding ?? 8;
|
|
796
|
+
// Create a temporary canvas to measure text
|
|
797
|
+
const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
|
|
798
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
799
|
+
tempCtx.font = `${fontSize}px Arial`;
|
|
800
|
+
const textSpacing = 10;
|
|
801
|
+
const effectiveMaxWidth = maxWidth ? maxWidth - paddingBox * 2 - boxSize - textSpacing : undefined;
|
|
802
|
+
let maxEntryWidth = 0;
|
|
803
|
+
const entryHeights = [];
|
|
804
|
+
entries.forEach(entry => {
|
|
805
|
+
let textWidth;
|
|
806
|
+
let textHeight;
|
|
807
|
+
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
808
|
+
const wrappedLines = wrapText(tempCtx, entry.label, effectiveMaxWidth);
|
|
809
|
+
textWidth = Math.max(...wrappedLines.map(line => tempCtx.measureText(line).width));
|
|
810
|
+
textHeight = wrappedLines.length * fontSize * 1.2;
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
textWidth = tempCtx.measureText(entry.label).width;
|
|
814
|
+
textHeight = fontSize;
|
|
815
|
+
}
|
|
816
|
+
const entryWidth = boxSize + textSpacing + textWidth;
|
|
817
|
+
maxEntryWidth = Math.max(maxEntryWidth, entryWidth);
|
|
818
|
+
entryHeights.push(Math.max(boxSize, textHeight));
|
|
819
|
+
});
|
|
820
|
+
const width = maxWidth ? maxWidth : Math.max(200, maxEntryWidth + paddingBox * 2);
|
|
821
|
+
const height = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? entrySpacing : 0), 0) + paddingBox * 2;
|
|
822
|
+
return { width, height };
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Draws legend
|
|
826
|
+
*/
|
|
827
|
+
async function drawLegend(ctx, x, y, entries, spacing, fontSize = 12, backgroundColor, borderColor, textColor, padding, maxWidth, wrapTextEnabled = true, backgroundGradient, textGradient, textStyle) {
|
|
828
|
+
if (!entries || entries.length === 0)
|
|
829
|
+
return;
|
|
830
|
+
ctx.save();
|
|
831
|
+
const boxSize = 15;
|
|
832
|
+
const entrySpacing = spacing || 15;
|
|
833
|
+
const textSpacing = 10;
|
|
834
|
+
const paddingBox = padding ?? 8;
|
|
835
|
+
ctx.font = `${fontSize}px Arial`;
|
|
836
|
+
// Calculate dimensions with text wrapping
|
|
837
|
+
const effectiveMaxWidth = maxWidth ? maxWidth - paddingBox * 2 - boxSize - textSpacing : undefined;
|
|
838
|
+
const entryHeights = [];
|
|
839
|
+
entries.forEach(entry => {
|
|
840
|
+
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
841
|
+
const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
|
|
842
|
+
const textHeight = wrappedLines.length * fontSize * 1.2;
|
|
843
|
+
entryHeights.push(Math.max(boxSize, textHeight));
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
entryHeights.push(boxSize);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
const legendHeight = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? entrySpacing : 0), 0) + paddingBox * 2;
|
|
850
|
+
let legendWidth = 200;
|
|
851
|
+
if (maxWidth) {
|
|
852
|
+
legendWidth = maxWidth;
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
let maxEntryWidth = 0;
|
|
856
|
+
entries.forEach((entry, index) => {
|
|
857
|
+
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
858
|
+
const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
|
|
859
|
+
const textWidth = Math.max(...wrappedLines.map(line => ctx.measureText(line).width));
|
|
860
|
+
maxEntryWidth = Math.max(maxEntryWidth, boxSize + textSpacing + textWidth);
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
const textWidth = ctx.measureText(entry.label).width;
|
|
864
|
+
maxEntryWidth = Math.max(maxEntryWidth, boxSize + textSpacing + textWidth);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
legendWidth = Math.max(200, maxEntryWidth + paddingBox * 2);
|
|
868
|
+
}
|
|
869
|
+
// Draw legend background (gradient or color) if provided
|
|
870
|
+
if (backgroundColor || backgroundGradient) {
|
|
871
|
+
ctx.beginPath();
|
|
872
|
+
ctx.rect(x, y, legendWidth, legendHeight);
|
|
873
|
+
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor || 'rgba(255, 255, 255, 0.9)', { x, y, w: legendWidth, h: legendHeight });
|
|
874
|
+
ctx.fill();
|
|
875
|
+
if (borderColor) {
|
|
876
|
+
ctx.strokeStyle = borderColor;
|
|
877
|
+
ctx.lineWidth = 1;
|
|
878
|
+
ctx.strokeRect(x, y, legendWidth, legendHeight);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const effectiveTextColor = textColor ?? '#000000';
|
|
882
|
+
ctx.textAlign = 'left';
|
|
883
|
+
ctx.textBaseline = 'middle';
|
|
884
|
+
let currentY = y + paddingBox;
|
|
885
|
+
for (let index = 0; index < entries.length; index++) {
|
|
886
|
+
const entry = entries[index];
|
|
887
|
+
const entryHeight = entryHeights[index];
|
|
888
|
+
const centerY = currentY + entryHeight / 2;
|
|
889
|
+
// Draw color box (gradient or color)
|
|
890
|
+
ctx.beginPath();
|
|
891
|
+
ctx.rect(x + paddingBox, centerY - boxSize / 2, boxSize, boxSize);
|
|
892
|
+
fillWithGradientOrColor(ctx, entry.gradient, entry.color || '#4A90E2', '#4A90E2', { x: x + paddingBox, y: centerY - boxSize / 2, w: boxSize, h: boxSize });
|
|
893
|
+
ctx.fill();
|
|
894
|
+
// Draw label (with wrapping if enabled) using enhanced text
|
|
895
|
+
const textX = x + paddingBox + boxSize + textSpacing;
|
|
896
|
+
if (wrapTextEnabled && effectiveMaxWidth) {
|
|
897
|
+
const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
|
|
898
|
+
const lineHeight = fontSize * 1.2;
|
|
899
|
+
const startY = centerY - (wrappedLines.length - 1) * lineHeight / 2;
|
|
900
|
+
for (let lineIndex = 0; lineIndex < wrappedLines.length; lineIndex++) {
|
|
901
|
+
await renderEnhancedText(ctx, wrappedLines[lineIndex], textX, startY + lineIndex * lineHeight, textStyle, fontSize, effectiveTextColor, textGradient);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
await renderEnhancedText(ctx, entry.label, textX, centerY, textStyle, fontSize, effectiveTextColor, textGradient);
|
|
906
|
+
}
|
|
907
|
+
currentY += entryHeight + entrySpacing;
|
|
908
|
+
}
|
|
909
|
+
ctx.restore();
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Creates a line chart with multiple series support
|
|
913
|
+
*/
|
|
914
|
+
async function createLineChart(series, options = {}) {
|
|
915
|
+
// Extract options with defaults
|
|
916
|
+
const width = options.dimensions?.width ?? 800;
|
|
917
|
+
const height = options.dimensions?.height ?? 600;
|
|
918
|
+
const padding = options.dimensions?.padding || {};
|
|
919
|
+
const paddingTop = padding.top ?? 60;
|
|
920
|
+
const paddingRight = padding.right ?? 100;
|
|
921
|
+
const paddingBottom = padding.bottom ?? 80;
|
|
922
|
+
const paddingLeft = padding.left ?? 100;
|
|
923
|
+
const backgroundColor = options.appearance?.backgroundColor ?? '#FFFFFF';
|
|
924
|
+
const backgroundGradient = options.appearance?.backgroundGradient;
|
|
925
|
+
const backgroundImage = options.appearance?.backgroundImage;
|
|
926
|
+
const axisColor = options.appearance?.axisColor ?? options.axes?.x?.color ?? options.axes?.y?.color ?? '#000000';
|
|
927
|
+
const axisWidth = options.appearance?.axisWidth ?? options.axes?.x?.width ?? options.axes?.y?.width ?? 2;
|
|
928
|
+
const arrowSize = options.appearance?.arrowSize ?? 10;
|
|
929
|
+
const chartTitle = options.labels?.title?.text;
|
|
930
|
+
const chartTitleFontSize = options.labels?.title?.fontSize ?? 24;
|
|
931
|
+
const chartTitleColor = options.labels?.title?.color ?? '#000000';
|
|
932
|
+
const showPointLabels = options.labels?.pointLabelDefaults?.show ?? false;
|
|
933
|
+
const pointLabelFontSize = options.labels?.pointLabelDefaults?.fontSize ?? 12;
|
|
934
|
+
const pointLabelColor = options.labels?.pointLabelDefaults?.color ?? '#000000';
|
|
935
|
+
const pointLabelPosition = options.labels?.pointLabelDefaults?.position ?? 'top';
|
|
936
|
+
const showLegend = options.legend?.show ?? false;
|
|
937
|
+
const legendSpacing = options.legend?.spacing ?? 20;
|
|
938
|
+
const legendEntries = options.legend?.entries;
|
|
939
|
+
const legendPosition = options.legend?.position ?? 'right'; // Default: right
|
|
940
|
+
const showGrid = options.grid?.show ?? false;
|
|
941
|
+
const gridColor = options.grid?.color ?? '#E0E0E0';
|
|
942
|
+
const gridWidth = options.grid?.width ?? 1;
|
|
943
|
+
const xAxisConfig = options.axes?.x || {};
|
|
944
|
+
const yAxisConfig = options.axes?.y || {};
|
|
945
|
+
const xAxisLabel = xAxisConfig.label;
|
|
946
|
+
const yAxisLabel = yAxisConfig.label;
|
|
947
|
+
const xAxisLabelColor = xAxisConfig.labelColor ?? '#000000';
|
|
948
|
+
const yAxisLabelColor = yAxisConfig.labelColor ?? '#000000';
|
|
949
|
+
const xAxisRange = xAxisConfig.range;
|
|
950
|
+
const yAxisRange = yAxisConfig.range;
|
|
951
|
+
const xAxisCustomValues = xAxisConfig.values;
|
|
952
|
+
const yAxisCustomValues = yAxisConfig.values;
|
|
953
|
+
const xAxisValueSpacing = xAxisConfig.valueSpacing;
|
|
954
|
+
const yAxisValueSpacing = yAxisConfig.valueSpacing;
|
|
955
|
+
const tickFontSize = xAxisConfig.tickFontSize ?? yAxisConfig.tickFontSize ?? 12;
|
|
956
|
+
const baseline = yAxisConfig.baseline;
|
|
957
|
+
const xAxisScale = xAxisConfig.scale ?? 'linear';
|
|
958
|
+
const yAxisScale = yAxisConfig.scale ?? 'linear';
|
|
959
|
+
const xAxisDateFormat = xAxisConfig.dateFormat;
|
|
960
|
+
const yAxisDateFormat = yAxisConfig.dateFormat;
|
|
961
|
+
const xAxisDateTime = xAxisConfig.dateTime ?? false;
|
|
962
|
+
const yAxisDateTime = yAxisConfig.dateTime ?? false;
|
|
963
|
+
// Collect all X and Y values from all series
|
|
964
|
+
const allXValues = [];
|
|
965
|
+
const allYValues = [];
|
|
966
|
+
series.forEach(serie => {
|
|
967
|
+
serie.data.forEach(point => {
|
|
968
|
+
allXValues.push(point.x);
|
|
969
|
+
allYValues.push(point.y);
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
// Calculate X-axis range
|
|
973
|
+
let xMin, xMax, xStep;
|
|
974
|
+
if (xAxisCustomValues && xAxisCustomValues.length > 0) {
|
|
975
|
+
xMin = Math.min(...xAxisCustomValues);
|
|
976
|
+
xMax = Math.max(...xAxisCustomValues);
|
|
977
|
+
xStep = 1;
|
|
978
|
+
}
|
|
979
|
+
else if (xAxisRange && xAxisRange.min !== undefined && xAxisRange.max !== undefined) {
|
|
980
|
+
xMin = xAxisRange.min;
|
|
981
|
+
xMax = xAxisRange.max;
|
|
982
|
+
xStep = xAxisRange.step ?? Math.ceil((xMax - xMin) / 10);
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
if (allXValues.length > 0) {
|
|
986
|
+
xMin = Math.min(...allXValues);
|
|
987
|
+
xMax = Math.max(...allXValues);
|
|
988
|
+
const range = xMax - xMin;
|
|
989
|
+
const padding = range * 0.1;
|
|
990
|
+
xMin = xMin - padding;
|
|
991
|
+
xMax = xMax + padding;
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
xMin = 0;
|
|
995
|
+
xMax = 100;
|
|
996
|
+
}
|
|
997
|
+
xStep = Math.ceil((xMax - xMin) / 10);
|
|
998
|
+
}
|
|
999
|
+
// Calculate Y-axis range
|
|
1000
|
+
let yMin, yMax, yStep;
|
|
1001
|
+
const hasExplicitYRange = yAxisRange && yAxisRange.min !== undefined && yAxisRange.max !== undefined;
|
|
1002
|
+
if (yAxisCustomValues && yAxisCustomValues.length > 0) {
|
|
1003
|
+
yMin = Math.min(...yAxisCustomValues);
|
|
1004
|
+
yMax = Math.max(...yAxisCustomValues);
|
|
1005
|
+
yStep = 1;
|
|
1006
|
+
}
|
|
1007
|
+
else if (hasExplicitYRange) {
|
|
1008
|
+
yMin = yAxisRange.min;
|
|
1009
|
+
yMax = yAxisRange.max;
|
|
1010
|
+
yStep = yAxisRange.step ?? Math.ceil((yMax - yMin) / 10);
|
|
1011
|
+
// Ensure baseline is within range
|
|
1012
|
+
if (baseline !== undefined) {
|
|
1013
|
+
yMin = Math.min(yMin, baseline);
|
|
1014
|
+
yMax = Math.max(yMax, baseline);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
if (allYValues.length > 0) {
|
|
1019
|
+
yMin = Math.min(...allYValues);
|
|
1020
|
+
yMax = Math.max(...allYValues);
|
|
1021
|
+
const effectiveBaseline = baseline !== undefined ? baseline : 0;
|
|
1022
|
+
yMin = Math.min(yMin, effectiveBaseline);
|
|
1023
|
+
yMax = Math.max(yMax, effectiveBaseline);
|
|
1024
|
+
const range = yMax - yMin;
|
|
1025
|
+
const padding = range * 0.1;
|
|
1026
|
+
yMin = Math.max(yMin - padding, Math.min(effectiveBaseline, yMin));
|
|
1027
|
+
yMax = yMax + padding;
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
yMin = 0;
|
|
1031
|
+
yMax = 100;
|
|
1032
|
+
}
|
|
1033
|
+
yStep = Math.ceil((yMax - yMin) / 10);
|
|
1034
|
+
}
|
|
1035
|
+
// Validate data values against explicit axis ranges
|
|
1036
|
+
const hasExplicitXRange = xAxisRange && xAxisRange.min !== undefined && xAxisRange.max !== undefined;
|
|
1037
|
+
if (hasExplicitXRange || xAxisCustomValues) {
|
|
1038
|
+
const effectiveXMin = xAxisCustomValues ? Math.min(...xAxisCustomValues) : xAxisRange.min;
|
|
1039
|
+
const effectiveXMax = xAxisCustomValues ? Math.max(...xAxisCustomValues) : xAxisRange.max;
|
|
1040
|
+
series.forEach((serie, seriesIndex) => {
|
|
1041
|
+
serie.data.forEach((point, pointIndex) => {
|
|
1042
|
+
if (point.x < effectiveXMin || point.x > effectiveXMax) {
|
|
1043
|
+
throw new Error(`Line Chart Error: Data value out of X-axis bounds.\n` +
|
|
1044
|
+
`Series "${serie.label}" point ${pointIndex} has X value ${point.x}, ` +
|
|
1045
|
+
`which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
if (hasExplicitYRange || yAxisCustomValues) {
|
|
1051
|
+
const effectiveYMin = yAxisCustomValues ? Math.min(...yAxisCustomValues) : yMin;
|
|
1052
|
+
const effectiveYMax = yAxisCustomValues ? Math.max(...yAxisCustomValues) : yMax;
|
|
1053
|
+
series.forEach((serie, seriesIndex) => {
|
|
1054
|
+
serie.data.forEach((point, pointIndex) => {
|
|
1055
|
+
if (point.y < effectiveYMin || point.y > effectiveYMax) {
|
|
1056
|
+
throw new Error(`Line Chart Error: Data value out of Y-axis bounds.\n` +
|
|
1057
|
+
`Series "${serie.label}" point ${pointIndex} has Y value ${point.y}, ` +
|
|
1058
|
+
`which exceeds the Y-axis range [${effectiveYMin}, ${effectiveYMax}].`);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
// Calculate legend dimensions and adjust canvas size based on legend position
|
|
1064
|
+
let legendWidth = 0;
|
|
1065
|
+
let legendHeight = 0;
|
|
1066
|
+
let extraWidth = 0;
|
|
1067
|
+
let extraHeight = 0;
|
|
1068
|
+
const minLegendSpacing = 10;
|
|
1069
|
+
if (showLegend) {
|
|
1070
|
+
const entries = legendEntries || series.map(s => ({
|
|
1071
|
+
color: s.color || '#4A90E2',
|
|
1072
|
+
label: s.label
|
|
1073
|
+
}));
|
|
1074
|
+
const legendFontSize = options.legend?.fontSize ?? 16;
|
|
1075
|
+
const legendMaxWidth = options.legend?.maxWidth;
|
|
1076
|
+
const legendWrapText = options.legend?.wrapText !== false;
|
|
1077
|
+
const legendPadding = options.legend?.padding;
|
|
1078
|
+
const legendDims = calculateLegendDimensions(entries, legendSpacing, legendFontSize, legendMaxWidth, legendWrapText, legendPadding);
|
|
1079
|
+
legendWidth = legendDims.width;
|
|
1080
|
+
legendHeight = legendDims.height;
|
|
1081
|
+
// Adjust canvas dimensions based on legend position
|
|
1082
|
+
if (legendPosition === 'left' || legendPosition === 'right') {
|
|
1083
|
+
extraWidth = legendWidth + minLegendSpacing;
|
|
1084
|
+
}
|
|
1085
|
+
else if (legendPosition === 'top' || legendPosition === 'bottom') {
|
|
1086
|
+
extraHeight = legendHeight + minLegendSpacing;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const adjustedWidth = width + extraWidth;
|
|
1090
|
+
const adjustedHeight = height + extraHeight;
|
|
1091
|
+
// Create canvas
|
|
1092
|
+
const canvas = (0, canvas_1.createCanvas)(adjustedWidth, adjustedHeight);
|
|
1093
|
+
const ctx = canvas.getContext('2d');
|
|
1094
|
+
// Fill background (gradient, image, or color)
|
|
1095
|
+
if (backgroundImage) {
|
|
1096
|
+
try {
|
|
1097
|
+
const bgImage = await (0, canvas_1.loadImage)(backgroundImage);
|
|
1098
|
+
ctx.drawImage(bgImage, 0, 0, adjustedWidth, adjustedHeight);
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
console.warn(`Failed to load background image: ${backgroundImage}`, error);
|
|
1102
|
+
// Fallback to gradient or color if image fails to load
|
|
1103
|
+
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
|
|
1104
|
+
x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
|
|
1105
|
+
});
|
|
1106
|
+
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
|
|
1111
|
+
x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
|
|
1112
|
+
});
|
|
1113
|
+
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
|
|
1114
|
+
}
|
|
1115
|
+
// Calculate axis positions
|
|
1116
|
+
const titleHeight = chartTitle ? chartTitleFontSize + 30 : 0;
|
|
1117
|
+
const axisLabelHeight = (xAxisLabel || yAxisLabel) ? tickFontSize + 40 : 0;
|
|
1118
|
+
// Adjust chart area based on legend position
|
|
1119
|
+
let chartAreaLeft = paddingLeft;
|
|
1120
|
+
let chartAreaRight = width - paddingRight;
|
|
1121
|
+
let chartAreaTop = paddingTop + titleHeight;
|
|
1122
|
+
let chartAreaBottom = height - paddingBottom;
|
|
1123
|
+
if (showLegend) {
|
|
1124
|
+
if (legendPosition === 'left') {
|
|
1125
|
+
chartAreaLeft = paddingLeft + legendWidth + minLegendSpacing;
|
|
1126
|
+
chartAreaRight = width - paddingRight;
|
|
1127
|
+
}
|
|
1128
|
+
else if (legendPosition === 'right') {
|
|
1129
|
+
chartAreaLeft = paddingLeft;
|
|
1130
|
+
chartAreaRight = width - paddingRight;
|
|
1131
|
+
}
|
|
1132
|
+
else if (legendPosition === 'top') {
|
|
1133
|
+
chartAreaTop = paddingTop + titleHeight + legendHeight + minLegendSpacing;
|
|
1134
|
+
chartAreaBottom = height - paddingBottom;
|
|
1135
|
+
}
|
|
1136
|
+
else if (legendPosition === 'bottom') {
|
|
1137
|
+
chartAreaTop = paddingTop + titleHeight;
|
|
1138
|
+
chartAreaBottom = height - paddingBottom;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const originX = chartAreaLeft;
|
|
1142
|
+
const originY = chartAreaBottom - axisLabelHeight;
|
|
1143
|
+
const axisEndY = chartAreaTop;
|
|
1144
|
+
const axisEndX = chartAreaRight;
|
|
1145
|
+
// Draw chart title
|
|
1146
|
+
if (chartTitle) {
|
|
1147
|
+
ctx.save();
|
|
1148
|
+
ctx.textAlign = 'center';
|
|
1149
|
+
ctx.textBaseline = 'top';
|
|
1150
|
+
// Title positioned with proper spacing from top
|
|
1151
|
+
const titleY = paddingTop + 10;
|
|
1152
|
+
const titleX = adjustedWidth / 2;
|
|
1153
|
+
await renderEnhancedText(ctx, chartTitle, titleX, titleY, options.labels?.title?.textStyle, chartTitleFontSize, chartTitleColor, options.labels?.title?.gradient);
|
|
1154
|
+
ctx.restore();
|
|
1155
|
+
}
|
|
1156
|
+
// Set axis style
|
|
1157
|
+
ctx.strokeStyle = axisColor;
|
|
1158
|
+
ctx.fillStyle = axisColor;
|
|
1159
|
+
ctx.lineWidth = axisWidth;
|
|
1160
|
+
ctx.lineCap = 'round';
|
|
1161
|
+
// Calculate baseline Y position
|
|
1162
|
+
const chartAreaHeight = originY - axisEndY;
|
|
1163
|
+
const effectiveBaseline = baseline !== undefined ? baseline : 0;
|
|
1164
|
+
const baselineY = originY - ((effectiveBaseline - yMin) / (yMax - yMin)) * chartAreaHeight;
|
|
1165
|
+
// Draw Y-axis
|
|
1166
|
+
ctx.beginPath();
|
|
1167
|
+
ctx.moveTo(originX, originY);
|
|
1168
|
+
ctx.lineTo(originX, axisEndY);
|
|
1169
|
+
ctx.stroke();
|
|
1170
|
+
// Draw Y-axis arrow
|
|
1171
|
+
drawArrow(ctx, originX, axisEndY, -Math.PI / 2, arrowSize);
|
|
1172
|
+
// Draw X-axis at baseline
|
|
1173
|
+
ctx.beginPath();
|
|
1174
|
+
ctx.moveTo(originX, baselineY);
|
|
1175
|
+
ctx.lineTo(axisEndX, baselineY);
|
|
1176
|
+
ctx.stroke();
|
|
1177
|
+
// Draw X-axis arrow
|
|
1178
|
+
drawArrow(ctx, axisEndX, baselineY, 0, arrowSize);
|
|
1179
|
+
// Draw Y-axis ticks and labels
|
|
1180
|
+
drawYAxisTicks(ctx, originX, originY, axisEndY, yMin, yMax, yStep, tickFontSize, yAxisCustomValues, yAxisValueSpacing, yAxisScale, yAxisDateFormat, yAxisDateTime);
|
|
1181
|
+
// Draw X-axis ticks and labels
|
|
1182
|
+
drawXAxisTicks(ctx, originX, originY, axisEndX, xMin, xMax, xStep, tickFontSize, xAxisCustomValues, xAxisValueSpacing, xAxisScale, xAxisDateFormat, xAxisDateTime);
|
|
1183
|
+
// Draw axis labels
|
|
1184
|
+
if (xAxisLabel) {
|
|
1185
|
+
ctx.save();
|
|
1186
|
+
ctx.fillStyle = xAxisLabelColor;
|
|
1187
|
+
ctx.font = `${tickFontSize}px Arial`;
|
|
1188
|
+
ctx.textAlign = 'center';
|
|
1189
|
+
ctx.textBaseline = 'top';
|
|
1190
|
+
// Position label with more spacing from tick values (tick labels are at originY + 10, so add more gap)
|
|
1191
|
+
ctx.fillText(xAxisLabel, (originX + axisEndX) / 2, originY + 25);
|
|
1192
|
+
ctx.restore();
|
|
1193
|
+
}
|
|
1194
|
+
if (yAxisLabel) {
|
|
1195
|
+
ctx.save();
|
|
1196
|
+
ctx.fillStyle = yAxisLabelColor;
|
|
1197
|
+
ctx.font = `${tickFontSize}px Arial`;
|
|
1198
|
+
ctx.textAlign = 'center';
|
|
1199
|
+
ctx.textBaseline = 'bottom';
|
|
1200
|
+
ctx.save();
|
|
1201
|
+
ctx.translate(paddingLeft / 2, (originY + axisEndY) / 2);
|
|
1202
|
+
ctx.rotate(-Math.PI / 2);
|
|
1203
|
+
ctx.fillText(yAxisLabel, 0, 0);
|
|
1204
|
+
ctx.restore();
|
|
1205
|
+
ctx.restore();
|
|
1206
|
+
}
|
|
1207
|
+
// Draw grid lines if enabled
|
|
1208
|
+
if (showGrid) {
|
|
1209
|
+
drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep, gridColor, gridWidth, true, xAxisCustomValues);
|
|
1210
|
+
drawGrid(ctx, originX, originY, axisEndX, axisEndY, yMin, yMax, yStep, gridColor, gridWidth, false, yAxisCustomValues);
|
|
1211
|
+
}
|
|
1212
|
+
// Calculate chart area dimensions for point conversion
|
|
1213
|
+
const chartAreaWidth = axisEndX - originX;
|
|
1214
|
+
const chartAreaHeightForPoints = originY - axisEndY;
|
|
1215
|
+
// Draw all lines (first pass: draw areas, then lines, then markers, then error bars)
|
|
1216
|
+
series.forEach(serie => {
|
|
1217
|
+
const lineColor = serie.color || '#4A90E2';
|
|
1218
|
+
const lineWidth = serie.lineWidth ?? 2;
|
|
1219
|
+
const lineStyle = serie.lineStyle || 'solid';
|
|
1220
|
+
const smoothness = serie.smoothness || 'none';
|
|
1221
|
+
// When correlation is enabled, default to scatter plot mode (no connecting line, show markers)
|
|
1222
|
+
const hasCorrelation = serie.correlation && serie.correlation.type && serie.correlation.type !== 'none' && serie.correlation.show !== false;
|
|
1223
|
+
const showLine = serie.showLine !== false && (serie.showLine === true || !hasCorrelation);
|
|
1224
|
+
const markerType = serie.marker?.type ?? 'circle';
|
|
1225
|
+
const markerSize = serie.marker?.size ?? (hasCorrelation ? 8 : 6); // Larger markers for scatter plots
|
|
1226
|
+
const markerColor = serie.marker?.color || lineColor;
|
|
1227
|
+
const markerFilled = serie.marker?.filled !== false && markerType !== 'cross'; // Default filled, except for cross
|
|
1228
|
+
const showMarkers = serie.marker?.show !== false || hasCorrelation; // Always show markers when correlation is enabled
|
|
1229
|
+
const showErrorBars = serie.errorBar?.show ?? false;
|
|
1230
|
+
const errorBarColor = serie.errorBar?.color || lineColor;
|
|
1231
|
+
const errorBarWidth = serie.errorBar?.width ?? 1;
|
|
1232
|
+
const errorBarCapSize = serie.errorBar?.capSize ?? 5;
|
|
1233
|
+
const areaConfig = serie.area;
|
|
1234
|
+
// Convert data points to canvas coordinates
|
|
1235
|
+
const canvasPoints = serie.data.map(point => {
|
|
1236
|
+
// Handle X coordinate with optional log scale
|
|
1237
|
+
let x;
|
|
1238
|
+
if (xAxisScale === 'log' && xMin > 0 && xMax > 0) {
|
|
1239
|
+
const logPos = logScale(point.x, xMin, xMax);
|
|
1240
|
+
x = originX + logPos * chartAreaWidth;
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
x = originX + ((point.x - xMin) / (xMax - xMin)) * chartAreaWidth;
|
|
1244
|
+
}
|
|
1245
|
+
// Handle Y coordinate with optional log scale
|
|
1246
|
+
let y;
|
|
1247
|
+
if (yAxisScale === 'log' && yMin > 0 && yMax > 0) {
|
|
1248
|
+
const logPos = logScale(point.y, yMin, yMax);
|
|
1249
|
+
y = originY - logPos * chartAreaHeightForPoints;
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
y = originY - ((point.y - yMin) / (yMax - yMin)) * chartAreaHeightForPoints;
|
|
1253
|
+
}
|
|
1254
|
+
// Clamp coordinates to chart boundaries to prevent markers from exceeding axis limits
|
|
1255
|
+
x = Math.max(originX, Math.min(axisEndX, x));
|
|
1256
|
+
y = Math.max(axisEndY, Math.min(originY, y));
|
|
1257
|
+
return {
|
|
1258
|
+
x,
|
|
1259
|
+
y,
|
|
1260
|
+
originalPoint: point
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
1263
|
+
// Calculate area size if area is enabled (will be calculated during area drawing)
|
|
1264
|
+
let areaSize = null;
|
|
1265
|
+
let shadeToYCanvas = null; // Store shade-to Y canvas position for area size display
|
|
1266
|
+
// Draw area shading first (so it appears behind the line)
|
|
1267
|
+
if (areaConfig && areaConfig.type && areaConfig.type !== 'none' && areaConfig.show !== false) {
|
|
1268
|
+
ctx.save();
|
|
1269
|
+
// Clip to chart area to prevent drawing outside boundaries
|
|
1270
|
+
ctx.beginPath();
|
|
1271
|
+
ctx.rect(originX, axisEndY, axisEndX - originX, originY - axisEndY);
|
|
1272
|
+
ctx.clip();
|
|
1273
|
+
const areaColor = areaConfig.color || lineColor;
|
|
1274
|
+
const areaOpacity = areaConfig.opacity ?? 0.3;
|
|
1275
|
+
// Parse color and apply opacity
|
|
1276
|
+
let fillColor = areaColor;
|
|
1277
|
+
if (areaColor.startsWith('#')) {
|
|
1278
|
+
const r = parseInt(areaColor.slice(1, 3), 16);
|
|
1279
|
+
const g = parseInt(areaColor.slice(3, 5), 16);
|
|
1280
|
+
const b = parseInt(areaColor.slice(5, 7), 16);
|
|
1281
|
+
fillColor = `rgba(${r}, ${g}, ${b}, ${areaOpacity})`;
|
|
1282
|
+
}
|
|
1283
|
+
else if (areaColor.startsWith('rgba')) {
|
|
1284
|
+
fillColor = areaColor;
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
fillColor = `rgba(74, 144, 226, ${areaOpacity})`; // Default blue with opacity
|
|
1288
|
+
}
|
|
1289
|
+
ctx.fillStyle = fillColor;
|
|
1290
|
+
ctx.beginPath();
|
|
1291
|
+
if (areaConfig.type === 'below') {
|
|
1292
|
+
// Determine the Y value to shade to
|
|
1293
|
+
let shadeToYValue;
|
|
1294
|
+
let localShadeToYCanvas;
|
|
1295
|
+
if (areaConfig.toValue !== undefined) {
|
|
1296
|
+
// Validate custom Y value for 'below' type
|
|
1297
|
+
const allYValues = serie.data.map(p => p.y);
|
|
1298
|
+
const minY = Math.min(...allYValues);
|
|
1299
|
+
const maxY = Math.max(...allYValues);
|
|
1300
|
+
if (areaConfig.toValue >= minY) {
|
|
1301
|
+
throw new Error(`Line Chart Error: Invalid area shading configuration.\n` +
|
|
1302
|
+
`For area type "below", the toValue (${areaConfig.toValue}) must be below all Y values in the line.\n` +
|
|
1303
|
+
`Line Y range: [${minY}, ${maxY}].\n` +
|
|
1304
|
+
`The toValue cannot be above or equal to the minimum Y value (${minY}), and cannot be within the line's Y range.`);
|
|
1305
|
+
}
|
|
1306
|
+
shadeToYValue = areaConfig.toValue;
|
|
1307
|
+
// Convert to canvas coordinates
|
|
1308
|
+
if (yAxisScale === 'log' && yMin > 0 && yMax > 0) {
|
|
1309
|
+
const logPos = logScale(shadeToYValue, yMin, yMax);
|
|
1310
|
+
localShadeToYCanvas = originY - logPos * chartAreaHeightForPoints;
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
localShadeToYCanvas = originY - ((shadeToYValue - yMin) / (yMax - yMin)) * chartAreaHeightForPoints;
|
|
1314
|
+
}
|
|
1315
|
+
// Clamp to chart boundaries
|
|
1316
|
+
localShadeToYCanvas = Math.max(axisEndY, Math.min(originY, localShadeToYCanvas));
|
|
1317
|
+
}
|
|
1318
|
+
else {
|
|
1319
|
+
// Use baseline (default behavior)
|
|
1320
|
+
shadeToYValue = baseline !== undefined ? baseline : 0;
|
|
1321
|
+
localShadeToYCanvas = baselineY;
|
|
1322
|
+
}
|
|
1323
|
+
shadeToYCanvas = localShadeToYCanvas; // Store for area size display
|
|
1324
|
+
// Calculate area size
|
|
1325
|
+
let sum = 0;
|
|
1326
|
+
for (let i = 0; i < serie.data.length - 1; i++) {
|
|
1327
|
+
const x1 = serie.data[i].x;
|
|
1328
|
+
const y1 = serie.data[i].y;
|
|
1329
|
+
const x2 = serie.data[i + 1].x;
|
|
1330
|
+
const y2 = serie.data[i + 1].y;
|
|
1331
|
+
const avgY = (y1 + y2) / 2;
|
|
1332
|
+
const height = avgY - shadeToYValue;
|
|
1333
|
+
const width = x2 - x1;
|
|
1334
|
+
sum += height * width;
|
|
1335
|
+
}
|
|
1336
|
+
areaSize = Math.abs(sum);
|
|
1337
|
+
// Draw area only between first and last data points
|
|
1338
|
+
// Start at first point on shade-to line
|
|
1339
|
+
ctx.moveTo(canvasPoints[0].x, localShadeToYCanvas);
|
|
1340
|
+
// Draw along the line through all points
|
|
1341
|
+
for (let i = 0; i < canvasPoints.length; i++) {
|
|
1342
|
+
ctx.lineTo(canvasPoints[i].x, canvasPoints[i].y);
|
|
1343
|
+
}
|
|
1344
|
+
// Close back to shade-to line at the last point
|
|
1345
|
+
ctx.lineTo(canvasPoints[canvasPoints.length - 1].x, shadeToYCanvas);
|
|
1346
|
+
// Close path (will automatically close to start)
|
|
1347
|
+
ctx.closePath();
|
|
1348
|
+
ctx.fill();
|
|
1349
|
+
}
|
|
1350
|
+
else if (areaConfig.type === 'above') {
|
|
1351
|
+
// Determine the Y value to shade to
|
|
1352
|
+
let shadeToYValue;
|
|
1353
|
+
let localShadeToYCanvas;
|
|
1354
|
+
if (areaConfig.toValue !== undefined) {
|
|
1355
|
+
// Validate custom Y value for 'above' type
|
|
1356
|
+
const allYValues = serie.data.map(p => p.y);
|
|
1357
|
+
const minY = Math.min(...allYValues);
|
|
1358
|
+
const maxY = Math.max(...allYValues);
|
|
1359
|
+
if (areaConfig.toValue <= maxY) {
|
|
1360
|
+
throw new Error(`Line Chart Error: Invalid area shading configuration.\n` +
|
|
1361
|
+
`For area type "above", the toValue (${areaConfig.toValue}) must be above all Y values in the line.\n` +
|
|
1362
|
+
`Line Y range: [${minY}, ${maxY}].\n` +
|
|
1363
|
+
`The toValue cannot be below or equal to the maximum Y value (${maxY}), and cannot be within the line's Y range.`);
|
|
1364
|
+
}
|
|
1365
|
+
shadeToYValue = areaConfig.toValue;
|
|
1366
|
+
// Convert to canvas coordinates
|
|
1367
|
+
if (yAxisScale === 'log' && yMin > 0 && yMax > 0) {
|
|
1368
|
+
const logPos = logScale(shadeToYValue, yMin, yMax);
|
|
1369
|
+
localShadeToYCanvas = originY - logPos * chartAreaHeightForPoints;
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
localShadeToYCanvas = originY - ((shadeToYValue - yMin) / (yMax - yMin)) * chartAreaHeightForPoints;
|
|
1373
|
+
}
|
|
1374
|
+
// Clamp to chart boundaries
|
|
1375
|
+
localShadeToYCanvas = Math.max(axisEndY, Math.min(originY, localShadeToYCanvas));
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
// Use top of chart (default behavior for 'above')
|
|
1379
|
+
shadeToYValue = yMax; // Will be converted to canvas coordinates
|
|
1380
|
+
localShadeToYCanvas = axisEndY;
|
|
1381
|
+
}
|
|
1382
|
+
shadeToYCanvas = localShadeToYCanvas; // Store for area size display
|
|
1383
|
+
// Calculate area size
|
|
1384
|
+
let sum = 0;
|
|
1385
|
+
for (let i = 0; i < serie.data.length - 1; i++) {
|
|
1386
|
+
const x1 = serie.data[i].x;
|
|
1387
|
+
const y1 = serie.data[i].y;
|
|
1388
|
+
const x2 = serie.data[i + 1].x;
|
|
1389
|
+
const y2 = serie.data[i + 1].y;
|
|
1390
|
+
const avgY = (y1 + y2) / 2;
|
|
1391
|
+
const height = shadeToYValue - avgY; // Reversed for above
|
|
1392
|
+
const width = x2 - x1;
|
|
1393
|
+
sum += height * width;
|
|
1394
|
+
}
|
|
1395
|
+
areaSize = Math.abs(sum);
|
|
1396
|
+
// Draw area from first point to last point, closing at shade-to line
|
|
1397
|
+
ctx.moveTo(canvasPoints[0].x, localShadeToYCanvas);
|
|
1398
|
+
// Draw along the line
|
|
1399
|
+
canvasPoints.forEach(point => {
|
|
1400
|
+
ctx.lineTo(point.x, point.y);
|
|
1401
|
+
});
|
|
1402
|
+
// Close back to shade-to line at the last point
|
|
1403
|
+
ctx.lineTo(canvasPoints[canvasPoints.length - 1].x, localShadeToYCanvas);
|
|
1404
|
+
// Close back to start
|
|
1405
|
+
ctx.closePath();
|
|
1406
|
+
ctx.fill();
|
|
1407
|
+
}
|
|
1408
|
+
else if (areaConfig.type === 'between' && areaConfig.secondLine) {
|
|
1409
|
+
// Shade area between two lines
|
|
1410
|
+
const secondLineColor = areaConfig.secondLine.color || '#50C878';
|
|
1411
|
+
const secondLinePoints = areaConfig.secondLine.data.map(point => {
|
|
1412
|
+
// Handle X coordinate with optional log scale
|
|
1413
|
+
let x;
|
|
1414
|
+
if (xAxisScale === 'log' && xMin > 0 && xMax > 0) {
|
|
1415
|
+
const logPos = logScale(point.x, xMin, xMax);
|
|
1416
|
+
x = originX + logPos * chartAreaWidth;
|
|
1417
|
+
}
|
|
1418
|
+
else {
|
|
1419
|
+
x = originX + ((point.x - xMin) / (xMax - xMin)) * chartAreaWidth;
|
|
1420
|
+
}
|
|
1421
|
+
// Handle Y coordinate with optional log scale
|
|
1422
|
+
let y;
|
|
1423
|
+
if (yAxisScale === 'log' && yMin > 0 && yMax > 0) {
|
|
1424
|
+
const logPos = logScale(point.y, yMin, yMax);
|
|
1425
|
+
y = originY - logPos * chartAreaHeightForPoints;
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
y = originY - ((point.y - yMin) / (yMax - yMin)) * chartAreaHeightForPoints;
|
|
1429
|
+
}
|
|
1430
|
+
// Clamp coordinates to chart boundaries
|
|
1431
|
+
x = Math.max(originX, Math.min(axisEndX, x));
|
|
1432
|
+
y = Math.max(axisEndY, Math.min(originY, y));
|
|
1433
|
+
return {
|
|
1434
|
+
x,
|
|
1435
|
+
y,
|
|
1436
|
+
originalPoint: point
|
|
1437
|
+
};
|
|
1438
|
+
});
|
|
1439
|
+
// Calculate area between two lines
|
|
1440
|
+
let sum = 0;
|
|
1441
|
+
const minLength = Math.min(serie.data.length, areaConfig.secondLine.data.length);
|
|
1442
|
+
for (let i = 0; i < minLength - 1; i++) {
|
|
1443
|
+
const x1 = serie.data[i].x;
|
|
1444
|
+
const y1 = serie.data[i].y;
|
|
1445
|
+
const x2 = serie.data[i + 1].x;
|
|
1446
|
+
const y2 = serie.data[i + 1].y;
|
|
1447
|
+
const y1Second = areaConfig.secondLine.data[i].y;
|
|
1448
|
+
const y2Second = areaConfig.secondLine.data[i + 1].y;
|
|
1449
|
+
const avgHeight = Math.abs(((y1 + y2) / 2) - ((y1Second + y2Second) / 2));
|
|
1450
|
+
const width = x2 - x1;
|
|
1451
|
+
sum += avgHeight * width;
|
|
1452
|
+
}
|
|
1453
|
+
areaSize = sum;
|
|
1454
|
+
// Draw from first line to second line
|
|
1455
|
+
ctx.moveTo(canvasPoints[0].x, canvasPoints[0].y);
|
|
1456
|
+
canvasPoints.forEach(point => {
|
|
1457
|
+
ctx.lineTo(point.x, point.y);
|
|
1458
|
+
});
|
|
1459
|
+
// Reverse through second line
|
|
1460
|
+
for (let i = secondLinePoints.length - 1; i >= 0; i--) {
|
|
1461
|
+
ctx.lineTo(secondLinePoints[i].x, secondLinePoints[i].y);
|
|
1462
|
+
}
|
|
1463
|
+
ctx.closePath();
|
|
1464
|
+
ctx.fill();
|
|
1465
|
+
// Draw second line separately
|
|
1466
|
+
ctx.save();
|
|
1467
|
+
ctx.strokeStyle = secondLineColor;
|
|
1468
|
+
ctx.lineWidth = areaConfig.secondLine.lineWidth ?? 2;
|
|
1469
|
+
applyLineStyle(ctx, areaConfig.secondLine.lineStyle || 'solid');
|
|
1470
|
+
ctx.beginPath();
|
|
1471
|
+
ctx.moveTo(secondLinePoints[0].x, secondLinePoints[0].y);
|
|
1472
|
+
for (let i = 1; i < secondLinePoints.length; i++) {
|
|
1473
|
+
ctx.lineTo(secondLinePoints[i].x, secondLinePoints[i].y);
|
|
1474
|
+
}
|
|
1475
|
+
ctx.stroke();
|
|
1476
|
+
ctx.restore();
|
|
1477
|
+
// Draw markers for second line if enabled
|
|
1478
|
+
if (areaConfig.secondLine.marker?.show !== false) {
|
|
1479
|
+
const secondMarkerType = areaConfig.secondLine.marker?.type ?? 'circle';
|
|
1480
|
+
const secondMarkerSize = areaConfig.secondLine.marker?.size ?? 6;
|
|
1481
|
+
const secondMarkerColor = areaConfig.secondLine.marker?.color || secondLineColor;
|
|
1482
|
+
const secondMarkerFilled = areaConfig.secondLine.marker?.filled !== false && secondMarkerType !== 'cross';
|
|
1483
|
+
secondLinePoints.forEach(canvasPoint => {
|
|
1484
|
+
if (secondMarkerType !== 'none') {
|
|
1485
|
+
drawMarker(ctx, canvasPoint.x, canvasPoint.y, secondMarkerType, secondMarkerSize, secondMarkerColor, secondMarkerFilled);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
else if (areaConfig.type === 'around') {
|
|
1491
|
+
// Shade area around the line (confidence interval)
|
|
1492
|
+
const upperBound = areaConfig.upperBound || [];
|
|
1493
|
+
const lowerBound = areaConfig.lowerBound || [];
|
|
1494
|
+
if (upperBound.length === canvasPoints.length && lowerBound.length === canvasPoints.length) {
|
|
1495
|
+
const upperPoints = upperBound.map((value, index) => ({
|
|
1496
|
+
x: canvasPoints[index].x,
|
|
1497
|
+
y: originY - ((value - yMin) / (yMax - yMin)) * chartAreaHeightForPoints
|
|
1498
|
+
}));
|
|
1499
|
+
const lowerPoints = lowerBound.map((value, index) => ({
|
|
1500
|
+
x: canvasPoints[index].x,
|
|
1501
|
+
y: originY - ((value - yMin) / (yMax - yMin)) * chartAreaHeightForPoints
|
|
1502
|
+
}));
|
|
1503
|
+
// Draw upper bound
|
|
1504
|
+
upperPoints.forEach(point => {
|
|
1505
|
+
ctx.lineTo(point.x, point.y);
|
|
1506
|
+
});
|
|
1507
|
+
// Draw lower bound in reverse
|
|
1508
|
+
for (let i = lowerPoints.length - 1; i >= 0; i--) {
|
|
1509
|
+
ctx.lineTo(lowerPoints[i].x, lowerPoints[i].y);
|
|
1510
|
+
}
|
|
1511
|
+
ctx.closePath();
|
|
1512
|
+
ctx.fill();
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
ctx.restore();
|
|
1516
|
+
}
|
|
1517
|
+
// Draw line (if enabled) - use the calculated showLine value
|
|
1518
|
+
if (showLine) {
|
|
1519
|
+
ctx.save();
|
|
1520
|
+
ctx.strokeStyle = lineColor;
|
|
1521
|
+
ctx.lineWidth = lineWidth;
|
|
1522
|
+
// Handle step lines separately (they don't use line dash)
|
|
1523
|
+
const isStepLine = lineStyle === 'step' || lineStyle === 'stepline';
|
|
1524
|
+
if (!isStepLine) {
|
|
1525
|
+
applyLineStyle(ctx, lineStyle);
|
|
1526
|
+
}
|
|
1527
|
+
if (smoothness === 'bezier' && canvasPoints.length > 1) {
|
|
1528
|
+
// Draw smooth Bezier curve
|
|
1529
|
+
const controlPoints = calculateBezierControlPoints(canvasPoints.map(p => ({ x: p.x, y: p.y })));
|
|
1530
|
+
ctx.beginPath();
|
|
1531
|
+
ctx.moveTo(canvasPoints[0].x, canvasPoints[0].y);
|
|
1532
|
+
for (let i = 0; i < canvasPoints.length - 1; i++) {
|
|
1533
|
+
const cp = controlPoints[i];
|
|
1534
|
+
ctx.bezierCurveTo(cp.cp1x, cp.cp1y, cp.cp2x, cp.cp2y, canvasPoints[i + 1].x, canvasPoints[i + 1].y);
|
|
1535
|
+
}
|
|
1536
|
+
ctx.stroke();
|
|
1537
|
+
}
|
|
1538
|
+
else if (smoothness === 'spline' && canvasPoints.length > 1) {
|
|
1539
|
+
// Draw cubic spline interpolation
|
|
1540
|
+
const splinePoints = calculateSplinePoints(canvasPoints.map(p => ({ x: p.x, y: p.y })));
|
|
1541
|
+
ctx.beginPath();
|
|
1542
|
+
ctx.moveTo(splinePoints[0].x, splinePoints[0].y);
|
|
1543
|
+
for (let i = 1; i < splinePoints.length; i++) {
|
|
1544
|
+
ctx.lineTo(splinePoints[i].x, splinePoints[i].y);
|
|
1545
|
+
}
|
|
1546
|
+
ctx.stroke();
|
|
1547
|
+
}
|
|
1548
|
+
else if (isStepLine && canvasPoints.length > 1) {
|
|
1549
|
+
// Draw step line (horizontal then vertical)
|
|
1550
|
+
ctx.beginPath();
|
|
1551
|
+
ctx.moveTo(canvasPoints[0].x, canvasPoints[0].y);
|
|
1552
|
+
for (let i = 0; i < canvasPoints.length - 1; i++) {
|
|
1553
|
+
// Horizontal line to next x position
|
|
1554
|
+
ctx.lineTo(canvasPoints[i + 1].x, canvasPoints[i].y);
|
|
1555
|
+
// Vertical line to next y position
|
|
1556
|
+
ctx.lineTo(canvasPoints[i + 1].x, canvasPoints[i + 1].y);
|
|
1557
|
+
}
|
|
1558
|
+
ctx.stroke();
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
// Draw straight lines
|
|
1562
|
+
ctx.beginPath();
|
|
1563
|
+
ctx.moveTo(canvasPoints[0].x, canvasPoints[0].y);
|
|
1564
|
+
for (let i = 1; i < canvasPoints.length; i++) {
|
|
1565
|
+
ctx.lineTo(canvasPoints[i].x, canvasPoints[i].y);
|
|
1566
|
+
}
|
|
1567
|
+
ctx.stroke();
|
|
1568
|
+
}
|
|
1569
|
+
ctx.restore();
|
|
1570
|
+
}
|
|
1571
|
+
// Draw correlation/regression line if enabled
|
|
1572
|
+
// Draw correlation line BEFORE markers so markers appear on top (like in scatter plots)
|
|
1573
|
+
if (serie.correlation && serie.correlation.type && serie.correlation.type !== 'none') {
|
|
1574
|
+
const correlationType = serie.correlation.type;
|
|
1575
|
+
const correlationColor = serie.correlation.color || lineColor;
|
|
1576
|
+
const correlationLineWidth = serie.correlation.lineWidth ?? 2;
|
|
1577
|
+
const correlationLineStyle = serie.correlation.lineStyle || 'dashed';
|
|
1578
|
+
const correlationDegree = serie.correlation.degree ?? 2;
|
|
1579
|
+
const showCorrelation = serie.correlation.show !== false;
|
|
1580
|
+
if (showCorrelation && serie.data.length >= 2) {
|
|
1581
|
+
// Generate correlation line points - ensure we use actual data range, not just axis range
|
|
1582
|
+
// For better correlation visualization, use a slightly extended range
|
|
1583
|
+
const dataXValues = serie.data.map(p => p.x);
|
|
1584
|
+
const dataXMin = Math.min(...dataXValues);
|
|
1585
|
+
const dataXMax = Math.max(...dataXValues);
|
|
1586
|
+
const xRangeForCorrelation = dataXMax - dataXMin;
|
|
1587
|
+
const correlationXMin = Math.max(xMin, dataXMin - xRangeForCorrelation * 0.1);
|
|
1588
|
+
const correlationXMax = Math.min(xMax, dataXMax + xRangeForCorrelation * 0.1);
|
|
1589
|
+
const correlationPoints = generateCorrelationPoints(serie.data.map(p => ({ x: p.x, y: p.y })), correlationType, correlationXMin, correlationXMax, correlationDegree);
|
|
1590
|
+
if (correlationPoints.length > 0) {
|
|
1591
|
+
// Convert correlation points to canvas coordinates, clamping to chart area
|
|
1592
|
+
const canvasCorrelationPoints = correlationPoints
|
|
1593
|
+
.map(point => {
|
|
1594
|
+
// Handle X coordinate with optional log scale
|
|
1595
|
+
let x;
|
|
1596
|
+
if (xAxisScale === 'log' && xMin > 0 && xMax > 0) {
|
|
1597
|
+
const logPos = logScale(point.x, xMin, xMax);
|
|
1598
|
+
x = originX + logPos * chartAreaWidth;
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
x = originX + ((point.x - xMin) / (xMax - xMin)) * chartAreaWidth;
|
|
1602
|
+
}
|
|
1603
|
+
// Handle Y coordinate with optional log scale
|
|
1604
|
+
let y;
|
|
1605
|
+
if (yAxisScale === 'log' && yMin > 0 && yMax > 0) {
|
|
1606
|
+
const logPos = logScale(point.y, yMin, yMax);
|
|
1607
|
+
y = originY - logPos * chartAreaHeightForPoints;
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
y = originY - ((point.y - yMin) / (yMax - yMin)) * chartAreaHeightForPoints;
|
|
1611
|
+
}
|
|
1612
|
+
return { x, y };
|
|
1613
|
+
})
|
|
1614
|
+
.filter(point => point.x >= originX && point.x <= axisEndX &&
|
|
1615
|
+
point.y >= axisEndY && point.y <= originY);
|
|
1616
|
+
if (canvasCorrelationPoints.length > 0) {
|
|
1617
|
+
// Draw correlation line
|
|
1618
|
+
ctx.save();
|
|
1619
|
+
ctx.strokeStyle = correlationColor;
|
|
1620
|
+
ctx.lineWidth = correlationLineWidth;
|
|
1621
|
+
applyLineStyle(ctx, correlationLineStyle);
|
|
1622
|
+
ctx.beginPath();
|
|
1623
|
+
ctx.moveTo(canvasCorrelationPoints[0].x, canvasCorrelationPoints[0].y);
|
|
1624
|
+
for (let i = 1; i < canvasCorrelationPoints.length; i++) {
|
|
1625
|
+
ctx.lineTo(canvasCorrelationPoints[i].x, canvasCorrelationPoints[i].y);
|
|
1626
|
+
}
|
|
1627
|
+
ctx.stroke();
|
|
1628
|
+
ctx.restore();
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
// Draw markers
|
|
1634
|
+
if (showMarkers) {
|
|
1635
|
+
canvasPoints.forEach(canvasPoint => {
|
|
1636
|
+
const point = canvasPoint.originalPoint;
|
|
1637
|
+
const shouldShowMarker = point.showMarker !== false;
|
|
1638
|
+
const markerColorForPoint = point.markerColor || markerColor;
|
|
1639
|
+
if (shouldShowMarker && markerType !== 'none') {
|
|
1640
|
+
drawMarker(ctx, canvasPoint.x, canvasPoint.y, markerType, markerSize, markerColorForPoint, markerFilled);
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
// Draw error bars
|
|
1645
|
+
if (showErrorBars) {
|
|
1646
|
+
canvasPoints.forEach(canvasPoint => {
|
|
1647
|
+
const point = canvasPoint.originalPoint;
|
|
1648
|
+
const errorBar = point.errorBar;
|
|
1649
|
+
if (errorBar && errorBar.show !== false) {
|
|
1650
|
+
const positive = errorBar.positive ?? 0;
|
|
1651
|
+
const negative = errorBar.negative ?? 0;
|
|
1652
|
+
const errorColor = errorBar.color || errorBarColor;
|
|
1653
|
+
const errorWidth = errorBar.width ?? errorBarWidth;
|
|
1654
|
+
const errorCapSize = errorBar.capSize ?? errorBarCapSize;
|
|
1655
|
+
if (positive > 0 || negative > 0) {
|
|
1656
|
+
drawErrorBar(ctx, canvasPoint.x, canvasPoint.y, positive, negative, errorColor, errorWidth, errorCapSize, chartAreaHeightForPoints, yMin, yMax);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
// Draw point labels
|
|
1662
|
+
if (showPointLabels) {
|
|
1663
|
+
ctx.save();
|
|
1664
|
+
ctx.fillStyle = pointLabelColor;
|
|
1665
|
+
ctx.font = `${pointLabelFontSize}px Arial`;
|
|
1666
|
+
ctx.textAlign = 'center';
|
|
1667
|
+
ctx.textBaseline = 'middle';
|
|
1668
|
+
canvasPoints.forEach(canvasPoint => {
|
|
1669
|
+
const point = canvasPoint.originalPoint;
|
|
1670
|
+
if (point.label) {
|
|
1671
|
+
let labelX = canvasPoint.x;
|
|
1672
|
+
let labelY = canvasPoint.y;
|
|
1673
|
+
switch (pointLabelPosition) {
|
|
1674
|
+
case 'top':
|
|
1675
|
+
labelY = canvasPoint.y - markerSize / 2 - 5;
|
|
1676
|
+
ctx.textBaseline = 'bottom';
|
|
1677
|
+
break;
|
|
1678
|
+
case 'bottom':
|
|
1679
|
+
labelY = canvasPoint.y + markerSize / 2 + 5;
|
|
1680
|
+
ctx.textBaseline = 'top';
|
|
1681
|
+
break;
|
|
1682
|
+
case 'left':
|
|
1683
|
+
labelX = canvasPoint.x - markerSize / 2 - 5;
|
|
1684
|
+
ctx.textAlign = 'right';
|
|
1685
|
+
break;
|
|
1686
|
+
case 'right':
|
|
1687
|
+
labelX = canvasPoint.x + markerSize / 2 + 5;
|
|
1688
|
+
ctx.textAlign = 'left';
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
ctx.fillText(point.label, labelX, labelY);
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
ctx.restore();
|
|
1695
|
+
}
|
|
1696
|
+
// Draw area size label if enabled
|
|
1697
|
+
if (areaSize !== null && areaConfig && areaConfig.showAreaSize === true) {
|
|
1698
|
+
ctx.save();
|
|
1699
|
+
ctx.fillStyle = areaConfig.areaSizeColor || '#000000';
|
|
1700
|
+
ctx.font = `${pointLabelFontSize}px Arial`;
|
|
1701
|
+
ctx.textAlign = 'center';
|
|
1702
|
+
ctx.textBaseline = 'middle';
|
|
1703
|
+
// Position label in the center of the area
|
|
1704
|
+
const centerX = (canvasPoints[0].x + canvasPoints[canvasPoints.length - 1].x) / 2;
|
|
1705
|
+
let centerY = 0;
|
|
1706
|
+
if (areaConfig.type === 'below') {
|
|
1707
|
+
const shadeY = shadeToYCanvas !== null ? shadeToYCanvas : baselineY;
|
|
1708
|
+
centerY = (shadeY + canvasPoints[Math.floor(canvasPoints.length / 2)].y) / 2;
|
|
1709
|
+
}
|
|
1710
|
+
else if (areaConfig.type === 'above') {
|
|
1711
|
+
const shadeY = shadeToYCanvas !== null ? shadeToYCanvas : axisEndY;
|
|
1712
|
+
centerY = (shadeY + canvasPoints[Math.floor(canvasPoints.length / 2)].y) / 2;
|
|
1713
|
+
}
|
|
1714
|
+
else if (areaConfig.type === 'between') {
|
|
1715
|
+
centerY = canvasPoints[Math.floor(canvasPoints.length / 2)].y;
|
|
1716
|
+
}
|
|
1717
|
+
ctx.fillText(`Area: ${areaSize.toFixed(2)}`, centerX, centerY);
|
|
1718
|
+
ctx.restore();
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
// Draw legend - positioned based on legendPosition option
|
|
1722
|
+
if (showLegend) {
|
|
1723
|
+
const entries = legendEntries || series.map(s => ({
|
|
1724
|
+
color: s.color || '#4A90E2',
|
|
1725
|
+
label: s.label
|
|
1726
|
+
}));
|
|
1727
|
+
const legendFontSize = options.legend?.fontSize ?? 16;
|
|
1728
|
+
const legendBgColor = options.legend?.backgroundColor;
|
|
1729
|
+
const legendBorderColor = options.legend?.borderColor;
|
|
1730
|
+
const legendTextColor = options.legend?.textColor;
|
|
1731
|
+
const legendPadding = options.legend?.padding;
|
|
1732
|
+
const legendMaxWidth = options.legend?.maxWidth;
|
|
1733
|
+
const legendWrapText = options.legend?.wrapText !== false;
|
|
1734
|
+
// Calculate legend position based on legendPosition option
|
|
1735
|
+
let legendX, legendY;
|
|
1736
|
+
const chartAreaHeight = originY - axisEndY;
|
|
1737
|
+
const chartAreaWidth = axisEndX - originX;
|
|
1738
|
+
switch (legendPosition) {
|
|
1739
|
+
case 'top':
|
|
1740
|
+
legendX = (adjustedWidth - legendWidth) / 2; // Centered horizontally
|
|
1741
|
+
legendY = paddingTop + titleHeight + minLegendSpacing;
|
|
1742
|
+
break;
|
|
1743
|
+
case 'bottom':
|
|
1744
|
+
legendX = (adjustedWidth - legendWidth) / 2; // Centered horizontally
|
|
1745
|
+
legendY = adjustedHeight - paddingBottom - legendHeight - minLegendSpacing;
|
|
1746
|
+
break;
|
|
1747
|
+
case 'left':
|
|
1748
|
+
legendX = paddingLeft + minLegendSpacing;
|
|
1749
|
+
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2; // Vertically centered in chart area
|
|
1750
|
+
break;
|
|
1751
|
+
case 'right':
|
|
1752
|
+
default:
|
|
1753
|
+
legendX = axisEndX + minLegendSpacing;
|
|
1754
|
+
legendY = axisEndY + (chartAreaHeight - legendHeight) / 2; // Vertically centered in chart area
|
|
1755
|
+
break;
|
|
1756
|
+
}
|
|
1757
|
+
await drawLegend(ctx, legendX, legendY, entries, legendSpacing, legendFontSize, legendBgColor, legendBorderColor, legendTextColor, legendPadding, legendMaxWidth, legendWrapText, options.legend?.backgroundGradient, options.legend?.textGradient, options.legend?.textStyle);
|
|
1758
|
+
}
|
|
1759
|
+
return canvas.toBuffer('image/png');
|
|
1760
|
+
}
|
|
1761
|
+
//# sourceMappingURL=linechart.js.map
|