apexify.js 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/CHANGELOG.md +263 -38
  2. package/README.md +248 -1109
  3. package/dist/cjs/Canvas/ApexPainter.d.ts +182 -204
  4. package/dist/cjs/Canvas/ApexPainter.d.ts.map +1 -1
  5. package/dist/cjs/Canvas/ApexPainter.js +482 -1286
  6. package/dist/cjs/Canvas/ApexPainter.js.map +1 -1
  7. package/dist/cjs/Canvas/extended/CanvasCreator.d.ts +33 -0
  8. package/dist/cjs/Canvas/extended/CanvasCreator.d.ts.map +1 -0
  9. package/dist/cjs/Canvas/extended/CanvasCreator.js +223 -0
  10. package/dist/cjs/Canvas/extended/CanvasCreator.js.map +1 -0
  11. package/dist/cjs/Canvas/extended/ChartCreator.d.ts +26 -0
  12. package/dist/cjs/Canvas/extended/ChartCreator.d.ts.map +1 -0
  13. package/dist/cjs/Canvas/extended/ChartCreator.js +50 -0
  14. package/dist/cjs/Canvas/extended/ChartCreator.js.map +1 -0
  15. package/dist/cjs/Canvas/extended/GIFCreator.d.ts +43 -0
  16. package/dist/cjs/Canvas/extended/GIFCreator.d.ts.map +1 -0
  17. package/dist/cjs/Canvas/extended/GIFCreator.js +157 -0
  18. package/dist/cjs/Canvas/extended/GIFCreator.js.map +1 -0
  19. package/dist/cjs/Canvas/extended/ImageCreator.d.ts +83 -0
  20. package/dist/cjs/Canvas/extended/ImageCreator.d.ts.map +1 -0
  21. package/dist/cjs/Canvas/extended/ImageCreator.js +479 -0
  22. package/dist/cjs/Canvas/extended/ImageCreator.js.map +1 -0
  23. package/dist/cjs/Canvas/extended/TextCreator.d.ts +35 -0
  24. package/dist/cjs/Canvas/extended/TextCreator.d.ts.map +1 -0
  25. package/dist/cjs/Canvas/extended/TextCreator.js +98 -0
  26. package/dist/cjs/Canvas/extended/TextCreator.js.map +1 -0
  27. package/dist/cjs/Canvas/extended/VideoCreator.d.ts +370 -0
  28. package/dist/cjs/Canvas/extended/VideoCreator.d.ts.map +1 -0
  29. package/dist/cjs/Canvas/extended/VideoCreator.js +478 -0
  30. package/dist/cjs/Canvas/extended/VideoCreator.js.map +1 -0
  31. package/dist/cjs/Canvas/utils/Background/bg.d.ts +1 -1
  32. package/dist/cjs/Canvas/utils/Background/bg.d.ts.map +1 -1
  33. package/dist/cjs/Canvas/utils/Background/bg.js +43 -7
  34. package/dist/cjs/Canvas/utils/Background/bg.js.map +1 -1
  35. package/dist/cjs/Canvas/utils/Charts/barchart.d.ts +230 -0
  36. package/dist/cjs/Canvas/utils/Charts/barchart.d.ts.map +1 -0
  37. package/dist/cjs/Canvas/utils/Charts/barchart.js +1891 -0
  38. package/dist/cjs/Canvas/utils/Charts/barchart.js.map +1 -0
  39. package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
  40. package/dist/cjs/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
  41. package/dist/cjs/Canvas/utils/Charts/comparisonchart.js +368 -0
  42. package/dist/cjs/Canvas/utils/Charts/comparisonchart.js.map +1 -0
  43. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
  44. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
  45. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
  46. package/dist/cjs/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
  47. package/dist/cjs/Canvas/utils/Charts/index.d.ts +45 -0
  48. package/dist/cjs/Canvas/utils/Charts/index.d.ts.map +1 -0
  49. package/dist/cjs/Canvas/utils/Charts/index.js +17 -0
  50. package/dist/cjs/Canvas/utils/Charts/index.js.map +1 -0
  51. package/dist/cjs/Canvas/utils/Charts/linechart.d.ts +216 -0
  52. package/dist/cjs/Canvas/utils/Charts/linechart.d.ts.map +1 -0
  53. package/dist/cjs/Canvas/utils/Charts/linechart.js +1761 -0
  54. package/dist/cjs/Canvas/utils/Charts/linechart.js.map +1 -0
  55. package/dist/cjs/Canvas/utils/Charts/piechart.d.ts +167 -0
  56. package/dist/cjs/Canvas/utils/Charts/piechart.d.ts.map +1 -0
  57. package/dist/cjs/Canvas/utils/Charts/piechart.js +794 -0
  58. package/dist/cjs/Canvas/utils/Charts/piechart.js.map +1 -0
  59. package/dist/cjs/Canvas/utils/General/batchOperations.d.ts.map +1 -1
  60. package/dist/cjs/Canvas/utils/General/batchOperations.js +3 -4
  61. package/dist/cjs/Canvas/utils/General/batchOperations.js.map +1 -1
  62. package/dist/cjs/Canvas/utils/General/general functions.d.ts.map +1 -1
  63. package/dist/cjs/Canvas/utils/General/general functions.js +62 -33
  64. package/dist/cjs/Canvas/utils/General/general functions.js.map +1 -1
  65. package/dist/cjs/Canvas/utils/General/imageStitching.d.ts.map +1 -1
  66. package/dist/cjs/Canvas/utils/General/imageStitching.js +3 -6
  67. package/dist/cjs/Canvas/utils/General/imageStitching.js.map +1 -1
  68. package/dist/cjs/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
  69. package/dist/cjs/Canvas/utils/Image/imageMasking.js +5 -12
  70. package/dist/cjs/Canvas/utils/Image/imageMasking.js.map +1 -1
  71. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts +4 -4
  72. package/dist/cjs/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  73. package/dist/cjs/Canvas/utils/Image/imageProperties.js +44 -9
  74. package/dist/cjs/Canvas/utils/Image/imageProperties.js.map +1 -1
  75. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
  76. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  77. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
  78. package/dist/cjs/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  79. package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts +1 -1
  80. package/dist/cjs/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
  81. package/dist/cjs/Canvas/utils/Texts/textProperties.js +48 -5
  82. package/dist/cjs/Canvas/utils/Texts/textProperties.js.map +1 -1
  83. package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts +489 -0
  84. package/dist/cjs/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
  85. package/dist/cjs/Canvas/utils/Video/videoHelpers.js +1835 -0
  86. package/dist/cjs/Canvas/utils/Video/videoHelpers.js.map +1 -0
  87. package/dist/cjs/Canvas/utils/errorUtils.d.ts +15 -0
  88. package/dist/cjs/Canvas/utils/errorUtils.d.ts.map +1 -0
  89. package/dist/cjs/Canvas/utils/errorUtils.js +26 -0
  90. package/dist/cjs/Canvas/utils/errorUtils.js.map +1 -0
  91. package/dist/cjs/Canvas/utils/types.d.ts +17 -178
  92. package/dist/cjs/Canvas/utils/types.d.ts.map +1 -1
  93. package/dist/cjs/Canvas/utils/types.js.map +1 -1
  94. package/dist/cjs/Canvas/utils/utils.d.ts +4 -3
  95. package/dist/cjs/Canvas/utils/utils.d.ts.map +1 -1
  96. package/dist/cjs/Canvas/utils/utils.js +40 -6
  97. package/dist/cjs/Canvas/utils/utils.js.map +1 -1
  98. package/dist/cjs/index.d.ts +1 -8
  99. package/dist/cjs/index.d.ts.map +1 -1
  100. package/dist/cjs/index.js +14 -45
  101. package/dist/cjs/index.js.map +1 -1
  102. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  103. package/dist/esm/Canvas/ApexPainter.d.ts +182 -204
  104. package/dist/esm/Canvas/ApexPainter.d.ts.map +1 -1
  105. package/dist/esm/Canvas/ApexPainter.js +482 -1286
  106. package/dist/esm/Canvas/ApexPainter.js.map +1 -1
  107. package/dist/esm/Canvas/extended/CanvasCreator.d.ts +33 -0
  108. package/dist/esm/Canvas/extended/CanvasCreator.d.ts.map +1 -0
  109. package/dist/esm/Canvas/extended/CanvasCreator.js +223 -0
  110. package/dist/esm/Canvas/extended/CanvasCreator.js.map +1 -0
  111. package/dist/esm/Canvas/extended/ChartCreator.d.ts +26 -0
  112. package/dist/esm/Canvas/extended/ChartCreator.d.ts.map +1 -0
  113. package/dist/esm/Canvas/extended/ChartCreator.js +50 -0
  114. package/dist/esm/Canvas/extended/ChartCreator.js.map +1 -0
  115. package/dist/esm/Canvas/extended/GIFCreator.d.ts +43 -0
  116. package/dist/esm/Canvas/extended/GIFCreator.d.ts.map +1 -0
  117. package/dist/esm/Canvas/extended/GIFCreator.js +157 -0
  118. package/dist/esm/Canvas/extended/GIFCreator.js.map +1 -0
  119. package/dist/esm/Canvas/extended/ImageCreator.d.ts +83 -0
  120. package/dist/esm/Canvas/extended/ImageCreator.d.ts.map +1 -0
  121. package/dist/esm/Canvas/extended/ImageCreator.js +479 -0
  122. package/dist/esm/Canvas/extended/ImageCreator.js.map +1 -0
  123. package/dist/esm/Canvas/extended/TextCreator.d.ts +35 -0
  124. package/dist/esm/Canvas/extended/TextCreator.d.ts.map +1 -0
  125. package/dist/esm/Canvas/extended/TextCreator.js +98 -0
  126. package/dist/esm/Canvas/extended/TextCreator.js.map +1 -0
  127. package/dist/esm/Canvas/extended/VideoCreator.d.ts +370 -0
  128. package/dist/esm/Canvas/extended/VideoCreator.d.ts.map +1 -0
  129. package/dist/esm/Canvas/extended/VideoCreator.js +478 -0
  130. package/dist/esm/Canvas/extended/VideoCreator.js.map +1 -0
  131. package/dist/esm/Canvas/utils/Background/bg.d.ts +1 -1
  132. package/dist/esm/Canvas/utils/Background/bg.d.ts.map +1 -1
  133. package/dist/esm/Canvas/utils/Background/bg.js +43 -7
  134. package/dist/esm/Canvas/utils/Background/bg.js.map +1 -1
  135. package/dist/esm/Canvas/utils/Charts/barchart.d.ts +230 -0
  136. package/dist/esm/Canvas/utils/Charts/barchart.d.ts.map +1 -0
  137. package/dist/esm/Canvas/utils/Charts/barchart.js +1891 -0
  138. package/dist/esm/Canvas/utils/Charts/barchart.js.map +1 -0
  139. package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts +103 -0
  140. package/dist/esm/Canvas/utils/Charts/comparisonchart.d.ts.map +1 -0
  141. package/dist/esm/Canvas/utils/Charts/comparisonchart.js +368 -0
  142. package/dist/esm/Canvas/utils/Charts/comparisonchart.js.map +1 -0
  143. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts +178 -0
  144. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.d.ts.map +1 -0
  145. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js +1389 -0
  146. package/dist/esm/Canvas/utils/Charts/horizontalbarchart.js.map +1 -0
  147. package/dist/esm/Canvas/utils/Charts/index.d.ts +45 -0
  148. package/dist/esm/Canvas/utils/Charts/index.d.ts.map +1 -0
  149. package/dist/esm/Canvas/utils/Charts/index.js +17 -0
  150. package/dist/esm/Canvas/utils/Charts/index.js.map +1 -0
  151. package/dist/esm/Canvas/utils/Charts/linechart.d.ts +216 -0
  152. package/dist/esm/Canvas/utils/Charts/linechart.d.ts.map +1 -0
  153. package/dist/esm/Canvas/utils/Charts/linechart.js +1761 -0
  154. package/dist/esm/Canvas/utils/Charts/linechart.js.map +1 -0
  155. package/dist/esm/Canvas/utils/Charts/piechart.d.ts +167 -0
  156. package/dist/esm/Canvas/utils/Charts/piechart.d.ts.map +1 -0
  157. package/dist/esm/Canvas/utils/Charts/piechart.js +794 -0
  158. package/dist/esm/Canvas/utils/Charts/piechart.js.map +1 -0
  159. package/dist/esm/Canvas/utils/General/batchOperations.d.ts.map +1 -1
  160. package/dist/esm/Canvas/utils/General/batchOperations.js +3 -4
  161. package/dist/esm/Canvas/utils/General/batchOperations.js.map +1 -1
  162. package/dist/esm/Canvas/utils/General/general functions.d.ts.map +1 -1
  163. package/dist/esm/Canvas/utils/General/general functions.js +62 -33
  164. package/dist/esm/Canvas/utils/General/general functions.js.map +1 -1
  165. package/dist/esm/Canvas/utils/General/imageStitching.d.ts.map +1 -1
  166. package/dist/esm/Canvas/utils/General/imageStitching.js +3 -6
  167. package/dist/esm/Canvas/utils/General/imageStitching.js.map +1 -1
  168. package/dist/esm/Canvas/utils/Image/imageMasking.d.ts.map +1 -1
  169. package/dist/esm/Canvas/utils/Image/imageMasking.js +5 -12
  170. package/dist/esm/Canvas/utils/Image/imageMasking.js.map +1 -1
  171. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts +4 -4
  172. package/dist/esm/Canvas/utils/Image/imageProperties.d.ts.map +1 -1
  173. package/dist/esm/Canvas/utils/Image/imageProperties.js +44 -9
  174. package/dist/esm/Canvas/utils/Image/imageProperties.js.map +1 -1
  175. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts +5 -0
  176. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.d.ts.map +1 -1
  177. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js +48 -5
  178. package/dist/esm/Canvas/utils/Texts/enhancedTextRenderer.js.map +1 -1
  179. package/dist/esm/Canvas/utils/Texts/textProperties.d.ts +1 -1
  180. package/dist/esm/Canvas/utils/Texts/textProperties.d.ts.map +1 -1
  181. package/dist/esm/Canvas/utils/Texts/textProperties.js +48 -5
  182. package/dist/esm/Canvas/utils/Texts/textProperties.js.map +1 -1
  183. package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts +489 -0
  184. package/dist/esm/Canvas/utils/Video/videoHelpers.d.ts.map +1 -0
  185. package/dist/esm/Canvas/utils/Video/videoHelpers.js +1835 -0
  186. package/dist/esm/Canvas/utils/Video/videoHelpers.js.map +1 -0
  187. package/dist/esm/Canvas/utils/errorUtils.d.ts +15 -0
  188. package/dist/esm/Canvas/utils/errorUtils.d.ts.map +1 -0
  189. package/dist/esm/Canvas/utils/errorUtils.js +26 -0
  190. package/dist/esm/Canvas/utils/errorUtils.js.map +1 -0
  191. package/dist/esm/Canvas/utils/types.d.ts +17 -178
  192. package/dist/esm/Canvas/utils/types.d.ts.map +1 -1
  193. package/dist/esm/Canvas/utils/types.js.map +1 -1
  194. package/dist/esm/Canvas/utils/utils.d.ts +4 -3
  195. package/dist/esm/Canvas/utils/utils.d.ts.map +1 -1
  196. package/dist/esm/Canvas/utils/utils.js +40 -6
  197. package/dist/esm/Canvas/utils/utils.js.map +1 -1
  198. package/dist/esm/index.d.ts +1 -8
  199. package/dist/esm/index.d.ts.map +1 -1
  200. package/dist/esm/index.js +14 -45
  201. package/dist/esm/index.js.map +1 -1
  202. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  203. package/package.json +118 -82
  204. package/dist/cjs/Canvas/utils/Charts/charts.d.ts +0 -13
  205. package/dist/cjs/Canvas/utils/Charts/charts.d.ts.map +0 -1
  206. package/dist/cjs/Canvas/utils/Charts/charts.js +0 -466
  207. package/dist/cjs/Canvas/utils/Charts/charts.js.map +0 -1
  208. package/dist/esm/Canvas/utils/Charts/charts.d.ts +0 -13
  209. package/dist/esm/Canvas/utils/Charts/charts.d.ts.map +0 -1
  210. package/dist/esm/Canvas/utils/Charts/charts.js +0 -466
  211. package/dist/esm/Canvas/utils/Charts/charts.js.map +0 -1
  212. package/lib/Canvas/ApexPainter.ts +0 -5414
  213. package/lib/Canvas/utils/Background/bg.ts +0 -285
  214. package/lib/Canvas/utils/Charts/charts.ts +0 -548
  215. package/lib/Canvas/utils/Custom/advancedLines.ts +0 -387
  216. package/lib/Canvas/utils/Custom/customLines.ts +0 -206
  217. package/lib/Canvas/utils/General/batchOperations.ts +0 -103
  218. package/lib/Canvas/utils/General/conversion.ts +0 -34
  219. package/lib/Canvas/utils/General/general functions.ts +0 -726
  220. package/lib/Canvas/utils/General/imageCompression.ts +0 -316
  221. package/lib/Canvas/utils/General/imageStitching.ts +0 -252
  222. package/lib/Canvas/utils/Image/imageEffects.ts +0 -175
  223. package/lib/Canvas/utils/Image/imageFilters.ts +0 -356
  224. package/lib/Canvas/utils/Image/imageMasking.ts +0 -335
  225. package/lib/Canvas/utils/Image/imageProperties.ts +0 -587
  226. package/lib/Canvas/utils/Image/professionalImageFilters.ts +0 -391
  227. package/lib/Canvas/utils/Image/simpleProfessionalFilters.ts +0 -229
  228. package/lib/Canvas/utils/Patterns/enhancedPatternRenderer.ts +0 -455
  229. package/lib/Canvas/utils/Shapes/shapes.ts +0 -528
  230. package/lib/Canvas/utils/Texts/enhancedTextRenderer.ts +0 -716
  231. package/lib/Canvas/utils/Texts/textPathRenderer.ts +0 -320
  232. package/lib/Canvas/utils/Texts/textProperties.ts +0 -231
  233. package/lib/Canvas/utils/types.ts +0 -983
  234. package/lib/Canvas/utils/utils.ts +0 -135
  235. package/lib/index.ts +0 -81
  236. 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