apexify.js 5.1.1 → 5.2.0

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