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,1389 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createHorizontalBarChart = createHorizontalBarChart;
4
+ const canvas_1 = require("@napi-rs/canvas");
5
+ const imageProperties_1 = require("../Image/imageProperties");
6
+ /**
7
+ * Helper function to render enhanced text with custom fonts, gradients, shadows, strokes
8
+ */
9
+ async function renderEnhancedText(ctx, text, x, y, style, fontSize, color, textGradient) {
10
+ ctx.save();
11
+ // Preserve text alignment settings
12
+ const savedTextAlign = ctx.textAlign;
13
+ const savedTextBaseline = ctx.textBaseline;
14
+ const effectiveFontSize = fontSize || style?.fontSize || 16;
15
+ const fontFamily = style?.fontFamily || style?.fontName || 'Arial';
16
+ let fontString = '';
17
+ if (style?.bold)
18
+ fontString += 'bold ';
19
+ if (style?.italic)
20
+ fontString += 'italic ';
21
+ fontString += `${effectiveFontSize}px "${fontFamily}"`;
22
+ ctx.font = fontString;
23
+ // Restore text alignment to ensure correct positioning
24
+ ctx.textAlign = savedTextAlign;
25
+ ctx.textBaseline = savedTextBaseline;
26
+ // Register custom font if provided
27
+ if (style?.fontPath && style?.fontName) {
28
+ try {
29
+ const { GlobalFonts } = await import('@napi-rs/canvas');
30
+ const path = await import('path');
31
+ const fullPath = path.join(process.cwd(), style.fontPath);
32
+ GlobalFonts.registerFromPath(fullPath, style.fontName);
33
+ ctx.font = fontString.replace(`"${fontFamily}"`, `"${style.fontName}"`);
34
+ }
35
+ catch (error) {
36
+ console.warn(`Failed to register font: ${style.fontPath}`, error);
37
+ }
38
+ }
39
+ // Apply shadow
40
+ if (style?.shadow) {
41
+ ctx.shadowColor = style.shadow.color || 'rgba(0,0,0,0.5)';
42
+ ctx.shadowOffsetX = style.shadow.offsetX || 2;
43
+ ctx.shadowOffsetY = style.shadow.offsetY || 2;
44
+ ctx.shadowBlur = style.shadow.blur || 4;
45
+ if (style.shadow.opacity !== undefined) {
46
+ ctx.globalAlpha = style.shadow.opacity;
47
+ }
48
+ }
49
+ // Set fill style (gradient or color)
50
+ if (textGradient) {
51
+ const metrics = ctx.measureText(text);
52
+ ctx.fillStyle = (0, imageProperties_1.createGradientFill)(ctx, textGradient, {
53
+ x, y, w: metrics.width, h: effectiveFontSize
54
+ });
55
+ }
56
+ else if (color) {
57
+ ctx.fillStyle = color;
58
+ }
59
+ // Draw text
60
+ ctx.fillText(text, x, y);
61
+ // Apply stroke
62
+ if (style?.stroke) {
63
+ ctx.strokeStyle = style.stroke.color || '#000000';
64
+ ctx.lineWidth = style.stroke.width || 1;
65
+ if (style.stroke.gradient) {
66
+ const metrics = ctx.measureText(text);
67
+ ctx.strokeStyle = (0, imageProperties_1.createGradientFill)(ctx, style.stroke.gradient, {
68
+ x, y, w: metrics.width, h: effectiveFontSize
69
+ });
70
+ }
71
+ ctx.strokeText(text, x, y);
72
+ }
73
+ // Reset shadow and alpha
74
+ ctx.shadowColor = 'transparent';
75
+ ctx.shadowOffsetX = 0;
76
+ ctx.shadowOffsetY = 0;
77
+ ctx.shadowBlur = 0;
78
+ ctx.globalAlpha = 1;
79
+ ctx.restore();
80
+ }
81
+ /**
82
+ * Helper function to fill a shape with gradient or color
83
+ */
84
+ function fillWithGradientOrColor(ctx, gradient, color, defaultColor = '#000000', rect) {
85
+ if (gradient && rect) {
86
+ ctx.fillStyle = (0, imageProperties_1.createGradientFill)(ctx, gradient, rect);
87
+ }
88
+ else {
89
+ ctx.fillStyle = color || defaultColor;
90
+ }
91
+ }
92
+ /**
93
+ * Draws an arrow at the end of an axis
94
+ */
95
+ function drawArrow(ctx, x, y, angle, size) {
96
+ ctx.save();
97
+ ctx.translate(x, y);
98
+ ctx.rotate(angle);
99
+ ctx.beginPath();
100
+ ctx.moveTo(0, 0);
101
+ ctx.lineTo(-size, -size / 2);
102
+ ctx.lineTo(-size, size / 2);
103
+ ctx.closePath();
104
+ ctx.fill();
105
+ ctx.restore();
106
+ }
107
+ /**
108
+ * Draws X-axis ticks and labels (horizontal axis - value axis)
109
+ */
110
+ function drawXAxisTicks(ctx, originX, originY, axisEndX, minValue, maxValue, step, tickFontSize, customValues, valueSpacing) {
111
+ ctx.save();
112
+ ctx.fillStyle = '#000000';
113
+ ctx.font = `${tickFontSize}px Arial`;
114
+ ctx.textAlign = 'center';
115
+ ctx.textBaseline = 'top';
116
+ const chartWidth = axisEndX - originX;
117
+ if (customValues && customValues.length > 0) {
118
+ // Position labels based on their actual values, not pixel spacing
119
+ const actualMin = Math.min(...customValues);
120
+ const actualMax = Math.max(...customValues);
121
+ const range = actualMax - actualMin || 1; // Avoid division by zero
122
+ let lastLabelX = -Infinity;
123
+ const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40; // Use valueSpacing as min spacing if provided
124
+ customValues.forEach((value) => {
125
+ const x = originX + ((value - actualMin) / range) * chartWidth;
126
+ const labelText = value.toString();
127
+ // Check if this label would overlap with the previous one
128
+ if (x - lastLabelX < minLabelSpacing && value > actualMin) {
129
+ // Skip this label to prevent overlap (but still draw tick mark)
130
+ ctx.beginPath();
131
+ ctx.moveTo(x, originY);
132
+ ctx.lineTo(x, originY + 5);
133
+ ctx.stroke();
134
+ return;
135
+ }
136
+ ctx.beginPath();
137
+ ctx.moveTo(x, originY);
138
+ ctx.lineTo(x, originY + 5);
139
+ ctx.stroke();
140
+ ctx.fillText(labelText, x, originY + 10);
141
+ lastLabelX = x; // Update last label center position
142
+ });
143
+ }
144
+ else {
145
+ // Range-based positioning - always position based on values, use valueSpacing only for label density
146
+ const range = maxValue - minValue || 1; // Avoid division by zero
147
+ // Calculate all tick positions first
148
+ const tickValues = [];
149
+ for (let value = minValue; value <= maxValue; value += step) {
150
+ tickValues.push(value);
151
+ }
152
+ // Draw ticks, but skip labels if they're too close together
153
+ let lastLabelX = -Infinity;
154
+ const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 40; // Use valueSpacing as min spacing if provided
155
+ for (const value of tickValues) {
156
+ const x = originX + ((value - minValue) / range) * chartWidth;
157
+ const labelText = value.toString();
158
+ // Check if this label center is too close to the previous label center
159
+ if (x - lastLabelX < minLabelSpacing && value > minValue) {
160
+ // Skip this label to prevent overlap - but still draw the tick mark
161
+ ctx.beginPath();
162
+ ctx.moveTo(x, originY);
163
+ ctx.lineTo(x, originY + 5);
164
+ ctx.stroke();
165
+ continue;
166
+ }
167
+ // Draw tick mark
168
+ ctx.beginPath();
169
+ ctx.moveTo(x, originY);
170
+ ctx.lineTo(x, originY + 5);
171
+ ctx.stroke();
172
+ // Draw label
173
+ ctx.fillText(labelText, x, originY + 10);
174
+ // Update last label position (center of the label)
175
+ lastLabelX = x;
176
+ }
177
+ }
178
+ ctx.restore();
179
+ }
180
+ /**
181
+ * Draws Y-axis ticks and labels (vertical axis - category axis)
182
+ */
183
+ function drawYAxisTicks(ctx, originX, originY, axisEndY, minValue, maxValue, step, tickFontSize, customValues, valueSpacing) {
184
+ ctx.save();
185
+ ctx.fillStyle = '#000000';
186
+ ctx.font = `${tickFontSize}px Arial`;
187
+ ctx.textAlign = 'right';
188
+ ctx.textBaseline = 'middle';
189
+ const chartHeight = originY - axisEndY;
190
+ if (customValues && customValues.length > 0) {
191
+ const totalValues = customValues.length;
192
+ const divisor = totalValues > 1 ? totalValues - 1 : 1;
193
+ if (valueSpacing && valueSpacing > 0) {
194
+ let currentY = originY;
195
+ customValues.forEach((value, index) => {
196
+ if (index === 0) {
197
+ currentY = originY;
198
+ }
199
+ else {
200
+ currentY -= valueSpacing;
201
+ }
202
+ if (currentY >= axisEndY && currentY <= originY) {
203
+ ctx.beginPath();
204
+ ctx.moveTo(originX - 5, currentY);
205
+ ctx.lineTo(originX, currentY);
206
+ ctx.stroke();
207
+ ctx.fillText(value.toString(), originX - 10, currentY);
208
+ }
209
+ });
210
+ }
211
+ else {
212
+ // Position based on value range
213
+ const range = maxValue - minValue || 1;
214
+ customValues.forEach((value) => {
215
+ const y = originY - ((value - minValue) / range) * chartHeight;
216
+ ctx.beginPath();
217
+ ctx.moveTo(originX - 5, y);
218
+ ctx.lineTo(originX, y);
219
+ ctx.stroke();
220
+ ctx.fillText(value.toString(), originX - 10, y);
221
+ });
222
+ }
223
+ }
224
+ else {
225
+ // Range-based positioning
226
+ const range = maxValue - minValue || 1;
227
+ let lastLabelY = Infinity;
228
+ const minLabelSpacing = valueSpacing && valueSpacing > 0 ? valueSpacing : 30; // Vertical spacing
229
+ for (let value = minValue; value <= maxValue; value += step) {
230
+ const y = originY - ((value - minValue) / range) * chartHeight;
231
+ const labelText = value.toString();
232
+ // Check if this label would overlap with the previous one
233
+ if (lastLabelY - y < minLabelSpacing && value > minValue) {
234
+ // Skip this label to prevent overlap - but still draw the tick mark
235
+ ctx.beginPath();
236
+ ctx.moveTo(originX - 5, y);
237
+ ctx.lineTo(originX, y);
238
+ ctx.stroke();
239
+ continue;
240
+ }
241
+ ctx.beginPath();
242
+ ctx.moveTo(originX - 5, y);
243
+ ctx.lineTo(originX, y);
244
+ ctx.stroke();
245
+ ctx.fillText(labelText, originX - 10, y);
246
+ lastLabelY = y; // Update last label position
247
+ }
248
+ }
249
+ ctx.restore();
250
+ }
251
+ /**
252
+ * Draws grid lines for horizontal bar chart
253
+ */
254
+ function drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep, yMin, yMax, yStep, yAxisCustomValues, xAxisCustomValues, gridColor = '#E0E0E0', gridWidth = 1) {
255
+ ctx.save();
256
+ ctx.strokeStyle = gridColor;
257
+ ctx.lineWidth = gridWidth;
258
+ ctx.setLineDash([2, 2]);
259
+ const chartWidth = axisEndX - originX;
260
+ const chartHeight = originY - axisEndY;
261
+ // Draw vertical grid lines (based on X-axis values)
262
+ if (xAxisCustomValues && xAxisCustomValues.length > 0) {
263
+ const actualMin = Math.min(...xAxisCustomValues);
264
+ const actualMax = Math.max(...xAxisCustomValues);
265
+ const xRange = actualMax - actualMin || 1;
266
+ xAxisCustomValues.forEach((value) => {
267
+ const x = originX + ((value - actualMin) / xRange) * chartWidth;
268
+ ctx.beginPath();
269
+ ctx.moveTo(x, axisEndY);
270
+ ctx.lineTo(x, originY);
271
+ ctx.stroke();
272
+ });
273
+ }
274
+ else {
275
+ const xRange = xMax - xMin || 1;
276
+ for (let value = xMin; value <= xMax; value += xStep) {
277
+ const x = originX + ((value - xMin) / xRange) * chartWidth;
278
+ ctx.beginPath();
279
+ ctx.moveTo(x, axisEndY);
280
+ ctx.lineTo(x, originY);
281
+ ctx.stroke();
282
+ }
283
+ }
284
+ // Draw horizontal grid lines (based on Y-axis values/range)
285
+ if (yAxisCustomValues && yAxisCustomValues.length > 0) {
286
+ const actualMin = Math.min(...yAxisCustomValues);
287
+ const actualMax = Math.max(...yAxisCustomValues);
288
+ const yRange = actualMax - actualMin || 1;
289
+ yAxisCustomValues.forEach((value) => {
290
+ const y = originY - ((value - actualMin) / yRange) * chartHeight;
291
+ ctx.beginPath();
292
+ ctx.moveTo(originX, y);
293
+ ctx.lineTo(axisEndX, y);
294
+ ctx.stroke();
295
+ });
296
+ }
297
+ else {
298
+ const yRange = yMax - yMin || 1;
299
+ for (let value = yMin; value <= yMax; value += yStep) {
300
+ const y = originY - ((value - yMin) / yRange) * chartHeight;
301
+ ctx.beginPath();
302
+ ctx.moveTo(originX, y);
303
+ ctx.lineTo(axisEndX, y);
304
+ ctx.stroke();
305
+ }
306
+ }
307
+ ctx.restore();
308
+ }
309
+ /**
310
+ * Wraps text to fit within a maximum width
311
+ */
312
+ function wrapText(ctx, text, maxWidth) {
313
+ const words = text.split(' ');
314
+ const lines = [];
315
+ let currentLine = words[0];
316
+ for (let i = 1; i < words.length; i++) {
317
+ const word = words[i];
318
+ const width = ctx.measureText(currentLine + ' ' + word).width;
319
+ if (width < maxWidth) {
320
+ currentLine += ' ' + word;
321
+ }
322
+ else {
323
+ lines.push(currentLine);
324
+ currentLine = word;
325
+ }
326
+ }
327
+ lines.push(currentLine);
328
+ return lines;
329
+ }
330
+ /**
331
+ * Calculates legend dimensions without needing a canvas context
332
+ */
333
+ function calculateLegendDimensions(legend, fontSize, maxWidth, wrapTextEnabled = true, paddingBox = 8) {
334
+ if (!legend || legend.length === 0)
335
+ return { width: 0, height: 0 };
336
+ const boxSize = 15;
337
+ const spacing = 10;
338
+ const padding = paddingBox;
339
+ // Create a temporary canvas to measure text
340
+ const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
341
+ const tempCtx = tempCanvas.getContext('2d');
342
+ tempCtx.font = `${fontSize}px Arial`;
343
+ const textSpacing = 10;
344
+ const effectiveMaxWidth = maxWidth ? maxWidth - padding * 2 - boxSize - textSpacing : undefined;
345
+ let maxEntryWidth = 0;
346
+ const entryHeights = [];
347
+ legend.forEach(entry => {
348
+ let textWidth;
349
+ let textHeight;
350
+ if (wrapTextEnabled && effectiveMaxWidth) {
351
+ const wrappedLines = wrapText(tempCtx, entry.label, effectiveMaxWidth);
352
+ textWidth = Math.max(...wrappedLines.map(line => tempCtx.measureText(line).width));
353
+ textHeight = wrappedLines.length * fontSize * 1.2;
354
+ }
355
+ else {
356
+ textWidth = tempCtx.measureText(entry.label).width;
357
+ textHeight = fontSize;
358
+ }
359
+ const entryWidth = boxSize + textSpacing + textWidth;
360
+ maxEntryWidth = Math.max(maxEntryWidth, entryWidth);
361
+ entryHeights.push(Math.max(boxSize, textHeight));
362
+ });
363
+ const legendWidth = maxWidth ? maxWidth : maxEntryWidth + padding * 2;
364
+ const legendHeight = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? spacing : 0), 0) + padding * 2;
365
+ return { width: legendWidth, height: legendHeight };
366
+ }
367
+ /**
368
+ * Draws legend/key showing colors and their meanings at a specific position
369
+ */
370
+ async function drawLegendAtPosition(ctx, legend, legendX, legendY, fontSize, backgroundColor = '#FFFFFF', textColor, borderColor, paddingBox, maxWidth, wrapTextEnabled = true, backgroundGradient, textGradient, textStyle) {
371
+ if (!legend || legend.length === 0)
372
+ return;
373
+ ctx.save();
374
+ const boxSize = 15;
375
+ const spacing = 10;
376
+ const padding = paddingBox ?? 8;
377
+ ctx.font = `${fontSize}px Arial`;
378
+ // Determine colors
379
+ const isDarkBackground = backgroundColor === '#000000' || backgroundColor.toLowerCase() === 'black';
380
+ const effectiveTextColor = textColor ?? (isDarkBackground ? '#FFFFFF' : '#000000');
381
+ const effectiveBgColor = isDarkBackground ? 'rgba(0, 0, 0, 0.8)' : (backgroundColor.startsWith('rgba') || backgroundColor.startsWith('rgb') ? backgroundColor : 'rgba(255, 255, 255, 0.9)');
382
+ const effectiveBorderColor = borderColor ?? (isDarkBackground ? '#FFFFFF' : '#000000');
383
+ // Calculate dimensions with text wrapping support
384
+ const textSpacing = 10;
385
+ const effectiveMaxWidth = maxWidth ? maxWidth - padding * 2 - boxSize - textSpacing : undefined;
386
+ let maxEntryWidth = 0;
387
+ const entryHeights = [];
388
+ legend.forEach(entry => {
389
+ let textWidth;
390
+ let textHeight;
391
+ if (wrapTextEnabled && effectiveMaxWidth) {
392
+ const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
393
+ textWidth = Math.max(...wrappedLines.map(line => ctx.measureText(line).width));
394
+ textHeight = wrappedLines.length * fontSize * 1.2;
395
+ }
396
+ else {
397
+ textWidth = ctx.measureText(entry.label).width;
398
+ textHeight = fontSize;
399
+ }
400
+ const entryWidth = boxSize + textSpacing + textWidth;
401
+ maxEntryWidth = Math.max(maxEntryWidth, entryWidth);
402
+ entryHeights.push(Math.max(boxSize, textHeight));
403
+ });
404
+ const legendWidth = maxWidth ? maxWidth : maxEntryWidth + padding * 2;
405
+ const legendHeight = entryHeights.reduce((sum, h, i) => sum + h + (i < entryHeights.length - 1 ? spacing : 0), 0) + padding * 2;
406
+ // Draw legend background (gradient or color)
407
+ ctx.beginPath();
408
+ ctx.rect(legendX, legendY, legendWidth, legendHeight);
409
+ fillWithGradientOrColor(ctx, backgroundGradient, effectiveBgColor, effectiveBgColor, { x: legendX, y: legendY, w: legendWidth, h: legendHeight });
410
+ ctx.fill();
411
+ ctx.strokeStyle = effectiveBorderColor;
412
+ ctx.lineWidth = 1;
413
+ ctx.strokeRect(legendX, legendY, legendWidth, legendHeight);
414
+ ctx.textAlign = 'left';
415
+ ctx.textBaseline = 'middle';
416
+ let currentY = legendY + padding;
417
+ for (let index = 0; index < legend.length; index++) {
418
+ const entry = legend[index];
419
+ const entryHeight = entryHeights[index];
420
+ const centerY = currentY + entryHeight / 2;
421
+ // Draw color box (gradient or color)
422
+ ctx.beginPath();
423
+ ctx.rect(legendX + padding, centerY - boxSize / 2, boxSize, boxSize);
424
+ fillWithGradientOrColor(ctx, entry.gradient, entry.color || '#4A90E2', '#4A90E2', { x: legendX + padding, y: centerY - boxSize / 2, w: boxSize, h: boxSize });
425
+ ctx.fill();
426
+ ctx.strokeStyle = effectiveBorderColor;
427
+ ctx.lineWidth = 1;
428
+ ctx.strokeRect(legendX + padding, centerY - boxSize / 2, boxSize, boxSize);
429
+ const textX = legendX + padding + boxSize + textSpacing;
430
+ if (wrapTextEnabled && effectiveMaxWidth) {
431
+ const wrappedLines = wrapText(ctx, entry.label, effectiveMaxWidth);
432
+ const lineHeight = fontSize * 1.2;
433
+ const startY = centerY - (wrappedLines.length - 1) * lineHeight / 2;
434
+ for (let lineIndex = 0; lineIndex < wrappedLines.length; lineIndex++) {
435
+ await renderEnhancedText(ctx, wrappedLines[lineIndex], textX, startY + lineIndex * lineHeight, textStyle, fontSize, effectiveTextColor, textGradient);
436
+ }
437
+ }
438
+ else {
439
+ await renderEnhancedText(ctx, entry.label, textX, centerY, textStyle, fontSize, effectiveTextColor, textGradient);
440
+ }
441
+ currentY += entryHeight + spacing;
442
+ }
443
+ ctx.restore();
444
+ }
445
+ /**
446
+ * Draws legend/key showing colors and their meanings
447
+ */
448
+ function drawLegend(ctx, legend, position, width, height, padding, fontSize, backgroundColor = '#FFFFFF', legendSpacing = 20) {
449
+ if (!legend || legend.length === 0)
450
+ return;
451
+ ctx.save();
452
+ const boxSize = 15;
453
+ const spacing = 10;
454
+ const paddingBox = 8;
455
+ ctx.font = `${fontSize}px Arial`;
456
+ const maxLabelWidth = Math.max(...legend.map(e => ctx.measureText(e.label).width));
457
+ const legendWidth = boxSize + spacing + maxLabelWidth + paddingBox * 2;
458
+ const legendHeight = legend.length * (boxSize + spacing) + paddingBox * 2;
459
+ let legendX, legendY;
460
+ switch (position) {
461
+ case 'top':
462
+ legendX = width - padding.right - legendWidth - legendSpacing;
463
+ legendY = padding.top + legendSpacing;
464
+ break;
465
+ case 'bottom':
466
+ legendX = width - padding.right - legendWidth - legendSpacing;
467
+ legendY = height - padding.bottom - legendHeight - legendSpacing;
468
+ break;
469
+ case 'right':
470
+ legendX = width - padding.right - legendWidth - legendSpacing;
471
+ legendY = padding.top + legendSpacing;
472
+ break;
473
+ case 'left':
474
+ legendX = padding.left + legendSpacing;
475
+ legendY = padding.top + legendSpacing;
476
+ break;
477
+ default:
478
+ legendX = width - padding.right - legendWidth - legendSpacing;
479
+ legendY = padding.top + legendSpacing;
480
+ }
481
+ const isDarkBackground = backgroundColor === '#000000' || backgroundColor.toLowerCase() === 'black';
482
+ const textColor = isDarkBackground ? '#FFFFFF' : '#000000';
483
+ const bgColor = isDarkBackground ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)';
484
+ const borderColor = isDarkBackground ? '#FFFFFF' : '#000000';
485
+ ctx.fillStyle = bgColor;
486
+ ctx.fillRect(legendX, legendY, legendWidth, legendHeight);
487
+ ctx.strokeStyle = borderColor;
488
+ ctx.lineWidth = 1;
489
+ ctx.strokeRect(legendX, legendY, legendWidth, legendHeight);
490
+ ctx.font = `${fontSize}px Arial`;
491
+ ctx.textAlign = 'left';
492
+ ctx.textBaseline = 'middle';
493
+ legend.forEach((entry, index) => {
494
+ const y = legendY + paddingBox + index * (boxSize + spacing) + boxSize / 2;
495
+ const x = legendX + paddingBox;
496
+ ctx.beginPath();
497
+ ctx.rect(x, y - boxSize / 2, boxSize, boxSize);
498
+ fillWithGradientOrColor(ctx, entry.gradient, entry.color || '#4A90E2', '#4A90E2', { x, y: y - boxSize / 2, w: boxSize, h: boxSize });
499
+ ctx.fill();
500
+ ctx.strokeStyle = borderColor;
501
+ ctx.lineWidth = 1;
502
+ ctx.strokeRect(x, y - boxSize / 2, boxSize, boxSize);
503
+ ctx.fillStyle = textColor;
504
+ ctx.fillText(entry.label, x + boxSize + spacing, y);
505
+ });
506
+ ctx.restore();
507
+ }
508
+ /**
509
+ * Calculates responsive canvas height based on number of bars
510
+ */
511
+ function calculateResponsiveHeight(dataLength, options = {}) {
512
+ const padding = options.dimensions?.padding || {};
513
+ const paddingTop = padding.top ?? 60;
514
+ const paddingBottom = padding.bottom ?? 80;
515
+ const minBarHeight = options.bars?.minHeight ?? 40;
516
+ const barSpacing = options.bars?.spacing ?? 15; // Use same default spacing
517
+ // Calculate minimum height needed: (number of bars * bar height) + (spacing between bars)
518
+ // Each bar needs minBarHeight, and between each pair of bars we need barSpacing
519
+ const chartAreaHeight = dataLength * minBarHeight + (dataLength - 1) * barSpacing;
520
+ // Add title height if needed
521
+ const titleHeight = options.labels?.title?.text ? (options.labels.title.fontSize ?? 24) + 30 : 0;
522
+ const axisLabelHeight = ((options.axes?.x?.label || options.axes?.y?.label) ? (options.labels?.barLabelDefaults?.fontSize ?? 14) + 20 : 0);
523
+ return paddingTop + titleHeight + chartAreaHeight + axisLabelHeight + paddingBottom;
524
+ }
525
+ /**
526
+ * Creates a horizontal bar chart
527
+ * @param data Array of horizontal bar chart data
528
+ * @param options Chart options
529
+ * @returns Canvas buffer
530
+ */
531
+ async function createHorizontalBarChart(data, options = {}) {
532
+ // Extract and map organized config to internal variables
533
+ let width = options.dimensions?.width ?? 800;
534
+ const padding = options.dimensions?.padding || {};
535
+ // Appearance
536
+ const backgroundColor = options.appearance?.backgroundColor ?? '#FFFFFF';
537
+ const backgroundGradient = options.appearance?.backgroundGradient;
538
+ const backgroundImage = options.appearance?.backgroundImage;
539
+ const axisColor = options.appearance?.axisColor ?? options.axes?.x?.color ?? options.axes?.y?.color ?? '#000000';
540
+ const axisWidth = options.appearance?.axisWidth ?? options.axes?.x?.width ?? options.axes?.y?.width ?? 2;
541
+ const arrowSize = options.appearance?.arrowSize ?? 10;
542
+ // Labels
543
+ const chartTitle = options.labels?.title?.text;
544
+ const chartTitleFontSize = options.labels?.title?.fontSize ?? 24;
545
+ const showBarLabels = options.labels?.barLabelDefaults?.show ?? true;
546
+ const barLabelPosition = options.labels?.barLabelDefaults?.defaultPosition ?? 'left';
547
+ const axisLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
548
+ const showValues = options.labels?.valueLabelDefaults?.show ?? true;
549
+ const valueFontSize = options.labels?.valueLabelDefaults?.fontSize ?? 12;
550
+ const valueColor = options.labels?.valueLabelDefaults?.defaultColor ?? '#000000';
551
+ // Axes
552
+ const xAxisLabel = options.axes?.x?.label;
553
+ const yAxisLabel = options.axes?.y?.label;
554
+ const axisLabelColor = options.axes?.x?.labelColor ?? options.axes?.y?.labelColor ?? '#000000';
555
+ const xAxisRange = options.axes?.x?.range;
556
+ const xAxisValues = options.axes?.x?.values;
557
+ const baseline = options.axes?.x?.baseline ?? 0; // Custom baseline value (default: 0)
558
+ const yAxisValues = options.axes?.y?.values;
559
+ const tickFontSize = options.axes?.x?.tickFontSize ?? options.axes?.y?.tickFontSize ?? 12;
560
+ const xAxisValueSpacing = options.axes?.x?.valueSpacing;
561
+ const yAxisValueSpacing = options.axes?.y?.valueSpacing;
562
+ // Legend
563
+ const showLegend = options.legend?.show ?? false;
564
+ const legend = options.legend?.entries;
565
+ const legendPosition = options.legend?.position ?? 'right'; // Default: right
566
+ // Grid
567
+ const showGrid = options.grid?.show ?? false;
568
+ const gridColor = options.grid?.color ?? '#E0E0E0';
569
+ const gridWidth = options.grid?.width ?? 1;
570
+ // Chart type
571
+ const chartType = options.type ?? 'standard';
572
+ // Bars
573
+ const minBarHeight = options.bars?.minHeight ?? 30;
574
+ const barSpacing = options.bars?.spacing;
575
+ const groupSpacing = options.bars?.groupSpacing ?? 10;
576
+ const segmentSpacing = options.bars?.segmentSpacing ?? 2;
577
+ const lollipopLineWidth = options.bars?.lineWidth ?? 2; // Line width for lollipop charts (default: 2)
578
+ const lollipopDotSize = options.bars?.dotSize ?? 8; // Dot/circle size for lollipop charts (default: 8)
579
+ const paddingTop = padding.top ?? 60;
580
+ const paddingRight = padding.right ?? 80;
581
+ const paddingBottom = padding.bottom ?? 80;
582
+ const paddingLeft = padding.left ?? 100;
583
+ // Calculate responsive height based on number of bars
584
+ let baseHeight = calculateResponsiveHeight(data.length, options);
585
+ // Calculate legend dimensions and adjust canvas size based on legend position
586
+ let legendWidth = 0;
587
+ let legendHeight = 0;
588
+ let extraWidth = 0;
589
+ let extraHeight = 0;
590
+ const minLegendSpacing = 10; // Minimum spacing from chart area
591
+ if (showLegend && legend && legend.length > 0) {
592
+ const legendMaxWidth = options.legend?.maxWidth;
593
+ const legendWrapText = options.legend?.wrapText !== false;
594
+ const legendPadding = options.legend?.padding;
595
+ const legendDims = calculateLegendDimensions(legend, axisLabelFontSize, legendMaxWidth, legendWrapText, legendPadding);
596
+ legendWidth = legendDims.width;
597
+ legendHeight = legendDims.height;
598
+ const legendSpacing = options.legend?.spacing ?? 20;
599
+ // Adjust canvas dimensions based on legend position
600
+ // For left position, add extra space for Y-axis labels and bar labels
601
+ if (legendPosition === 'left') {
602
+ // Estimate Y-axis label width: measure potential category labels or numeric values
603
+ const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
604
+ const tempCtx = tempCanvas.getContext('2d');
605
+ let estimatedYAxisLabelWidth = 80; // Default estimate (category labels can be longer)
606
+ if (tempCtx) {
607
+ // Check if bar labels are on the left (they act as Y-axis labels)
608
+ const barLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
609
+ const showBarLabels = options.labels?.barLabelDefaults?.show ?? true;
610
+ const barLabelPosition = options.labels?.barLabelDefaults?.defaultPosition ?? 'left';
611
+ const hasLeftLabels = showBarLabels && (barLabelPosition === 'left' ||
612
+ data.some(item => (item.labelPosition ?? barLabelPosition) === 'left'));
613
+ if (hasLeftLabels) {
614
+ // Measure category labels (bar labels) which are typically longer
615
+ tempCtx.font = `${barLabelFontSize}px Arial`;
616
+ data.forEach(d => {
617
+ const labelWidth = tempCtx.measureText(d.label).width;
618
+ estimatedYAxisLabelWidth = Math.max(estimatedYAxisLabelWidth, labelWidth);
619
+ });
620
+ // Add padding: 5px (label offset from originX) + 10px (spacing) = 15px total
621
+ estimatedYAxisLabelWidth += 15;
622
+ }
623
+ else {
624
+ // No left labels, but might have Y-axis numeric ticks
625
+ tempCtx.font = `${tickFontSize}px Arial`;
626
+ // Estimate for numeric Y-axis values if custom values are provided
627
+ estimatedYAxisLabelWidth = 60; // Default for numeric values
628
+ // Add padding: 10px (label offset) + 5px (tick) + 15px (spacing) = 30px total
629
+ estimatedYAxisLabelWidth += 30;
630
+ }
631
+ }
632
+ extraWidth = legendWidth + legendSpacing + estimatedYAxisLabelWidth + minLegendSpacing;
633
+ }
634
+ else if (legendPosition === 'right') {
635
+ extraWidth = legendWidth + legendSpacing + minLegendSpacing;
636
+ }
637
+ else if (legendPosition === 'top' || legendPosition === 'bottom') {
638
+ extraHeight = legendHeight + legendSpacing + minLegendSpacing;
639
+ }
640
+ }
641
+ // adjustedWidth and adjustedHeight are already calculated above
642
+ // Determine X-axis (value axis) range
643
+ // For grouped charts: find max value across all segments
644
+ // For stacked charts: find max sum of values per category
645
+ // For lollipop charts: same as standard (single value per bar)
646
+ let allValues = [];
647
+ if (chartType === 'grouped' || chartType === 'stacked' || chartType === 'lollipop') {
648
+ if (chartType === 'grouped') {
649
+ // For grouped: find max value across all segments
650
+ data.forEach(d => {
651
+ if (d.values && d.values.length > 0) {
652
+ d.values.forEach(seg => allValues.push(seg.value));
653
+ }
654
+ else if (d.value !== undefined) {
655
+ allValues.push(d.value);
656
+ }
657
+ });
658
+ }
659
+ else {
660
+ // For stacked: find max sum per category
661
+ data.forEach(d => {
662
+ if (d.values && d.values.length > 0) {
663
+ const sum = d.values.reduce((acc, seg) => acc + seg.value, 0);
664
+ allValues.push(sum);
665
+ }
666
+ else if (d.value !== undefined) {
667
+ allValues.push(d.value);
668
+ }
669
+ });
670
+ }
671
+ }
672
+ else {
673
+ // Standard chart: use value directly
674
+ allValues = data.map(d => d.value ?? 0).filter(v => v !== undefined && v !== null);
675
+ }
676
+ let xMin, xMax;
677
+ let xAxisCustomValues = xAxisValues;
678
+ const hasExplicitXRange = xAxisRange && xAxisRange.min !== undefined && xAxisRange.max !== undefined;
679
+ // Check if any bars have xStart/xEnd (value ranges)
680
+ const hasValueRanges = data.some(d => d.xStart !== undefined || d.xEnd !== undefined);
681
+ if (hasValueRanges) {
682
+ const allXStarts = data.map(d => d.xStart ?? d.value ?? 0).filter(v => v !== undefined);
683
+ const allXEnds = data.map(d => d.xEnd ?? d.value ?? 0).filter(v => v !== undefined);
684
+ xMin = Math.min(...allXStarts, ...allXEnds);
685
+ xMax = Math.max(...allXStarts, ...allXEnds);
686
+ // Add some padding
687
+ const xPadding = (xMax - xMin) * 0.1;
688
+ xMin = Math.max(0, xMin - xPadding);
689
+ xMax = xMax + xPadding;
690
+ }
691
+ else if (xAxisCustomValues && xAxisCustomValues.length > 0) {
692
+ xMin = Math.min(...xAxisCustomValues);
693
+ xMax = Math.max(...xAxisCustomValues);
694
+ }
695
+ else if (hasExplicitXRange) {
696
+ xMin = xAxisRange.min;
697
+ xMax = xAxisRange.max;
698
+ // Ensure baseline is within range
699
+ const effectiveBaseline = baseline !== undefined ? baseline : 0;
700
+ xMin = Math.min(xMin, effectiveBaseline);
701
+ xMax = Math.max(xMax, effectiveBaseline);
702
+ }
703
+ else {
704
+ xMin = 0;
705
+ xMax = Math.max(...allValues, 1);
706
+ const xPadding = (xMax - xMin) * 0.1;
707
+ const effectiveBaseline = baseline !== undefined ? baseline : 0;
708
+ // Ensure baseline is always included in the range
709
+ xMin = Math.min(Math.max(0, xMin - xPadding), effectiveBaseline);
710
+ xMax = xMax + xPadding;
711
+ }
712
+ // Determine Y-axis (category axis) range - similar to X-axis in standard chart
713
+ const yAxisRange = options.axes?.y?.range;
714
+ let yMin, yMax;
715
+ let yAxisCustomValues = yAxisValues;
716
+ const hasExplicitYRange = yAxisRange && yAxisRange.min !== undefined && yAxisRange.max !== undefined;
717
+ if (yAxisCustomValues && yAxisCustomValues.length > 0) {
718
+ yMin = Math.min(...yAxisCustomValues);
719
+ yMax = Math.max(...yAxisCustomValues);
720
+ }
721
+ else if (hasExplicitYRange) {
722
+ yMin = yAxisRange.min;
723
+ yMax = yAxisRange.max;
724
+ }
725
+ else {
726
+ // Auto-calculate from data indices (0 to data.length - 1)
727
+ yMin = 0;
728
+ yMax = data.length - 1;
729
+ }
730
+ // Validate data values against explicit axis ranges
731
+ if (hasExplicitXRange || xAxisCustomValues) {
732
+ const effectiveXMin = xAxisCustomValues ? Math.min(...xAxisCustomValues) : xAxisRange.min;
733
+ const effectiveXMax = xAxisCustomValues ? Math.max(...xAxisCustomValues) : xAxisRange.max;
734
+ data.forEach((item, itemIndex) => {
735
+ // Check value (X-axis for horizontal bars)
736
+ if (item.value !== undefined && (item.value < effectiveXMin || item.value > effectiveXMax)) {
737
+ throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
738
+ `Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has value ${item.value}, ` +
739
+ `which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
740
+ }
741
+ // Check xStart and xEnd if they exist
742
+ if (item.xStart !== undefined && (item.xStart < effectiveXMin || item.xStart > effectiveXMax)) {
743
+ throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
744
+ `Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has xStart value ${item.xStart}, ` +
745
+ `which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
746
+ }
747
+ if (item.xEnd !== undefined && (item.xEnd < effectiveXMin || item.xEnd > effectiveXMax)) {
748
+ throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
749
+ `Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has xEnd value ${item.xEnd}, ` +
750
+ `which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
751
+ }
752
+ // Check grouped/stacked values
753
+ if (item.values && item.values.length > 0) {
754
+ item.values.forEach((seg, segIndex) => {
755
+ if (seg.value < effectiveXMin || seg.value > effectiveXMax) {
756
+ throw new Error(`Horizontal Bar Chart Error: Data value out of X-axis bounds.\n` +
757
+ `Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" segment ${segIndex} has value ${seg.value}, ` +
758
+ `which exceeds the X-axis range [${effectiveXMin}, ${effectiveXMax}].`);
759
+ }
760
+ });
761
+ }
762
+ });
763
+ }
764
+ if (hasExplicitYRange || yAxisCustomValues) {
765
+ const effectiveYMin = yAxisCustomValues ? Math.min(...yAxisCustomValues) : yAxisRange.min;
766
+ const effectiveYMax = yAxisCustomValues ? Math.max(...yAxisCustomValues) : yAxisRange.max;
767
+ data.forEach((item, itemIndex) => {
768
+ // Check yStart and yEnd (Y-axis for horizontal bars)
769
+ if (item.yStart !== undefined && (item.yStart < effectiveYMin || item.yStart > effectiveYMax)) {
770
+ throw new Error(`Horizontal Bar Chart Error: Data value out of Y-axis bounds.\n` +
771
+ `Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has yStart value ${item.yStart}, ` +
772
+ `which exceeds the Y-axis range [${effectiveYMin}, ${effectiveYMax}].`);
773
+ }
774
+ if (item.yEnd !== undefined && (item.yEnd < effectiveYMin || item.yEnd > effectiveYMax)) {
775
+ throw new Error(`Horizontal Bar Chart Error: Data value out of Y-axis bounds.\n` +
776
+ `Bar ${itemIndex} "${item.label || `at index ${itemIndex}`}" has yEnd value ${item.yEnd}, ` +
777
+ `which exceeds the Y-axis range [${effectiveYMin}, ${effectiveYMax}].`);
778
+ }
779
+ });
780
+ }
781
+ // Legend dimensions already calculated above, no need to recalculate
782
+ // Calculate adjusted dimensions (needed before creating canvas)
783
+ const adjustedWidth = width + extraWidth;
784
+ const adjustedHeight = baseHeight + extraHeight;
785
+ // Create canvas
786
+ const canvas = (0, canvas_1.createCanvas)(adjustedWidth, adjustedHeight);
787
+ const ctx = canvas.getContext('2d');
788
+ // Fill background (gradient, image, or color)
789
+ if (backgroundImage) {
790
+ try {
791
+ const bgImage = await (0, canvas_1.loadImage)(backgroundImage);
792
+ ctx.drawImage(bgImage, 0, 0, adjustedWidth, adjustedHeight);
793
+ }
794
+ catch (error) {
795
+ console.warn(`Failed to load background image: ${backgroundImage}`, error);
796
+ // Fallback to gradient or color if image fails to load
797
+ fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
798
+ x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
799
+ });
800
+ ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
801
+ }
802
+ }
803
+ else {
804
+ fillWithGradientOrColor(ctx, backgroundGradient, backgroundColor, backgroundColor, {
805
+ x: 0, y: 0, w: adjustedWidth, h: adjustedHeight
806
+ });
807
+ ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
808
+ }
809
+ // Calculate axis positions
810
+ const titleHeight = chartTitle ? chartTitleFontSize + 30 : 0;
811
+ const axisLabelHeight = (xAxisLabel || yAxisLabel) ? axisLabelFontSize + 20 : 0;
812
+ // Adjust chart area based on legend position
813
+ // Note: adjustedWidth and adjustedHeight are already calculated above (before canvas creation)
814
+ let chartAreaLeft = paddingLeft;
815
+ let chartAreaRight = width - paddingRight;
816
+ let chartAreaTop = paddingTop + titleHeight;
817
+ let chartAreaBottom = adjustedHeight - paddingBottom;
818
+ if (showLegend && legend && legend.length > 0) {
819
+ const legendSpacing = options.legend?.spacing ?? 20;
820
+ if (legendPosition === 'left') {
821
+ // Calculate actual Y-axis label width (category labels or numeric values)
822
+ let actualYAxisLabelWidth = 80; // Default estimate
823
+ const tempCanvas = (0, canvas_1.createCanvas)(1, 1);
824
+ const tempCtx = tempCanvas.getContext('2d');
825
+ if (tempCtx) {
826
+ // Check if bar labels are positioned on the left (they act as Y-axis labels)
827
+ const barLabelFontSize = options.labels?.barLabelDefaults?.fontSize ?? 14;
828
+ tempCtx.font = `${barLabelFontSize}px Arial`;
829
+ // Check if bar labels are on the left side
830
+ const hasLeftLabels = barLabelPosition === 'left' ||
831
+ data.some(item => (item.labelPosition ?? barLabelPosition) === 'left');
832
+ if (hasLeftLabels && showBarLabels) {
833
+ // Measure category labels (bar labels) - these are the Y-axis labels
834
+ data.forEach(d => {
835
+ const labelWidth = tempCtx.measureText(d.label).width;
836
+ actualYAxisLabelWidth = Math.max(actualYAxisLabelWidth, labelWidth);
837
+ });
838
+ // Add padding: 5px (label offset from originX) + 10px (spacing) = 15px total
839
+ actualYAxisLabelWidth += 15;
840
+ }
841
+ else {
842
+ // No left labels, but might have Y-axis numeric ticks
843
+ tempCtx.font = `${tickFontSize}px Arial`;
844
+ // Estimate for numeric Y-axis values if custom values are provided
845
+ actualYAxisLabelWidth = 60; // Default for numeric values
846
+ // Add padding: 10px (label offset) + 5px (tick) + 15px (spacing) = 30px total
847
+ actualYAxisLabelWidth += 30;
848
+ }
849
+ }
850
+ // Position chart area to leave room for legend + Y-axis labels
851
+ chartAreaLeft = paddingLeft + legendWidth + legendSpacing + actualYAxisLabelWidth;
852
+ chartAreaRight = width - paddingRight;
853
+ }
854
+ else if (legendPosition === 'right') {
855
+ chartAreaLeft = paddingLeft;
856
+ chartAreaRight = width - paddingRight;
857
+ }
858
+ else if (legendPosition === 'top') {
859
+ chartAreaTop = paddingTop + titleHeight + legendHeight + legendSpacing + minLegendSpacing;
860
+ chartAreaBottom = adjustedHeight - paddingBottom;
861
+ }
862
+ else if (legendPosition === 'bottom') {
863
+ chartAreaTop = paddingTop + titleHeight;
864
+ chartAreaBottom = adjustedHeight - paddingBottom;
865
+ }
866
+ }
867
+ const originX = chartAreaLeft;
868
+ // Use adjustedHeight for originY calculation to account for legend space
869
+ const originY = adjustedHeight - paddingBottom - axisLabelHeight;
870
+ const axisEndY = chartAreaTop;
871
+ const axisEndX = chartAreaRight;
872
+ // Draw chart title
873
+ if (chartTitle) {
874
+ ctx.save();
875
+ ctx.textAlign = 'center';
876
+ ctx.textBaseline = 'top';
877
+ // Title positioned with proper spacing from top
878
+ const titleY = paddingTop + 10;
879
+ const titleX = adjustedWidth / 2;
880
+ await renderEnhancedText(ctx, chartTitle, titleX, titleY, options.labels?.title?.textStyle, chartTitleFontSize, options.labels?.title?.color, options.labels?.title?.gradient);
881
+ ctx.restore();
882
+ }
883
+ // Set axis style
884
+ ctx.strokeStyle = axisColor;
885
+ ctx.fillStyle = axisColor;
886
+ ctx.lineWidth = axisWidth;
887
+ ctx.lineCap = 'round';
888
+ // X-axis will be drawn after calculating zero line
889
+ // Draw Y-axis (vertical - category axis)
890
+ ctx.beginPath();
891
+ ctx.moveTo(originX, originY);
892
+ ctx.lineTo(originX, axisEndY);
893
+ ctx.stroke();
894
+ // Draw Y-axis arrow
895
+ drawArrow(ctx, originX, axisEndY, -Math.PI / 2, arrowSize); // Y-axis arrow (up)
896
+ // Calculate X-axis step
897
+ const xStep = xAxisRange?.step ?? Math.ceil((xMax - xMin) / 10);
898
+ // Calculate Y-axis step
899
+ const yStep = yAxisRange?.step ?? 1;
900
+ // Calculate chart area dimensions (needed for baseline calculation)
901
+ const chartAreaWidth = axisEndX - originX;
902
+ // Calculate baseline position for X-axis (custom baseline value, default is 0)
903
+ const baselineX = originX + ((baseline - xMin) / (xMax - xMin)) * chartAreaWidth;
904
+ // Draw X-axis at baseline position (horizontal line at originY)
905
+ ctx.beginPath();
906
+ ctx.moveTo(originX, originY);
907
+ ctx.lineTo(axisEndX, originY);
908
+ ctx.stroke();
909
+ // Draw X-axis arrow
910
+ drawArrow(ctx, axisEndX, originY, 0, arrowSize);
911
+ // Draw X-axis ticks and labels at baseline position
912
+ drawXAxisTicks(ctx, originX, originY, axisEndX, xMin, xMax, xStep, tickFontSize, xAxisCustomValues, xAxisValueSpacing);
913
+ // Draw Y-axis ticks and labels (with values/range support)
914
+ drawYAxisTicks(ctx, originX, originY, axisEndY, yMin, yMax, yStep, tickFontSize, yAxisCustomValues, yAxisValueSpacing);
915
+ // Draw axis labels
916
+ if (xAxisLabel) {
917
+ ctx.save();
918
+ ctx.fillStyle = axisLabelColor;
919
+ ctx.font = `${axisLabelFontSize}px Arial`;
920
+ ctx.textAlign = 'center';
921
+ ctx.textBaseline = 'top';
922
+ ctx.fillText(xAxisLabel, (originX + axisEndX) / 2, originY + 25);
923
+ ctx.restore();
924
+ }
925
+ if (yAxisLabel) {
926
+ // Check if bar labels are on the left side - if so, position Y-axis label further left
927
+ let maxBarLabelWidth = 0;
928
+ if (showBarLabels) {
929
+ // Check if default position or any bar has labels on the left
930
+ const hasLeftLabels = barLabelPosition === 'left' ||
931
+ data.some(item => (item.labelPosition ?? barLabelPosition) === 'left');
932
+ if (hasLeftLabels) {
933
+ // Calculate maximum width of bar labels
934
+ ctx.save();
935
+ ctx.font = `${axisLabelFontSize}px Arial`;
936
+ data.forEach(item => {
937
+ const currentLabelPosition = item.labelPosition ?? barLabelPosition;
938
+ if (currentLabelPosition === 'left') {
939
+ const labelWidth = ctx.measureText(item.label).width;
940
+ maxBarLabelWidth = Math.max(maxBarLabelWidth, labelWidth);
941
+ }
942
+ });
943
+ ctx.restore();
944
+ }
945
+ }
946
+ ctx.save();
947
+ ctx.fillStyle = axisLabelColor;
948
+ ctx.font = `${axisLabelFontSize}px Arial`;
949
+ ctx.textAlign = 'center';
950
+ ctx.textBaseline = 'bottom';
951
+ // Position Y-axis label further left if bar labels are on the left
952
+ // Add extra spacing (20px) after the bar labels
953
+ const labelX = originX - maxBarLabelWidth - 20 - 30;
954
+ const labelY = (originY + axisEndY) / 2;
955
+ ctx.translate(labelX, labelY);
956
+ ctx.rotate(-Math.PI / 2);
957
+ ctx.fillText(yAxisLabel, 0, 0);
958
+ ctx.restore();
959
+ }
960
+ // Draw grid lines if enabled
961
+ if (showGrid) {
962
+ drawGrid(ctx, originX, originY, axisEndX, axisEndY, xMin, xMax, xStep, yMin, yMax, yStep, yAxisCustomValues, xAxisCustomValues, gridColor, gridWidth);
963
+ }
964
+ // Draw legend if provided - positioned based on legendPosition option
965
+ if (showLegend && legend && legend.length > 0) {
966
+ const legendSpacing = options.legend?.spacing ?? 20;
967
+ const legendFontSize = options.legend?.fontSize ?? 16;
968
+ const legendTextColor = options.legend?.textColor;
969
+ const legendBorderColor = options.legend?.borderColor;
970
+ const legendBgColor = options.legend?.backgroundColor;
971
+ const legendPadding = options.legend?.padding;
972
+ const legendMaxWidth = options.legend?.maxWidth;
973
+ const legendWrapText = options.legend?.wrapText !== false;
974
+ // Calculate legend position based on legendPosition option
975
+ let legendX, legendY;
976
+ const chartAreaHeight = originY - axisEndY;
977
+ const chartAreaWidth = axisEndX - originX;
978
+ switch (legendPosition) {
979
+ case 'top':
980
+ legendX = (adjustedWidth - legendWidth) / 2; // Centered horizontally
981
+ legendY = paddingTop + titleHeight + minLegendSpacing;
982
+ break;
983
+ case 'bottom':
984
+ legendX = (adjustedWidth - legendWidth) / 2; // Centered horizontally
985
+ legendY = adjustedHeight - paddingBottom - legendHeight - minLegendSpacing;
986
+ break;
987
+ case 'left':
988
+ // Position legend at the very left edge to make maximum room for Y-axis labels
989
+ // The chart area already accounts for legend width + label width, so position legend at leftmost
990
+ legendX = paddingLeft;
991
+ legendY = axisEndY + (chartAreaHeight - legendHeight) / 2; // Vertically centered in chart area
992
+ break;
993
+ case 'right':
994
+ default:
995
+ legendX = axisEndX + minLegendSpacing;
996
+ legendY = axisEndY + (chartAreaHeight - legendHeight) / 2; // Vertically centered in chart area
997
+ break;
998
+ }
999
+ await drawLegendAtPosition(ctx, legend, legendX, legendY, legendFontSize, legendBgColor || backgroundColor, legendTextColor, legendBorderColor, legendPadding, legendMaxWidth, legendWrapText, options.legend?.backgroundGradient, options.legend?.textGradient, options.legend?.textStyle);
1000
+ }
1001
+ // Calculate chart area dimensions (Y-axis area for bars)
1002
+ // chartAreaWidth and baselineX already calculated above when drawing X-axis
1003
+ const chartAreaHeight = originY - axisEndY;
1004
+ // Calculate bar dimensions to fit within Y-axis bounds (between axisEndY and originY)
1005
+ const calculatedBarSpacing = barSpacing ?? 15;
1006
+ const totalSpacing = (data.length - 1) * calculatedBarSpacing;
1007
+ const availableHeight = chartAreaHeight - totalSpacing;
1008
+ const calculatedBarHeight = Math.max(minBarHeight, availableHeight / data.length);
1009
+ const labelsToDraw = [];
1010
+ // Track value label positions per bar (for adjusting bar label positions)
1011
+ const valueLabelPositions = new Map();
1012
+ // First pass: Draw all bars (no labels)
1013
+ data.forEach((item, index) => {
1014
+ // Calculate bar Y position - start from axisEndY (top) and space bars downward
1015
+ // First bar starts after spacing, each subsequent bar: previous position + bar height + spacing
1016
+ const barY = axisEndY + (index * (calculatedBarHeight + calculatedBarSpacing)) + calculatedBarSpacing;
1017
+ const barCenterY = barY + calculatedBarHeight / 2;
1018
+ // Ensure bar stays within Y-axis bounds (between axisEndY and originY)
1019
+ if (barY + calculatedBarHeight > originY) {
1020
+ // Bar would exceed Y-axis bottom - skip it to prevent overflow
1021
+ return;
1022
+ }
1023
+ // Ensure bar doesn't exceed Y-axis bounds
1024
+ if (barY + calculatedBarHeight > originY) {
1025
+ // Adjust if bar would go below originY (Y-axis bottom)
1026
+ return; // Skip this bar if it doesn't fit
1027
+ }
1028
+ // Calculate bar position and dimensions for label positioning (used for all chart types)
1029
+ let barX, barEndX, barLength;
1030
+ // Handle grouped/stacked/lollipop vs standard charts
1031
+ if ((chartType === 'grouped' || chartType === 'stacked' || chartType === 'lollipop') && item.values && item.values.length > 0) {
1032
+ // Grouped or stacked chart
1033
+ const segments = item.values;
1034
+ const numSegments = segments.length;
1035
+ if (chartType === 'grouped') {
1036
+ // Grouped: bars side-by-side (vertically stacked in horizontal chart)
1037
+ const segmentHeight = (calculatedBarHeight - (groupSpacing * (numSegments - 1))) / numSegments;
1038
+ // Calculate overall bar bounds for label positioning (use max segment)
1039
+ const maxSegment = segments.reduce((max, seg) => seg.value > max.value ? seg : max, segments[0]);
1040
+ if (item.xStart !== undefined || item.xEnd !== undefined) {
1041
+ const startValue = item.xStart ?? xMin;
1042
+ const endValue = item.xEnd ?? maxSegment.value;
1043
+ const startRatio = (startValue - xMin) / (xMax - xMin);
1044
+ const endRatio = (endValue - xMin) / (xMax - xMin);
1045
+ barX = originX + startRatio * chartAreaWidth;
1046
+ barEndX = originX + endRatio * chartAreaWidth;
1047
+ }
1048
+ else {
1049
+ // Calculate based on positive/negative
1050
+ if (maxSegment.value >= 0) {
1051
+ const positiveRatio = (maxSegment.value - 0) / (xMax - xMin);
1052
+ barX = baselineX;
1053
+ barEndX = baselineX + positiveRatio * chartAreaWidth;
1054
+ }
1055
+ else {
1056
+ const negativeRatio = (baseline - maxSegment.value) / (xMax - xMin);
1057
+ barX = baselineX - negativeRatio * chartAreaWidth;
1058
+ barEndX = baselineX;
1059
+ }
1060
+ }
1061
+ barLength = Math.abs(barEndX - barX);
1062
+ segments.forEach((segment, segIndex) => {
1063
+ const segY = barY + (segIndex * (segmentHeight + groupSpacing));
1064
+ const segCenterY = segY + segmentHeight / 2;
1065
+ // Calculate segment bar position and length
1066
+ let segBarX, segBarEndX;
1067
+ if (item.xStart !== undefined || item.xEnd !== undefined) {
1068
+ const startValue = item.xStart ?? xMin;
1069
+ const endValue = item.xEnd ?? segment.value;
1070
+ const startRatio = (startValue - xMin) / (xMax - xMin);
1071
+ const endRatio = (endValue - xMin) / (xMax - xMin);
1072
+ segBarX = originX + startRatio * chartAreaWidth;
1073
+ segBarEndX = originX + endRatio * chartAreaWidth;
1074
+ }
1075
+ else {
1076
+ // Calculate bar position based on positive/negative value
1077
+ if (segment.value >= baseline) {
1078
+ const positiveRatio = (segment.value - baseline) / (xMax - xMin);
1079
+ segBarX = baselineX;
1080
+ segBarEndX = baselineX + positiveRatio * chartAreaWidth;
1081
+ }
1082
+ else {
1083
+ const negativeRatio = (baseline - segment.value) / (xMax - xMin);
1084
+ segBarX = baselineX - negativeRatio * chartAreaWidth;
1085
+ segBarEndX = baselineX;
1086
+ }
1087
+ }
1088
+ const segBarLength = Math.abs(segBarEndX - segBarX);
1089
+ // Draw segment bar with gradient or color
1090
+ ctx.beginPath();
1091
+ ctx.rect(segBarX, segY, segBarLength, segmentHeight);
1092
+ fillWithGradientOrColor(ctx, segment.gradient || item.gradient, segment.color || item.color || '#4A90E2', '#4A90E2', { x: segBarX, y: segY, w: segBarLength, h: segmentHeight });
1093
+ ctx.fill();
1094
+ // Store value label for later drawing
1095
+ const shouldShowValue = segment.showValue !== undefined ? segment.showValue : showValues;
1096
+ if (shouldShowValue) {
1097
+ labelsToDraw.push({
1098
+ type: 'value',
1099
+ text: segment.value.toString(),
1100
+ x: segment.value >= baseline ? segBarEndX + 5 : segBarX - 5,
1101
+ y: segCenterY,
1102
+ align: segment.value >= baseline ? 'left' : 'right',
1103
+ baseline: 'middle',
1104
+ color: segment.valueColor || valueColor,
1105
+ fontSize: valueFontSize
1106
+ });
1107
+ }
1108
+ });
1109
+ }
1110
+ else {
1111
+ // Stacked: bars on top of each other (horizontally stacked in horizontal chart)
1112
+ let accumulatedLength = 0;
1113
+ segments.forEach((segment, segIndex) => {
1114
+ // For stacked, separate positive and negative segments
1115
+ let segmentLength;
1116
+ let segBarX;
1117
+ if (segment.value >= baseline) {
1118
+ const positiveRatio = (segment.value - baseline) / (xMax - xMin);
1119
+ segmentLength = positiveRatio * chartAreaWidth;
1120
+ segBarX = baselineX + accumulatedLength;
1121
+ }
1122
+ else {
1123
+ const negativeRatio = (baseline - segment.value) / (xMax - xMin);
1124
+ segmentLength = negativeRatio * chartAreaWidth;
1125
+ segBarX = baselineX - accumulatedLength - segmentLength;
1126
+ }
1127
+ // Draw segment bar
1128
+ ctx.fillStyle = segment.color || item.color || '#4A90E2';
1129
+ ctx.fillRect(segBarX, barY, segmentLength, calculatedBarHeight);
1130
+ // Store value label for later drawing
1131
+ const shouldShowValue = segment.showValue !== undefined ? segment.showValue : showValues;
1132
+ if (shouldShowValue && segmentLength > valueFontSize + 10) {
1133
+ labelsToDraw.push({
1134
+ type: 'value',
1135
+ text: segment.value.toString(),
1136
+ x: segBarX + segmentLength / 2,
1137
+ y: barCenterY,
1138
+ align: 'center',
1139
+ baseline: 'middle',
1140
+ color: segment.valueColor || valueColor,
1141
+ fontSize: valueFontSize
1142
+ });
1143
+ }
1144
+ accumulatedLength += segmentLength;
1145
+ });
1146
+ // Calculate overall bar bounds for label positioning
1147
+ barX = originX;
1148
+ barEndX = originX + accumulatedLength;
1149
+ barLength = accumulatedLength;
1150
+ // Store total value label for later drawing
1151
+ const totalValue = segments.reduce((sum, seg) => sum + seg.value, 0);
1152
+ const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
1153
+ if (shouldShowValue) {
1154
+ // Calculate total position
1155
+ const totalPositive = segments.filter(s => s.value >= 0).reduce((sum, s) => sum + s.value, 0);
1156
+ const totalNegative = segments.filter(s => s.value < 0).reduce((sum, s) => sum + Math.abs(s.value), 0);
1157
+ const totalPositiveLength = (totalPositive / (xMax - xMin)) * chartAreaWidth;
1158
+ const totalNegativeLength = (totalNegative / (xMax - xMin)) * chartAreaWidth;
1159
+ const totalX = totalValue >= 0
1160
+ ? baselineX + totalPositiveLength + 5
1161
+ : baselineX - totalNegativeLength - 5;
1162
+ labelsToDraw.push({
1163
+ type: 'value',
1164
+ text: totalValue.toString(),
1165
+ x: totalX,
1166
+ y: barCenterY,
1167
+ align: totalValue >= 0 ? 'left' : 'right',
1168
+ baseline: 'middle',
1169
+ color: item.valueColor || valueColor,
1170
+ fontSize: valueFontSize
1171
+ });
1172
+ }
1173
+ }
1174
+ }
1175
+ else if (chartType === 'lollipop') {
1176
+ // Lollipop chart: line with dot at end (horizontal)
1177
+ const value = item.value ?? baseline;
1178
+ // Calculate value X position
1179
+ let valueX;
1180
+ if (value >= baseline) {
1181
+ // Value to the right of baseline
1182
+ const positiveRatio = (value - baseline) / (xMax - xMin);
1183
+ valueX = baselineX + positiveRatio * chartAreaWidth;
1184
+ }
1185
+ else {
1186
+ // Value to the left of baseline
1187
+ const negativeRatio = (baseline - value) / (xMax - xMin);
1188
+ valueX = baselineX - negativeRatio * chartAreaWidth;
1189
+ }
1190
+ // Draw horizontal line from baseline to value position
1191
+ ctx.save();
1192
+ ctx.strokeStyle = item.color || '#4A90E2';
1193
+ ctx.lineWidth = lollipopLineWidth;
1194
+ ctx.beginPath();
1195
+ ctx.moveTo(baselineX, barCenterY);
1196
+ ctx.lineTo(valueX, barCenterY);
1197
+ ctx.stroke();
1198
+ // Draw dot/circle at value position
1199
+ ctx.fillStyle = item.color || '#4A90E2';
1200
+ ctx.beginPath();
1201
+ ctx.arc(valueX, barCenterY, lollipopDotSize / 2, 0, Math.PI * 2);
1202
+ ctx.fill();
1203
+ // Draw dot border for better visibility
1204
+ ctx.strokeStyle = item.color || '#4A90E2';
1205
+ ctx.lineWidth = 1;
1206
+ ctx.stroke();
1207
+ ctx.restore();
1208
+ // Store value label for later drawing
1209
+ const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
1210
+ if (shouldShowValue) {
1211
+ // Store value label position for this bar (for adjusting bar label position)
1212
+ if (value >= baseline) {
1213
+ valueLabelPositions.set(index, { x: valueX + lollipopDotSize / 2 + 5, fontSize: valueFontSize, align: 'left' });
1214
+ }
1215
+ labelsToDraw.push({
1216
+ type: 'value',
1217
+ text: value.toString(),
1218
+ x: value >= baseline ? valueX + lollipopDotSize / 2 + 5 : valueX - lollipopDotSize / 2 - 5,
1219
+ y: barCenterY,
1220
+ align: value >= baseline ? 'left' : 'right',
1221
+ baseline: 'middle',
1222
+ color: item.valueColor || valueColor,
1223
+ fontSize: valueFontSize
1224
+ });
1225
+ }
1226
+ // Set bar bounds for label positioning
1227
+ barX = baselineX;
1228
+ barEndX = valueX;
1229
+ barLength = Math.abs(barEndX - barX);
1230
+ }
1231
+ else {
1232
+ // Standard chart: single bar
1233
+ // Calculate bar position and length
1234
+ // If xStart/xEnd are provided, use them for bar range; otherwise use value
1235
+ if (item.xStart !== undefined || item.xEnd !== undefined) {
1236
+ const startValue = item.xStart ?? xMin;
1237
+ const endValue = item.xEnd ?? (item.value ?? 0);
1238
+ const startRatio = (startValue - xMin) / (xMax - xMin);
1239
+ const endRatio = (endValue - xMin) / (xMax - xMin);
1240
+ barX = originX + startRatio * chartAreaWidth;
1241
+ barEndX = originX + endRatio * chartAreaWidth;
1242
+ }
1243
+ else {
1244
+ // Use value as end position, handle relative to baseline
1245
+ const value = item.value ?? baseline;
1246
+ if (value >= baseline) {
1247
+ const positiveRatio = (value - baseline) / (xMax - xMin);
1248
+ barX = baselineX;
1249
+ barEndX = baselineX + positiveRatio * chartAreaWidth;
1250
+ }
1251
+ else {
1252
+ const negativeRatio = (baseline - value) / (xMax - xMin);
1253
+ barX = baselineX - negativeRatio * chartAreaWidth;
1254
+ barEndX = baselineX;
1255
+ }
1256
+ }
1257
+ barLength = barEndX - barX;
1258
+ // Draw horizontal bar
1259
+ ctx.beginPath();
1260
+ ctx.rect(barX, barY, barLength, calculatedBarHeight);
1261
+ fillWithGradientOrColor(ctx, item.gradient, item.color || '#4A90E2', '#4A90E2', { x: barX, y: barY, w: barLength, h: calculatedBarHeight });
1262
+ ctx.fill();
1263
+ // Store value label for later drawing
1264
+ const shouldShowValue = item.showValue !== undefined ? item.showValue : showValues;
1265
+ if (shouldShowValue) {
1266
+ const value = item.value ?? baseline;
1267
+ const valueLabelX = value >= baseline ? barEndX + 5 : barX - 5;
1268
+ const valueLabelAlign = value >= baseline ? 'left' : 'right';
1269
+ // Store value label position for this bar (for adjusting bar label position)
1270
+ if (value >= baseline) {
1271
+ valueLabelPositions.set(index, { x: valueLabelX, fontSize: valueFontSize, align: valueLabelAlign });
1272
+ }
1273
+ labelsToDraw.push({
1274
+ type: 'value',
1275
+ text: value.toString(),
1276
+ x: valueLabelX,
1277
+ y: barCenterY,
1278
+ align: valueLabelAlign,
1279
+ baseline: 'middle',
1280
+ color: item.valueColor || valueColor,
1281
+ fontSize: valueFontSize
1282
+ });
1283
+ }
1284
+ }
1285
+ // Store bar label information for later drawing
1286
+ if (showBarLabels) {
1287
+ let labelX, labelY;
1288
+ let textAlign = 'right';
1289
+ let textBaseline = 'middle';
1290
+ const currentLabelPosition = item.labelPosition ?? barLabelPosition;
1291
+ switch (currentLabelPosition) {
1292
+ case 'left':
1293
+ labelX = originX - 5;
1294
+ labelY = barCenterY;
1295
+ textAlign = 'right';
1296
+ textBaseline = 'middle';
1297
+ break;
1298
+ case 'right':
1299
+ labelX = barEndX + 5;
1300
+ labelY = barCenterY;
1301
+ // Check if there's a value label at the right - if so, position bar label to the right of it
1302
+ const valueLabelInfo = valueLabelPositions.get(index);
1303
+ if (valueLabelInfo && valueLabelInfo.align === 'left') {
1304
+ // Value label is at right, so position bar label to the right of it
1305
+ // Calculate spacing: value label width + gap
1306
+ ctx.save();
1307
+ ctx.font = `${valueLabelInfo.fontSize}px Arial`;
1308
+ const valueLabelWidth = ctx.measureText((item.value ?? baseline).toString()).width;
1309
+ ctx.restore();
1310
+ const spacing = 5; // Gap between value and bar label
1311
+ labelX = valueLabelInfo.x + valueLabelWidth + spacing;
1312
+ }
1313
+ else {
1314
+ labelX = barEndX + 5;
1315
+ }
1316
+ textAlign = 'left';
1317
+ textBaseline = 'middle';
1318
+ break;
1319
+ case 'top':
1320
+ labelX = barX + barLength / 2;
1321
+ // Check if there's a value label - for horizontal charts, value labels are at the end (right side)
1322
+ // So 'top' bar label won't conflict with value labels (they're on different axes)
1323
+ // But we still need to check if value is shown and adjust if needed
1324
+ // For horizontal charts, 'top' means above the bar, value labels are at the end
1325
+ // So no conflict, but if we want to be safe, we can check
1326
+ labelY = barY - 5;
1327
+ textAlign = 'center';
1328
+ textBaseline = 'bottom';
1329
+ break;
1330
+ case 'bottom':
1331
+ labelX = barX + barLength / 2;
1332
+ labelY = barY + calculatedBarHeight + 5;
1333
+ textAlign = 'center';
1334
+ textBaseline = 'top';
1335
+ break;
1336
+ case 'inside':
1337
+ labelX = barX + barLength / 2;
1338
+ labelY = barCenterY;
1339
+ textAlign = 'center';
1340
+ textBaseline = 'middle';
1341
+ break;
1342
+ default:
1343
+ labelX = originX - 5;
1344
+ labelY = barCenterY;
1345
+ textAlign = 'right';
1346
+ textBaseline = 'middle';
1347
+ }
1348
+ // Calculate label color (for 'inside' position, check if bar is dark)
1349
+ let labelColor = item.labelColor || '#000000';
1350
+ if (currentLabelPosition === 'inside') {
1351
+ const barColor = item.color || '#4A90E2';
1352
+ const isDark = barColor === '#000000' || barColor.toLowerCase().includes('dark') ||
1353
+ (barColor.startsWith('#') && parseInt(barColor.slice(1, 3), 16) < 128);
1354
+ labelColor = isDark ? '#FFFFFF' : (item.labelColor || '#000000');
1355
+ }
1356
+ labelsToDraw.push({
1357
+ type: 'bar',
1358
+ text: item.label,
1359
+ x: labelX,
1360
+ y: labelY,
1361
+ align: textAlign,
1362
+ baseline: textBaseline,
1363
+ color: labelColor,
1364
+ fontSize: axisLabelFontSize
1365
+ });
1366
+ }
1367
+ });
1368
+ // Second pass: Draw all labels (values and bar labels) on top of everything
1369
+ for (const label of labelsToDraw) {
1370
+ ctx.save();
1371
+ ctx.textAlign = label.align;
1372
+ ctx.textBaseline = label.baseline;
1373
+ // Determine text style and gradient based on label type
1374
+ let textStyle;
1375
+ let textGradient;
1376
+ if (label.type === 'bar') {
1377
+ textStyle = options.labels?.barLabelDefaults?.textStyle || label.textStyle;
1378
+ textGradient = options.labels?.barLabelDefaults?.gradient || label.gradient;
1379
+ }
1380
+ else if (label.type === 'value') {
1381
+ textStyle = options.labels?.valueLabelDefaults?.textStyle || label.textStyle;
1382
+ textGradient = options.labels?.valueLabelDefaults?.gradient || label.gradient;
1383
+ }
1384
+ await renderEnhancedText(ctx, label.text, label.x, label.y, textStyle, label.fontSize, label.color, textGradient);
1385
+ ctx.restore();
1386
+ }
1387
+ return canvas.toBuffer('image/png');
1388
+ }
1389
+ //# sourceMappingURL=horizontalbarchart.js.map