photon-grid-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (555) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +0 -0
  3. package/cell-selection/active-grid-registry.d.ts +37 -0
  4. package/cell-selection/active-grid-registry.d.ts.map +1 -0
  5. package/cell-selection/active-grid-registry.js +25 -0
  6. package/cell-selection/active-grid-registry.js.map +1 -0
  7. package/cell-selection/cell-selection-engine.d.ts +336 -0
  8. package/cell-selection/cell-selection-engine.d.ts.map +1 -0
  9. package/cell-selection/cell-selection-engine.js +1534 -0
  10. package/cell-selection/cell-selection-engine.js.map +1 -0
  11. package/cell-selection/selection-range.d.ts +16 -0
  12. package/cell-selection/selection-range.d.ts.map +1 -0
  13. package/cell-selection/selection-range.js +48 -0
  14. package/cell-selection/selection-range.js.map +1 -0
  15. package/cell-selection/selection-renderer.d.ts +24 -0
  16. package/cell-selection/selection-renderer.d.ts.map +1 -0
  17. package/cell-selection/selection-renderer.js +107 -0
  18. package/cell-selection/selection-renderer.js.map +1 -0
  19. package/chart/chart-analyzer/aggregation-analyzer.d.ts +18 -0
  20. package/chart/chart-analyzer/aggregation-analyzer.d.ts.map +1 -0
  21. package/chart/chart-analyzer/aggregation-analyzer.js +63 -0
  22. package/chart/chart-analyzer/aggregation-analyzer.js.map +1 -0
  23. package/chart/chart-analyzer/bar-analyzer.d.ts +19 -0
  24. package/chart/chart-analyzer/bar-analyzer.d.ts.map +1 -0
  25. package/chart/chart-analyzer/bar-analyzer.js +56 -0
  26. package/chart/chart-analyzer/bar-analyzer.js.map +1 -0
  27. package/chart/chart-analyzer/bubble-analyzer.d.ts +16 -0
  28. package/chart/chart-analyzer/bubble-analyzer.d.ts.map +1 -0
  29. package/chart/chart-analyzer/bubble-analyzer.js +61 -0
  30. package/chart/chart-analyzer/bubble-analyzer.js.map +1 -0
  31. package/chart/chart-analyzer/cardinality-analyzer.d.ts +13 -0
  32. package/chart/chart-analyzer/cardinality-analyzer.d.ts.map +1 -0
  33. package/chart/chart-analyzer/cardinality-analyzer.js +28 -0
  34. package/chart/chart-analyzer/cardinality-analyzer.js.map +1 -0
  35. package/chart/chart-analyzer/dimension-analyzer.d.ts +12 -0
  36. package/chart/chart-analyzer/dimension-analyzer.d.ts.map +1 -0
  37. package/chart/chart-analyzer/dimension-analyzer.js +33 -0
  38. package/chart/chart-analyzer/dimension-analyzer.js.map +1 -0
  39. package/chart/chart-analyzer/histogram-analyzer.d.ts +11 -0
  40. package/chart/chart-analyzer/histogram-analyzer.d.ts.map +1 -0
  41. package/chart/chart-analyzer/histogram-analyzer.js +43 -0
  42. package/chart/chart-analyzer/histogram-analyzer.js.map +1 -0
  43. package/chart/chart-analyzer/index.d.ts +58 -0
  44. package/chart/chart-analyzer/index.d.ts.map +1 -0
  45. package/chart/chart-analyzer/index.js +130 -0
  46. package/chart/chart-analyzer/index.js.map +1 -0
  47. package/chart/chart-analyzer/line-analyzer.d.ts +23 -0
  48. package/chart/chart-analyzer/line-analyzer.d.ts.map +1 -0
  49. package/chart/chart-analyzer/line-analyzer.js +61 -0
  50. package/chart/chart-analyzer/line-analyzer.js.map +1 -0
  51. package/chart/chart-analyzer/measure-analyzer.d.ts +11 -0
  52. package/chart/chart-analyzer/measure-analyzer.d.ts.map +1 -0
  53. package/chart/chart-analyzer/measure-analyzer.js +44 -0
  54. package/chart/chart-analyzer/measure-analyzer.js.map +1 -0
  55. package/chart/chart-analyzer/pie-analyzer.d.ts +19 -0
  56. package/chart/chart-analyzer/pie-analyzer.d.ts.map +1 -0
  57. package/chart/chart-analyzer/pie-analyzer.js +64 -0
  58. package/chart/chart-analyzer/pie-analyzer.js.map +1 -0
  59. package/chart/chart-analyzer/recommendation-engine.d.ts +18 -0
  60. package/chart/chart-analyzer/recommendation-engine.d.ts.map +1 -0
  61. package/chart/chart-analyzer/recommendation-engine.js +82 -0
  62. package/chart/chart-analyzer/recommendation-engine.js.map +1 -0
  63. package/chart/chart-analyzer/scatter-analyzer.d.ts +16 -0
  64. package/chart/chart-analyzer/scatter-analyzer.d.ts.map +1 -0
  65. package/chart/chart-analyzer/scatter-analyzer.js +70 -0
  66. package/chart/chart-analyzer/scatter-analyzer.js.map +1 -0
  67. package/chart/chart-analyzer/types.d.ts +52 -0
  68. package/chart/chart-analyzer/types.d.ts.map +1 -0
  69. package/chart/chart-analyzer/types.js +10 -0
  70. package/chart/chart-analyzer/types.js.map +1 -0
  71. package/chart/chart-analyzer.d.ts +12 -0
  72. package/chart/chart-analyzer.d.ts.map +1 -0
  73. package/chart/chart-analyzer.js +77 -0
  74. package/chart/chart-analyzer.js.map +1 -0
  75. package/chart/chart-data-transformer.d.ts +26 -0
  76. package/chart/chart-data-transformer.d.ts.map +1 -0
  77. package/chart/chart-data-transformer.js +93 -0
  78. package/chart/chart-data-transformer.js.map +1 -0
  79. package/chart/chart-engine.d.ts +38 -0
  80. package/chart/chart-engine.d.ts.map +1 -0
  81. package/chart/chart-engine.js +96 -0
  82. package/chart/chart-engine.js.map +1 -0
  83. package/chart/chart-panel.d.ts +54 -0
  84. package/chart/chart-panel.d.ts.map +1 -0
  85. package/chart/chart-panel.js +350 -0
  86. package/chart/chart-panel.js.map +1 -0
  87. package/chart/chart-renderer.d.ts +93 -0
  88. package/chart/chart-renderer.d.ts.map +1 -0
  89. package/chart/chart-renderer.js +1330 -0
  90. package/chart/chart-renderer.js.map +1 -0
  91. package/chart/sparkline/sparkline-renderer.d.ts +269 -0
  92. package/chart/sparkline/sparkline-renderer.d.ts.map +1 -0
  93. package/chart/sparkline/sparkline-renderer.js +875 -0
  94. package/chart/sparkline/sparkline-renderer.js.map +1 -0
  95. package/chart/sparkline/sparkline.types.d.ts +214 -0
  96. package/chart/sparkline/sparkline.types.d.ts.map +1 -0
  97. package/chart/sparkline/sparkline.types.js +8 -0
  98. package/chart/sparkline/sparkline.types.js.map +1 -0
  99. package/column-groups/column-group-drag-handler.d.ts +169 -0
  100. package/column-groups/column-group-drag-handler.d.ts.map +1 -0
  101. package/column-groups/column-group-drag-handler.js +517 -0
  102. package/column-groups/column-group-drag-handler.js.map +1 -0
  103. package/column-groups/column-group-header-builder.d.ts +106 -0
  104. package/column-groups/column-group-header-builder.d.ts.map +1 -0
  105. package/column-groups/column-group-header-builder.js +311 -0
  106. package/column-groups/column-group-header-builder.js.map +1 -0
  107. package/column-groups/column-group-model.d.ts +248 -0
  108. package/column-groups/column-group-model.d.ts.map +1 -0
  109. package/column-groups/column-group-model.js +668 -0
  110. package/column-groups/column-group-model.js.map +1 -0
  111. package/column-groups/column-group-state-manager.d.ts +110 -0
  112. package/column-groups/column-group-state-manager.d.ts.map +1 -0
  113. package/column-groups/column-group-state-manager.js +153 -0
  114. package/column-groups/column-group-state-manager.js.map +1 -0
  115. package/column-groups/column-group.types.d.ts +250 -0
  116. package/column-groups/column-group.types.d.ts.map +1 -0
  117. package/column-groups/column-group.types.js +20 -0
  118. package/column-groups/column-group.types.js.map +1 -0
  119. package/column-groups/display-group-builder.d.ts +91 -0
  120. package/column-groups/display-group-builder.d.ts.map +1 -0
  121. package/column-groups/display-group-builder.js +225 -0
  122. package/column-groups/display-group-builder.js.map +1 -0
  123. package/column-groups/display-group-drag-handler.d.ts +205 -0
  124. package/column-groups/display-group-drag-handler.d.ts.map +1 -0
  125. package/column-groups/display-group-drag-handler.js +536 -0
  126. package/column-groups/display-group-drag-handler.js.map +1 -0
  127. package/column-groups/display-group-engine.d.ts +310 -0
  128. package/column-groups/display-group-engine.d.ts.map +1 -0
  129. package/column-groups/display-group-engine.js +549 -0
  130. package/column-groups/display-group-engine.js.map +1 -0
  131. package/column-groups/display-group-header-builder.d.ts +94 -0
  132. package/column-groups/display-group-header-builder.d.ts.map +1 -0
  133. package/column-groups/display-group-header-builder.js +224 -0
  134. package/column-groups/display-group-header-builder.js.map +1 -0
  135. package/column-groups/display-group.types.d.ts +140 -0
  136. package/column-groups/display-group.types.d.ts.map +1 -0
  137. package/column-groups/display-group.types.js +2 -0
  138. package/column-groups/display-group.types.js.map +1 -0
  139. package/column-groups/index.d.ts +25 -0
  140. package/column-groups/index.d.ts.map +1 -0
  141. package/column-groups/index.js +26 -0
  142. package/column-groups/index.js.map +1 -0
  143. package/column-groups/logical-group-registry.d.ts +77 -0
  144. package/column-groups/logical-group-registry.d.ts.map +1 -0
  145. package/column-groups/logical-group-registry.js +118 -0
  146. package/column-groups/logical-group-registry.js.map +1 -0
  147. package/core/column-model.d.ts +30 -0
  148. package/core/column-model.d.ts.map +1 -0
  149. package/core/column-model.js +203 -0
  150. package/core/column-model.js.map +1 -0
  151. package/core/grid-api.d.ts +187 -0
  152. package/core/grid-api.d.ts.map +1 -0
  153. package/core/grid-api.js +569 -0
  154. package/core/grid-api.js.map +1 -0
  155. package/core/grid-context.d.ts +61 -0
  156. package/core/grid-context.d.ts.map +1 -0
  157. package/core/grid-context.js +2 -0
  158. package/core/grid-context.js.map +1 -0
  159. package/core/grid-core.d.ts +49 -0
  160. package/core/grid-core.d.ts.map +1 -0
  161. package/core/grid-core.js +675 -0
  162. package/core/grid-core.js.map +1 -0
  163. package/core/grid-store.d.ts +62 -0
  164. package/core/grid-store.d.ts.map +1 -0
  165. package/core/grid-store.js +104 -0
  166. package/core/grid-store.js.map +1 -0
  167. package/core/row-model.d.ts +32 -0
  168. package/core/row-model.d.ts.map +1 -0
  169. package/core/row-model.js +150 -0
  170. package/core/row-model.js.map +1 -0
  171. package/drag-drop/drag-autoscroll.d.ts +14 -0
  172. package/drag-drop/drag-autoscroll.d.ts.map +1 -0
  173. package/drag-drop/drag-autoscroll.js +66 -0
  174. package/drag-drop/drag-autoscroll.js.map +1 -0
  175. package/drag-drop/drag-drop-engine.d.ts +55 -0
  176. package/drag-drop/drag-drop-engine.d.ts.map +1 -0
  177. package/drag-drop/drag-drop-engine.js +179 -0
  178. package/drag-drop/drag-drop-engine.js.map +1 -0
  179. package/drag-drop/drag-preview.d.ts +32 -0
  180. package/drag-drop/drag-preview.d.ts.map +1 -0
  181. package/drag-drop/drag-preview.js +74 -0
  182. package/drag-drop/drag-preview.js.map +1 -0
  183. package/engines/aggregation/aggregation-engine.d.ts +66 -0
  184. package/engines/aggregation/aggregation-engine.d.ts.map +1 -0
  185. package/engines/aggregation/aggregation-engine.js +149 -0
  186. package/engines/aggregation/aggregation-engine.js.map +1 -0
  187. package/engines/clipboard/clipboard-engine.d.ts +89 -0
  188. package/engines/clipboard/clipboard-engine.d.ts.map +1 -0
  189. package/engines/clipboard/clipboard-engine.js +323 -0
  190. package/engines/clipboard/clipboard-engine.js.map +1 -0
  191. package/engines/editing/cell-editor-engine.d.ts +62 -0
  192. package/engines/editing/cell-editor-engine.d.ts.map +1 -0
  193. package/engines/editing/cell-editor-engine.js +359 -0
  194. package/engines/editing/cell-editor-engine.js.map +1 -0
  195. package/engines/editing/custom-dropdown-editor.d.ts +99 -0
  196. package/engines/editing/custom-dropdown-editor.d.ts.map +1 -0
  197. package/engines/editing/custom-dropdown-editor.js +355 -0
  198. package/engines/editing/custom-dropdown-editor.js.map +1 -0
  199. package/engines/editing/value-parser.d.ts +12 -0
  200. package/engines/editing/value-parser.d.ts.map +1 -0
  201. package/engines/editing/value-parser.js +125 -0
  202. package/engines/editing/value-parser.js.map +1 -0
  203. package/engines/export/export-engine.d.ts +16 -0
  204. package/engines/export/export-engine.d.ts.map +1 -0
  205. package/engines/export/export-engine.js +99 -0
  206. package/engines/export/export-engine.js.map +1 -0
  207. package/engines/filter/filter-condition.d.ts +7 -0
  208. package/engines/filter/filter-condition.d.ts.map +1 -0
  209. package/engines/filter/filter-condition.js +126 -0
  210. package/engines/filter/filter-condition.js.map +1 -0
  211. package/engines/filter/filter-engine.d.ts +35 -0
  212. package/engines/filter/filter-engine.d.ts.map +1 -0
  213. package/engines/filter/filter-engine.js +150 -0
  214. package/engines/filter/filter-engine.js.map +1 -0
  215. package/engines/filter/filter-panel.d.ts +103 -0
  216. package/engines/filter/filter-panel.d.ts.map +1 -0
  217. package/engines/filter/filter-panel.js +645 -0
  218. package/engines/filter/filter-panel.js.map +1 -0
  219. package/engines/grouping/group-node.d.ts +21 -0
  220. package/engines/grouping/group-node.d.ts.map +1 -0
  221. package/engines/grouping/group-node.js +96 -0
  222. package/engines/grouping/group-node.js.map +1 -0
  223. package/engines/grouping/grouping-engine.d.ts +33 -0
  224. package/engines/grouping/grouping-engine.d.ts.map +1 -0
  225. package/engines/grouping/grouping-engine.js +118 -0
  226. package/engines/grouping/grouping-engine.js.map +1 -0
  227. package/engines/master-detail/master-detail-engine.d.ts +77 -0
  228. package/engines/master-detail/master-detail-engine.d.ts.map +1 -0
  229. package/engines/master-detail/master-detail-engine.js +199 -0
  230. package/engines/master-detail/master-detail-engine.js.map +1 -0
  231. package/engines/pagination/pagination-engine.d.ts +33 -0
  232. package/engines/pagination/pagination-engine.d.ts.map +1 -0
  233. package/engines/pagination/pagination-engine.js +110 -0
  234. package/engines/pagination/pagination-engine.js.map +1 -0
  235. package/engines/selection/row-selection-engine.d.ts +26 -0
  236. package/engines/selection/row-selection-engine.d.ts.map +1 -0
  237. package/engines/selection/row-selection-engine.js +145 -0
  238. package/engines/selection/row-selection-engine.js.map +1 -0
  239. package/engines/sort/sort-comparator.d.ts +43 -0
  240. package/engines/sort/sort-comparator.d.ts.map +1 -0
  241. package/engines/sort/sort-comparator.js +113 -0
  242. package/engines/sort/sort-comparator.js.map +1 -0
  243. package/engines/sort/sort-engine.d.ts +67 -0
  244. package/engines/sort/sort-engine.d.ts.map +1 -0
  245. package/engines/sort/sort-engine.js +178 -0
  246. package/engines/sort/sort-engine.js.map +1 -0
  247. package/engines/summary/summary-engine.d.ts +17 -0
  248. package/engines/summary/summary-engine.d.ts.map +1 -0
  249. package/engines/summary/summary-engine.js +72 -0
  250. package/engines/summary/summary-engine.js.map +1 -0
  251. package/engines/tree/tree-data-service.d.ts +117 -0
  252. package/engines/tree/tree-data-service.d.ts.map +1 -0
  253. package/engines/tree/tree-data-service.js +296 -0
  254. package/engines/tree/tree-data-service.js.map +1 -0
  255. package/engines/tree/tree-expansion-service.d.ts +35 -0
  256. package/engines/tree/tree-expansion-service.d.ts.map +1 -0
  257. package/engines/tree/tree-expansion-service.js +84 -0
  258. package/engines/tree/tree-expansion-service.js.map +1 -0
  259. package/engines/tree/tree-node.d.ts +40 -0
  260. package/engines/tree/tree-node.d.ts.map +1 -0
  261. package/engines/tree/tree-node.js +199 -0
  262. package/engines/tree/tree-node.js.map +1 -0
  263. package/engines/tree/tree-selection-service.d.ts +35 -0
  264. package/engines/tree/tree-selection-service.d.ts.map +1 -0
  265. package/engines/tree/tree-selection-service.js +72 -0
  266. package/engines/tree/tree-selection-service.js.map +1 -0
  267. package/engines/undo-redo/undo-redo-engine.d.ts +114 -0
  268. package/engines/undo-redo/undo-redo-engine.d.ts.map +1 -0
  269. package/engines/undo-redo/undo-redo-engine.js +165 -0
  270. package/engines/undo-redo/undo-redo-engine.js.map +1 -0
  271. package/event-bus/event-bus.d.ts +21 -0
  272. package/event-bus/event-bus.d.ts.map +1 -0
  273. package/event-bus/event-bus.js +106 -0
  274. package/event-bus/event-bus.js.map +1 -0
  275. package/icons/icon-registry.d.ts +19 -0
  276. package/icons/icon-registry.d.ts.map +1 -0
  277. package/icons/icon-registry.js +37 -0
  278. package/icons/icon-registry.js.map +1 -0
  279. package/icons/icon-renderer.d.ts +18 -0
  280. package/icons/icon-renderer.d.ts.map +1 -0
  281. package/icons/icon-renderer.js +75 -0
  282. package/icons/icon-renderer.js.map +1 -0
  283. package/icons/icon-sets/core-icons.d.ts +2 -0
  284. package/icons/icon-sets/core-icons.d.ts.map +1 -0
  285. package/icons/icon-sets/core-icons.js +48 -0
  286. package/icons/icon-sets/core-icons.js.map +1 -0
  287. package/index.d.ts +72 -0
  288. package/index.d.ts.map +1 -0
  289. package/index.js +54 -0
  290. package/index.js.map +1 -0
  291. package/package.json +64 -0
  292. package/photon-ai/builtins/filter.commands.d.ts +4 -0
  293. package/photon-ai/builtins/filter.commands.d.ts.map +1 -0
  294. package/photon-ai/builtins/filter.commands.js +189 -0
  295. package/photon-ai/builtins/filter.commands.js.map +1 -0
  296. package/photon-ai/builtins/grouping.commands.d.ts +4 -0
  297. package/photon-ai/builtins/grouping.commands.d.ts.map +1 -0
  298. package/photon-ai/builtins/grouping.commands.js +115 -0
  299. package/photon-ai/builtins/grouping.commands.js.map +1 -0
  300. package/photon-ai/builtins/index.d.ts +10 -0
  301. package/photon-ai/builtins/index.d.ts.map +1 -0
  302. package/photon-ai/builtins/index.js +26 -0
  303. package/photon-ai/builtins/index.js.map +1 -0
  304. package/photon-ai/builtins/info.commands.d.ts +4 -0
  305. package/photon-ai/builtins/info.commands.d.ts.map +1 -0
  306. package/photon-ai/builtins/info.commands.js +201 -0
  307. package/photon-ai/builtins/info.commands.js.map +1 -0
  308. package/photon-ai/builtins/move.commands.d.ts +4 -0
  309. package/photon-ai/builtins/move.commands.d.ts.map +1 -0
  310. package/photon-ai/builtins/move.commands.js +53 -0
  311. package/photon-ai/builtins/move.commands.js.map +1 -0
  312. package/photon-ai/builtins/pin.commands.d.ts +4 -0
  313. package/photon-ai/builtins/pin.commands.d.ts.map +1 -0
  314. package/photon-ai/builtins/pin.commands.js +106 -0
  315. package/photon-ai/builtins/pin.commands.js.map +1 -0
  316. package/photon-ai/builtins/selection.commands.d.ts +4 -0
  317. package/photon-ai/builtins/selection.commands.d.ts.map +1 -0
  318. package/photon-ai/builtins/selection.commands.js +102 -0
  319. package/photon-ai/builtins/selection.commands.js.map +1 -0
  320. package/photon-ai/builtins/sort.commands.d.ts +4 -0
  321. package/photon-ai/builtins/sort.commands.d.ts.map +1 -0
  322. package/photon-ai/builtins/sort.commands.js +67 -0
  323. package/photon-ai/builtins/sort.commands.js.map +1 -0
  324. package/photon-ai/builtins/visibility.commands.d.ts +4 -0
  325. package/photon-ai/builtins/visibility.commands.d.ts.map +1 -0
  326. package/photon-ai/builtins/visibility.commands.js +75 -0
  327. package/photon-ai/builtins/visibility.commands.js.map +1 -0
  328. package/photon-ai/command-builder.d.ts +21 -0
  329. package/photon-ai/command-builder.d.ts.map +1 -0
  330. package/photon-ai/command-builder.js +18 -0
  331. package/photon-ai/command-builder.js.map +1 -0
  332. package/photon-ai/command-executor.d.ts +15 -0
  333. package/photon-ai/command-executor.d.ts.map +1 -0
  334. package/photon-ai/command-executor.js +26 -0
  335. package/photon-ai/command-executor.js.map +1 -0
  336. package/photon-ai/entity-resolver.d.ts +121 -0
  337. package/photon-ai/entity-resolver.d.ts.map +1 -0
  338. package/photon-ai/entity-resolver.js +393 -0
  339. package/photon-ai/entity-resolver.js.map +1 -0
  340. package/photon-ai/fuzzy-match.d.ts +12 -0
  341. package/photon-ai/fuzzy-match.d.ts.map +1 -0
  342. package/photon-ai/fuzzy-match.js +37 -0
  343. package/photon-ai/fuzzy-match.js.map +1 -0
  344. package/photon-ai/index.d.ts +15 -0
  345. package/photon-ai/index.d.ts.map +1 -0
  346. package/photon-ai/index.js +13 -0
  347. package/photon-ai/index.js.map +1 -0
  348. package/photon-ai/intent-parser.d.ts +20 -0
  349. package/photon-ai/intent-parser.d.ts.map +1 -0
  350. package/photon-ai/intent-parser.js +52 -0
  351. package/photon-ai/intent-parser.js.map +1 -0
  352. package/photon-ai/photon-ai-memory.d.ts +42 -0
  353. package/photon-ai/photon-ai-memory.d.ts.map +1 -0
  354. package/photon-ai/photon-ai-memory.js +86 -0
  355. package/photon-ai/photon-ai-memory.js.map +1 -0
  356. package/photon-ai/photon-ai-panel.d.ts +54 -0
  357. package/photon-ai/photon-ai-panel.d.ts.map +1 -0
  358. package/photon-ai/photon-ai-panel.js +191 -0
  359. package/photon-ai/photon-ai-panel.js.map +1 -0
  360. package/photon-ai/photon-ai-registry.d.ts +25 -0
  361. package/photon-ai/photon-ai-registry.d.ts.map +1 -0
  362. package/photon-ai/photon-ai-registry.js +41 -0
  363. package/photon-ai/photon-ai-registry.js.map +1 -0
  364. package/photon-ai/photon-ai-service.d.ts +71 -0
  365. package/photon-ai/photon-ai-service.d.ts.map +1 -0
  366. package/photon-ai/photon-ai-service.js +165 -0
  367. package/photon-ai/photon-ai-service.js.map +1 -0
  368. package/photon-ai/photon-ai.types.d.ts +100 -0
  369. package/photon-ai/photon-ai.types.d.ts.map +1 -0
  370. package/photon-ai/photon-ai.types.js +2 -0
  371. package/photon-ai/photon-ai.types.js.map +1 -0
  372. package/photon-ai/query-splitter.d.ts +22 -0
  373. package/photon-ai/query-splitter.d.ts.map +1 -0
  374. package/photon-ai/query-splitter.js +81 -0
  375. package/photon-ai/query-splitter.js.map +1 -0
  376. package/photon-ai/text-normalizer.d.ts +33 -0
  377. package/photon-ai/text-normalizer.d.ts.map +1 -0
  378. package/photon-ai/text-normalizer.js +132 -0
  379. package/photon-ai/text-normalizer.js.map +1 -0
  380. package/renderer/auto-scroller.d.ts +78 -0
  381. package/renderer/auto-scroller.d.ts.map +1 -0
  382. package/renderer/auto-scroller.js +141 -0
  383. package/renderer/auto-scroller.js.map +1 -0
  384. package/renderer/body-renderer.d.ts +191 -0
  385. package/renderer/body-renderer.d.ts.map +1 -0
  386. package/renderer/body-renderer.js +806 -0
  387. package/renderer/body-renderer.js.map +1 -0
  388. package/renderer/cell-renderer.d.ts +26 -0
  389. package/renderer/cell-renderer.d.ts.map +1 -0
  390. package/renderer/cell-renderer.js +201 -0
  391. package/renderer/cell-renderer.js.map +1 -0
  392. package/renderer/column-menu.d.ts +285 -0
  393. package/renderer/column-menu.d.ts.map +1 -0
  394. package/renderer/column-menu.js +873 -0
  395. package/renderer/column-menu.js.map +1 -0
  396. package/renderer/column-style-manager.d.ts +38 -0
  397. package/renderer/column-style-manager.d.ts.map +1 -0
  398. package/renderer/column-style-manager.js +110 -0
  399. package/renderer/column-style-manager.js.map +1 -0
  400. package/renderer/detail-row-renderer.d.ts +153 -0
  401. package/renderer/detail-row-renderer.d.ts.map +1 -0
  402. package/renderer/detail-row-renderer.js +388 -0
  403. package/renderer/detail-row-renderer.js.map +1 -0
  404. package/renderer/dom-utils.d.ts +22 -0
  405. package/renderer/dom-utils.d.ts.map +1 -0
  406. package/renderer/dom-utils.js +132 -0
  407. package/renderer/dom-utils.js.map +1 -0
  408. package/renderer/footer-renderer.d.ts +25 -0
  409. package/renderer/footer-renderer.d.ts.map +1 -0
  410. package/renderer/footer-renderer.js +168 -0
  411. package/renderer/footer-renderer.js.map +1 -0
  412. package/renderer/grid-renderer.d.ts +295 -0
  413. package/renderer/grid-renderer.d.ts.map +1 -0
  414. package/renderer/grid-renderer.js +1456 -0
  415. package/renderer/grid-renderer.js.map +1 -0
  416. package/renderer/group-context-menu.d.ts +79 -0
  417. package/renderer/group-context-menu.d.ts.map +1 -0
  418. package/renderer/group-context-menu.js +238 -0
  419. package/renderer/group-context-menu.js.map +1 -0
  420. package/renderer/group-drop-zone.d.ts +56 -0
  421. package/renderer/group-drop-zone.d.ts.map +1 -0
  422. package/renderer/group-drop-zone.js +376 -0
  423. package/renderer/group-drop-zone.js.map +1 -0
  424. package/renderer/header-renderer.d.ts +264 -0
  425. package/renderer/header-renderer.d.ts.map +1 -0
  426. package/renderer/header-renderer.js +1321 -0
  427. package/renderer/header-renderer.js.map +1 -0
  428. package/renderer/overlay-renderer.d.ts +16 -0
  429. package/renderer/overlay-renderer.d.ts.map +1 -0
  430. package/renderer/overlay-renderer.js +64 -0
  431. package/renderer/overlay-renderer.js.map +1 -0
  432. package/renderer/renderer-resolver.d.ts +18 -0
  433. package/renderer/renderer-resolver.d.ts.map +1 -0
  434. package/renderer/renderer-resolver.js +18 -0
  435. package/renderer/renderer-resolver.js.map +1 -0
  436. package/renderer/row-animator.d.ts +72 -0
  437. package/renderer/row-animator.d.ts.map +1 -0
  438. package/renderer/row-animator.js +175 -0
  439. package/renderer/row-animator.js.map +1 -0
  440. package/renderer/row-drag-renderer.d.ts +58 -0
  441. package/renderer/row-drag-renderer.d.ts.map +1 -0
  442. package/renderer/row-drag-renderer.js +395 -0
  443. package/renderer/row-drag-renderer.js.map +1 -0
  444. package/renderer/row-offset-search.d.ts +17 -0
  445. package/renderer/row-offset-search.d.ts.map +1 -0
  446. package/renderer/row-offset-search.js +31 -0
  447. package/renderer/row-offset-search.js.map +1 -0
  448. package/renderer/row-position-sheet.d.ts +11 -0
  449. package/renderer/row-position-sheet.d.ts.map +1 -0
  450. package/renderer/row-position-sheet.js +29 -0
  451. package/renderer/row-position-sheet.js.map +1 -0
  452. package/renderer/scroll-controller.d.ts +58 -0
  453. package/renderer/scroll-controller.d.ts.map +1 -0
  454. package/renderer/scroll-controller.js +200 -0
  455. package/renderer/scroll-controller.js.map +1 -0
  456. package/renderer/sticky-row-tracker.d.ts +40 -0
  457. package/renderer/sticky-row-tracker.d.ts.map +1 -0
  458. package/renderer/sticky-row-tracker.js +63 -0
  459. package/renderer/sticky-row-tracker.js.map +1 -0
  460. package/renderer/tooltip-renderer.d.ts +36 -0
  461. package/renderer/tooltip-renderer.d.ts.map +1 -0
  462. package/renderer/tooltip-renderer.js +133 -0
  463. package/renderer/tooltip-renderer.js.map +1 -0
  464. package/renderer/tree-cell-renderer.d.ts +37 -0
  465. package/renderer/tree-cell-renderer.d.ts.map +1 -0
  466. package/renderer/tree-cell-renderer.js +75 -0
  467. package/renderer/tree-cell-renderer.js.map +1 -0
  468. package/renderer/tree-sticky-row-tracker.d.ts +62 -0
  469. package/renderer/tree-sticky-row-tracker.d.ts.map +1 -0
  470. package/renderer/tree-sticky-row-tracker.js +120 -0
  471. package/renderer/tree-sticky-row-tracker.js.map +1 -0
  472. package/renderer/virtual-scroll-renderer.d.ts +37 -0
  473. package/renderer/virtual-scroll-renderer.d.ts.map +1 -0
  474. package/renderer/virtual-scroll-renderer.js +133 -0
  475. package/renderer/virtual-scroll-renderer.js.map +1 -0
  476. package/styles/base-styles.d.ts +3 -0
  477. package/styles/base-styles.d.ts.map +1 -0
  478. package/styles/base-styles.js +3045 -0
  479. package/styles/base-styles.js.map +1 -0
  480. package/styles/themes/theme-alpine.d.ts +2 -0
  481. package/styles/themes/theme-alpine.d.ts.map +1 -0
  482. package/styles/themes/theme-alpine.js +120 -0
  483. package/styles/themes/theme-alpine.js.map +1 -0
  484. package/styles/themes/theme-balham.d.ts +2 -0
  485. package/styles/themes/theme-balham.d.ts.map +1 -0
  486. package/styles/themes/theme-balham.js +120 -0
  487. package/styles/themes/theme-balham.js.map +1 -0
  488. package/styles/themes/theme-dark.d.ts +2 -0
  489. package/styles/themes/theme-dark.d.ts.map +1 -0
  490. package/styles/themes/theme-dark.js +216 -0
  491. package/styles/themes/theme-dark.js.map +1 -0
  492. package/styles/themes/theme-material.d.ts +2 -0
  493. package/styles/themes/theme-material.d.ts.map +1 -0
  494. package/styles/themes/theme-material.js +134 -0
  495. package/styles/themes/theme-material.js.map +1 -0
  496. package/styles/themes/theme-quartz.d.ts +2 -0
  497. package/styles/themes/theme-quartz.d.ts.map +1 -0
  498. package/styles/themes/theme-quartz.js +120 -0
  499. package/styles/themes/theme-quartz.js.map +1 -0
  500. package/theme/css-var-injector.d.ts +10 -0
  501. package/theme/css-var-injector.d.ts.map +1 -0
  502. package/theme/css-var-injector.js +63 -0
  503. package/theme/css-var-injector.js.map +1 -0
  504. package/theme/theme-manager.d.ts +20 -0
  505. package/theme/theme-manager.d.ts.map +1 -0
  506. package/theme/theme-manager.js +79 -0
  507. package/theme/theme-manager.js.map +1 -0
  508. package/theme/themes/dark-theme.d.ts +3 -0
  509. package/theme/themes/dark-theme.d.ts.map +1 -0
  510. package/theme/themes/dark-theme.js +149 -0
  511. package/theme/themes/dark-theme.js.map +1 -0
  512. package/theme/themes/light-theme.d.ts +3 -0
  513. package/theme/themes/light-theme.d.ts.map +1 -0
  514. package/theme/themes/light-theme.js +149 -0
  515. package/theme/themes/light-theme.js.map +1 -0
  516. package/types/column.types.d.ts +195 -0
  517. package/types/column.types.d.ts.map +1 -0
  518. package/types/column.types.js +2 -0
  519. package/types/column.types.js.map +1 -0
  520. package/types/event.types.d.ts +213 -0
  521. package/types/event.types.d.ts.map +1 -0
  522. package/types/event.types.js +67 -0
  523. package/types/event.types.js.map +1 -0
  524. package/types/filter.types.d.ts +42 -0
  525. package/types/filter.types.d.ts.map +1 -0
  526. package/types/filter.types.js +2 -0
  527. package/types/filter.types.js.map +1 -0
  528. package/types/grid.types.d.ts +214 -0
  529. package/types/grid.types.d.ts.map +1 -0
  530. package/types/grid.types.js +2 -0
  531. package/types/grid.types.js.map +1 -0
  532. package/types/master-detail.types.d.ts +112 -0
  533. package/types/master-detail.types.d.ts.map +1 -0
  534. package/types/master-detail.types.js +2 -0
  535. package/types/master-detail.types.js.map +1 -0
  536. package/types/photon-ai.types.d.ts +19 -0
  537. package/types/photon-ai.types.d.ts.map +1 -0
  538. package/types/photon-ai.types.js +2 -0
  539. package/types/photon-ai.types.js.map +1 -0
  540. package/types/renderer.types.d.ts +156 -0
  541. package/types/renderer.types.d.ts.map +1 -0
  542. package/types/renderer.types.js +19 -0
  543. package/types/renderer.types.js.map +1 -0
  544. package/types/row.types.d.ts +127 -0
  545. package/types/row.types.d.ts.map +1 -0
  546. package/types/row.types.js +9 -0
  547. package/types/row.types.js.map +1 -0
  548. package/types/theme.types.d.ts +159 -0
  549. package/types/theme.types.d.ts.map +1 -0
  550. package/types/theme.types.js +2 -0
  551. package/types/theme.types.js.map +1 -0
  552. package/types/tree-data.types.d.ts +94 -0
  553. package/types/tree-data.types.d.ts.map +1 -0
  554. package/types/tree-data.types.js +2 -0
  555. package/types/tree-data.types.js.map +1 -0
@@ -0,0 +1,1330 @@
1
+ const DEFAULTS = {
2
+ type: 'column-grouped',
3
+ width: 600,
4
+ height: 320,
5
+ padding: 40,
6
+ showLegend: true,
7
+ showGrid: true,
8
+ showValues: false,
9
+ barWidth: 0.75,
10
+ lineWidth: 2,
11
+ smooth: false,
12
+ fontFamily: 'system-ui, sans-serif',
13
+ fontSize: 12,
14
+ textColor: '#374151',
15
+ gridColor: '#e5e7eb',
16
+ backgroundColor: 'transparent',
17
+ animationDuration: 400,
18
+ };
19
+ const APEX_COLORS = [
20
+ '#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0',
21
+ '#3F51B5', '#03A9F4', '#4CAF50', '#F9CE1D', '#FF9800',
22
+ ];
23
+ function assignColors(data) {
24
+ return {
25
+ labels: data.labels,
26
+ datasets: data.datasets.map((ds, i) => ({
27
+ ...ds,
28
+ color: ds.color ?? APEX_COLORS[i % APEX_COLORS.length],
29
+ })),
30
+ };
31
+ }
32
+ export class ChartRenderer {
33
+ constructor(canvas) {
34
+ this.animProgress = 1;
35
+ this.rafId = null;
36
+ // Hover state
37
+ this.hoverX = null;
38
+ this.hoverY = null;
39
+ this.hoverRafId = null;
40
+ /** Smoothly lerped cursor-Y used to animate the tooltip like ApexCharts. */
41
+ this.tooltipSmoothedY = null;
42
+ this.lastData = null;
43
+ this.lastOptions = null;
44
+ /** Per-dataset scale multiplier used by toggle animations (0 = hidden, 1 = full). */
45
+ this.seriesScales = [];
46
+ /** Active RAF IDs for per-series toggle animations, keyed by dataset index. */
47
+ this.seriesToggleRafs = new Map();
48
+ this.canvas = canvas;
49
+ const ctx = canvas.getContext('2d');
50
+ if (!ctx)
51
+ throw new Error('[PhotonGrid] ChartRenderer: could not get 2d context');
52
+ this.ctx = ctx;
53
+ this.attachEvents();
54
+ }
55
+ render(data, opts = { type: 'column-grouped' }) {
56
+ const options = { ...DEFAULTS, ...opts };
57
+ this.canvas.width = options.width;
58
+ this.canvas.height = options.height;
59
+ const coloredData = assignColors(data);
60
+ this.lastData = coloredData;
61
+ this.lastOptions = options;
62
+ // Full re-render: reset every series to fully visible
63
+ this.seriesScales = coloredData.datasets.map(() => 1);
64
+ if (this.rafId !== null) {
65
+ cancelAnimationFrame(this.rafId);
66
+ this.rafId = null;
67
+ }
68
+ if (options.animationDuration > 0) {
69
+ this.animate(coloredData, options);
70
+ }
71
+ else {
72
+ this.animProgress = 1;
73
+ this.draw(coloredData, options, 1);
74
+ }
75
+ }
76
+ destroy() {
77
+ if (this.rafId !== null) {
78
+ cancelAnimationFrame(this.rafId);
79
+ this.rafId = null;
80
+ }
81
+ if (this.hoverRafId !== null) {
82
+ cancelAnimationFrame(this.hoverRafId);
83
+ this.hoverRafId = null;
84
+ }
85
+ for (const rafId of this.seriesToggleRafs.values())
86
+ cancelAnimationFrame(rafId);
87
+ this.seriesToggleRafs.clear();
88
+ }
89
+ /**
90
+ * Animate a single dataset in or out without re-running the full chart animation.
91
+ * Each dataset's bar heights are multiplied by a per-series scale factor that is
92
+ * smoothly interpolated from its current value to 0 (hide) or 1 (show).
93
+ *
94
+ * @param index - Index in `data.datasets` to animate.
95
+ * @param toVisible - `true` = grow bars into view; `false` = shrink bars to zero.
96
+ * @param data - Full chart data including the toggled series.
97
+ * @param opts - Render options matching the current chart configuration.
98
+ */
99
+ toggleSeries(index, toVisible, data, opts = { type: 'column-grouped' }) {
100
+ const options = { ...DEFAULTS, ...opts };
101
+ const coloredData = assignColors(data);
102
+ this.lastData = coloredData;
103
+ this.lastOptions = options;
104
+ while (this.seriesScales.length < coloredData.datasets.length)
105
+ this.seriesScales.push(1);
106
+ const existing = this.seriesToggleRafs.get(index);
107
+ if (existing !== undefined) {
108
+ cancelAnimationFrame(existing);
109
+ this.seriesToggleRafs.delete(index);
110
+ }
111
+ const from = this.seriesScales[index] ?? (toVisible ? 0 : 1);
112
+ const to = toVisible ? 1 : 0;
113
+ const duration = 280;
114
+ const start = performance.now();
115
+ const tick = (now) => {
116
+ const t = Math.min(1, (now - start) / duration);
117
+ const eased = toVisible ? this.easeOutQuart(t) : 1 - this.easeOutQuart(1 - t);
118
+ this.seriesScales[index] = from + (to - from) * eased;
119
+ this.draw(coloredData, options, 1);
120
+ if (t < 1) {
121
+ this.seriesToggleRafs.set(index, requestAnimationFrame(tick));
122
+ }
123
+ else {
124
+ this.seriesScales[index] = to;
125
+ this.seriesToggleRafs.delete(index);
126
+ this.draw(coloredData, options, 1);
127
+ }
128
+ };
129
+ this.seriesToggleRafs.set(index, requestAnimationFrame(tick));
130
+ }
131
+ // ── Hover event handling ─────────────────────────────────────────────────
132
+ attachEvents() {
133
+ this.canvas.addEventListener('mousemove', (e) => {
134
+ const rect = this.canvas.getBoundingClientRect();
135
+ const sx = this.canvas.width / rect.width;
136
+ const sy = this.canvas.height / rect.height;
137
+ this.hoverX = (e.clientX - rect.left) * sx;
138
+ this.hoverY = (e.clientY - rect.top) * sy;
139
+ this.canvas.style.cursor = 'crosshair';
140
+ this.scheduleHoverRedraw();
141
+ });
142
+ this.canvas.addEventListener('mouseleave', () => {
143
+ this.hoverX = null;
144
+ this.hoverY = null;
145
+ this.tooltipSmoothedY = null; // snap reset so next entry starts fresh
146
+ this.canvas.style.cursor = 'default';
147
+ this.scheduleHoverRedraw();
148
+ });
149
+ }
150
+ scheduleHoverRedraw() {
151
+ if (this.hoverRafId !== null)
152
+ cancelAnimationFrame(this.hoverRafId);
153
+ this.hoverRafId = requestAnimationFrame(() => {
154
+ this.hoverRafId = null;
155
+ if (!this.lastData || !this.lastOptions || this.animProgress < 1)
156
+ return;
157
+ const needsMore = this.stepTooltipLerp();
158
+ this.draw(this.lastData, this.lastOptions, 1);
159
+ if (this.hoverX !== null) {
160
+ this.drawHover(this.lastData, this.lastOptions);
161
+ }
162
+ // Keep firing frames until the tooltip has fully caught up to the cursor
163
+ if (needsMore && this.hoverX !== null)
164
+ this.scheduleHoverRedraw();
165
+ });
166
+ }
167
+ /**
168
+ * Eases `tooltipSmoothedY` toward the raw cursor Y (`hoverY`) by a lerp
169
+ * factor that produces an ApexCharts-style lag behind fast movement.
170
+ *
171
+ * @returns `true` when another frame is still required to complete the easing.
172
+ */
173
+ stepTooltipLerp() {
174
+ if (this.hoverY === null) {
175
+ this.tooltipSmoothedY = null;
176
+ return false;
177
+ }
178
+ if (this.tooltipSmoothedY === null) {
179
+ // Snap on the very first frame of a new hover so there is no initial delay
180
+ this.tooltipSmoothedY = this.hoverY;
181
+ return false;
182
+ }
183
+ const diff = this.hoverY - this.tooltipSmoothedY;
184
+ if (Math.abs(diff) < 0.5) {
185
+ this.tooltipSmoothedY = this.hoverY;
186
+ return false;
187
+ }
188
+ this.tooltipSmoothedY += diff * 0.14; // ~0.14 ≈ ApexCharts tooltip easing feel
189
+ return true;
190
+ }
191
+ // ── Hover overlay dispatcher ──────────────────────────────────────────────
192
+ drawHover(data, opts) {
193
+ const type = opts.type;
194
+ const isHBar = type.startsWith('bar-') || type === 'bar';
195
+ const skip = ['pie', 'doughnut', 'polar', 'funnel'];
196
+ if (skip.includes(type))
197
+ return;
198
+ if (isHBar) {
199
+ this.drawBarHover(data, opts);
200
+ }
201
+ else {
202
+ this.drawCartesianHover(data, opts);
203
+ }
204
+ }
205
+ // ── Cartesian hover (column, line, area, scatter) ─────────────────────────
206
+ drawCartesianHover(data, opts) {
207
+ const { ctx } = this;
208
+ const { plotLeft, plotTop, plotBottom, plotRight, plotW, plotH } = this.getPlotArea(opts, data);
209
+ const hx = this.hoverX;
210
+ if (hx < plotLeft || hx > plotRight)
211
+ return;
212
+ const nPoints = data.labels.length;
213
+ if (!nPoints)
214
+ return;
215
+ const type = opts.type;
216
+ const isColumn = type.startsWith('column');
217
+ // Find nearest index
218
+ let idx;
219
+ let crosshairX;
220
+ if (isColumn) {
221
+ const groupW = plotW / Math.max(nPoints, 1);
222
+ idx = Math.min(Math.floor((hx - plotLeft) / groupW), nPoints - 1);
223
+ crosshairX = plotLeft + idx * groupW + groupW / 2;
224
+ }
225
+ else {
226
+ const xStep = nPoints > 1 ? plotW / (nPoints - 1) : 0;
227
+ idx = xStep > 0 ? Math.round((hx - plotLeft) / xStep) : 0;
228
+ idx = Math.max(0, Math.min(idx, nPoints - 1));
229
+ crosshairX = nPoints > 1 ? plotLeft + (idx / (nPoints - 1)) * plotW : plotLeft + plotW / 2;
230
+ }
231
+ // ── Vertical crosshair ──
232
+ ctx.save();
233
+ ctx.strokeStyle = 'rgba(100,116,139,0.30)';
234
+ ctx.lineWidth = 1;
235
+ ctx.setLineDash([5, 4]);
236
+ ctx.beginPath();
237
+ ctx.moveTo(crosshairX, plotTop);
238
+ ctx.lineTo(crosshairX, plotBottom);
239
+ ctx.stroke();
240
+ ctx.setLineDash([]);
241
+ ctx.restore();
242
+ // ── Highlight dots for line / area / scatter ──
243
+ if (['line', 'area', 'scatter'].includes(type)) {
244
+ const rawMax = Math.max(...data.datasets.flatMap((d) => d.data), 0);
245
+ const maxVal = this.niceMax(rawMax);
246
+ const getY = (v) => plotBottom - (v / maxVal) * plotH;
247
+ for (const ds of data.datasets) {
248
+ const val = ds.data[idx] ?? 0;
249
+ const dotX = crosshairX;
250
+ const dotY = getY(val);
251
+ ctx.save();
252
+ ctx.beginPath();
253
+ ctx.arc(dotX, dotY, 5, 0, Math.PI * 2);
254
+ ctx.fillStyle = ds.color ?? APEX_COLORS[0];
255
+ ctx.fill();
256
+ ctx.strokeStyle = '#fff';
257
+ ctx.lineWidth = 2.5;
258
+ ctx.stroke();
259
+ ctx.restore();
260
+ }
261
+ }
262
+ // ── Column: highlight the hovered group with a faint backdrop ──
263
+ if (isColumn) {
264
+ const groupW = plotW / Math.max(nPoints, 1);
265
+ const bx = plotLeft + idx * groupW;
266
+ ctx.save();
267
+ ctx.fillStyle = 'rgba(100,116,139,0.06)';
268
+ ctx.fillRect(bx, plotTop, groupW, plotBottom - plotTop);
269
+ ctx.restore();
270
+ }
271
+ // ── Tooltip ── anchor Y to the smoothly interpolated cursor position
272
+ this.drawTooltip(data, opts, idx, crosshairX, this.tooltipSmoothedY, plotTop, plotBottom, false);
273
+ }
274
+ // ── Horizontal bar hover ──────────────────────────────────────────────────
275
+ drawBarHover(data, opts) {
276
+ const { ctx } = this;
277
+ const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
278
+ const plotLeft = 80;
279
+ const plotTop = 10;
280
+ const plotRight = opts.width - 20;
281
+ const plotBottom = opts.height - legendH - 30;
282
+ const plotH = plotBottom - plotTop;
283
+ const hy = this.hoverY;
284
+ if (hy < plotTop || hy > plotBottom)
285
+ return;
286
+ const nGroups = data.labels.length;
287
+ if (!nGroups)
288
+ return;
289
+ const groupH = plotH / Math.max(nGroups, 1);
290
+ const idx = Math.min(Math.floor((hy - plotTop) / groupH), nGroups - 1);
291
+ const crosshairY = plotTop + idx * groupH + groupH / 2;
292
+ // Horizontal crosshair
293
+ ctx.save();
294
+ ctx.strokeStyle = 'rgba(100,116,139,0.30)';
295
+ ctx.lineWidth = 1;
296
+ ctx.setLineDash([5, 4]);
297
+ ctx.beginPath();
298
+ ctx.moveTo(plotLeft, crosshairY);
299
+ ctx.lineTo(plotRight, crosshairY);
300
+ ctx.stroke();
301
+ ctx.setLineDash([]);
302
+ ctx.restore();
303
+ // Row highlight
304
+ ctx.save();
305
+ ctx.fillStyle = 'rgba(100,116,139,0.06)';
306
+ ctx.fillRect(plotLeft, plotTop + idx * groupH, plotRight - plotLeft, groupH);
307
+ ctx.restore();
308
+ // Tooltip (position to the right of the plot)
309
+ this.drawTooltip(data, opts, idx, plotRight - 10, crosshairY, plotTop, plotBottom, true);
310
+ }
311
+ // ── Tooltip ───────────────────────────────────────────────────────────────
312
+ drawTooltip(data, opts, idx, anchorX, anchorY, plotTop, plotBottom, fromRight) {
313
+ const { ctx } = this;
314
+ const xLabel = data.labels[idx] ?? '';
315
+ const rows = data.datasets
316
+ .filter((_, i) => (this.seriesScales[i] ?? 1) > 0.05)
317
+ .map((ds) => ({
318
+ color: ds.color ?? APEX_COLORS[0],
319
+ label: ds.label,
320
+ value: this.formatNum(ds.data[idx] ?? 0),
321
+ }));
322
+ const PAD = 10;
323
+ const LINE = 18;
324
+ const SW = 8; // swatch width
325
+ const GAP = 7; // gap between swatch and label
326
+ ctx.font = `600 ${opts.fontSize}px ${opts.fontFamily}`;
327
+ const headerW = ctx.measureText(xLabel).width;
328
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
329
+ const maxValueW = Math.max(...rows.map((r) => {
330
+ ctx.font = `600 ${opts.fontSize}px ${opts.fontFamily}`;
331
+ const vw = ctx.measureText(r.value).width;
332
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
333
+ const lw = ctx.measureText(this.truncate(r.label, 16)).width;
334
+ return SW + GAP + lw + 16 + vw;
335
+ }), 0);
336
+ const tooltipW = Math.max(headerW, maxValueW) + PAD * 2;
337
+ const tooltipH = PAD * 2 + LINE + rows.length * LINE + (rows.length > 0 ? 4 : 0);
338
+ // Position: right of anchor, flip if too close to right edge
339
+ let tx;
340
+ if (fromRight) {
341
+ tx = anchorX - tooltipW - 14;
342
+ }
343
+ else {
344
+ tx = anchorX + 14;
345
+ if (tx + tooltipW > opts.width - 8)
346
+ tx = anchorX - tooltipW - 14;
347
+ }
348
+ tx = Math.max(4, tx);
349
+ // Vertical: center on anchor or mid-plot
350
+ const midY = anchorY !== null ? anchorY : (plotTop + plotBottom) / 2;
351
+ let ty = midY - tooltipH / 2;
352
+ ty = Math.max(plotTop, Math.min(ty, plotBottom - tooltipH));
353
+ ty = Math.max(4, ty);
354
+ // ── Shadow + background ──
355
+ ctx.save();
356
+ ctx.shadowColor = 'rgba(0,0,0,0.12)';
357
+ ctx.shadowBlur = 12;
358
+ ctx.shadowOffsetY = 3;
359
+ ctx.fillStyle = opts.backgroundColor === 'transparent' ? '#fff' : opts.backgroundColor;
360
+ ctx.strokeStyle = 'rgba(0,0,0,0.08)';
361
+ ctx.lineWidth = 1;
362
+ ctx.beginPath();
363
+ ctx.roundRect(tx, ty, tooltipW, tooltipH, 6);
364
+ ctx.fill();
365
+ ctx.shadowColor = 'transparent';
366
+ ctx.stroke();
367
+ ctx.restore();
368
+ // ── Header (X label) ──
369
+ ctx.save();
370
+ ctx.font = `600 ${opts.fontSize}px ${opts.fontFamily}`;
371
+ ctx.fillStyle = opts.textColor;
372
+ ctx.textAlign = 'left';
373
+ ctx.textBaseline = 'middle';
374
+ ctx.fillText(xLabel, tx + PAD, ty + PAD + LINE / 2);
375
+ // ── Separator ──
376
+ ctx.strokeStyle = 'rgba(0,0,0,0.07)';
377
+ ctx.lineWidth = 1;
378
+ ctx.beginPath();
379
+ ctx.moveTo(tx + PAD, ty + PAD + LINE);
380
+ ctx.lineTo(tx + tooltipW - PAD, ty + PAD + LINE);
381
+ ctx.stroke();
382
+ // ── Dataset rows ──
383
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
384
+ for (let i = 0; i < rows.length; i++) {
385
+ const ry = ty + PAD + LINE + 4 + i * LINE + LINE / 2;
386
+ // Color swatch (rounded)
387
+ ctx.fillStyle = rows[i].color;
388
+ ctx.beginPath();
389
+ ctx.roundRect(tx + PAD, ry - SW / 2, SW, SW, 2);
390
+ ctx.fill();
391
+ // Series label
392
+ ctx.fillStyle = opts.textColor;
393
+ ctx.textAlign = 'left';
394
+ ctx.fillText(this.truncate(rows[i].label, 16), tx + PAD + SW + GAP, ry);
395
+ // Value — bold, right-aligned
396
+ ctx.font = `600 ${opts.fontSize}px ${opts.fontFamily}`;
397
+ ctx.fillStyle = rows[i].color;
398
+ ctx.textAlign = 'right';
399
+ ctx.fillText(rows[i].value, tx + tooltipW - PAD, ry);
400
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
401
+ }
402
+ ctx.textBaseline = 'alphabetic';
403
+ ctx.restore();
404
+ }
405
+ // ── Animation ────────────────────────────────────────────────────────────
406
+ animate(data, options) {
407
+ this.animProgress = 0;
408
+ const start = performance.now();
409
+ const tick = (now) => {
410
+ const elapsed = now - start;
411
+ this.animProgress = Math.min(1, elapsed / options.animationDuration);
412
+ const eased = this.easeOutQuart(this.animProgress);
413
+ this.draw(data, options, eased);
414
+ if (this.animProgress < 1) {
415
+ this.rafId = requestAnimationFrame(tick);
416
+ }
417
+ else {
418
+ this.rafId = null;
419
+ }
420
+ };
421
+ this.rafId = requestAnimationFrame(tick);
422
+ }
423
+ getPlotArea(opts, data) {
424
+ const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
425
+ const plotLeft = 65;
426
+ const plotTop = 20;
427
+ const plotRight = opts.width - 20;
428
+ const plotBottom = opts.height - legendH - 36;
429
+ return {
430
+ plotLeft,
431
+ plotTop,
432
+ plotRight,
433
+ plotBottom,
434
+ plotW: plotRight - plotLeft,
435
+ plotH: plotBottom - plotTop,
436
+ legendH,
437
+ };
438
+ }
439
+ draw(data, options, progress = 1) {
440
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
441
+ if (options.backgroundColor !== 'transparent') {
442
+ this.ctx.fillStyle = options.backgroundColor;
443
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
444
+ }
445
+ let type = options.type;
446
+ if (type === 'bar')
447
+ type = 'column-grouped';
448
+ switch (type) {
449
+ case 'column-grouped':
450
+ this.drawColumnGrouped(data, options, progress);
451
+ break;
452
+ case 'column-stacked':
453
+ this.drawColumnStacked(data, options, progress);
454
+ break;
455
+ case 'column-100stacked':
456
+ this.drawColumn100Stacked(data, options, progress);
457
+ break;
458
+ case 'bar-grouped':
459
+ this.drawBarGrouped(data, options, progress);
460
+ break;
461
+ case 'bar-stacked':
462
+ this.drawBarStacked(data, options, progress);
463
+ break;
464
+ case 'bar-100stacked':
465
+ this.drawBar100Stacked(data, options, progress);
466
+ break;
467
+ case 'line':
468
+ this.drawLine(data, options, progress);
469
+ break;
470
+ case 'area':
471
+ this.drawArea(data, options, progress);
472
+ break;
473
+ case 'pie':
474
+ this.drawPie(data, options, progress, false);
475
+ break;
476
+ case 'doughnut':
477
+ this.drawPie(data, options, progress, true);
478
+ break;
479
+ case 'scatter':
480
+ this.drawScatter(data, options, progress);
481
+ break;
482
+ case 'polar':
483
+ this.drawPolar(data, options, progress);
484
+ break;
485
+ case 'funnel':
486
+ this.drawFunnel(data, options, progress);
487
+ break;
488
+ default:
489
+ this.drawColumnGrouped(data, options, progress);
490
+ break;
491
+ }
492
+ if (options.showLegend && data.datasets.length > 1) {
493
+ this.drawLegend(data, options);
494
+ }
495
+ }
496
+ // ── Column Grouped ──────────────────────────────────────────────────────────
497
+ drawColumnGrouped(data, opts, progress) {
498
+ const { ctx } = this;
499
+ const { plotLeft, plotTop, plotBottom, plotRight, plotW, plotH } = this.getPlotArea(opts, data);
500
+ const nDatasets = data.datasets.length;
501
+ const usePerSeries = nDatasets > 1;
502
+ // When datasets have very different magnitudes each series is normalised to
503
+ // its own maximum so every bar group is visually represented. The Y-axis
504
+ // switches to a relative (0–100 %) scale in that case.
505
+ const seriesMaxes = data.datasets.map((ds) => this.niceMax(Math.max(...ds.data, 0)));
506
+ const globalRawMax = Math.max(...data.datasets.flatMap((d) => d.data), 0);
507
+ const globalMax = this.niceMax(globalRawMax);
508
+ if (opts.showGrid) {
509
+ if (usePerSeries)
510
+ this.drawGridLinesRelative(plotLeft, plotTop, plotW, plotH, opts);
511
+ else
512
+ this.drawGridLines(plotLeft, plotTop, plotW, plotH, globalMax, opts);
513
+ }
514
+ else {
515
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
516
+ }
517
+ const nGroups = data.labels.length;
518
+ const groupWidth = plotW / Math.max(nGroups, 1);
519
+ const barW = (groupWidth * opts.barWidth) / nDatasets;
520
+ const labelStep = this.getLabelStep(nGroups, plotW, opts.fontSize);
521
+ const rotateLabels = nGroups > 8;
522
+ const labelAlpha = Math.min(1, progress * 2.5);
523
+ for (let gi = 0; gi < nGroups; gi++) {
524
+ const groupX = plotLeft + gi * groupWidth + (groupWidth * (1 - opts.barWidth)) / 2;
525
+ for (let di = 0; di < nDatasets; di++) {
526
+ const value = data.datasets[di].data[gi] ?? 0;
527
+ const maxForSeries = usePerSeries ? (seriesMaxes[di] || 1) : globalMax;
528
+ const seriesScale = this.seriesScales[di] ?? 1;
529
+ const barH = (value / maxForSeries) * plotH * progress * seriesScale;
530
+ const barX = groupX + di * barW;
531
+ const barY = plotBottom - barH;
532
+ ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
533
+ ctx.beginPath();
534
+ ctx.roundRect(barX, barY, Math.max(barW - 1, 1), Math.max(barH, 0), [3, 3, 0, 0]);
535
+ ctx.fill();
536
+ }
537
+ if (gi % labelStep === 0) {
538
+ ctx.save();
539
+ ctx.globalAlpha = labelAlpha;
540
+ ctx.fillStyle = opts.textColor;
541
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
542
+ const labelX = groupX + groupWidth * opts.barWidth * 0.5;
543
+ const labelY = plotBottom + 14;
544
+ if (rotateLabels) {
545
+ ctx.translate(labelX, labelY);
546
+ ctx.rotate(Math.PI / 4);
547
+ ctx.textAlign = 'left';
548
+ ctx.fillText(this.truncate(data.labels[gi], 12), 0, 0);
549
+ }
550
+ else {
551
+ ctx.textAlign = 'center';
552
+ ctx.fillText(this.truncate(data.labels[gi], 12), labelX, labelY);
553
+ }
554
+ ctx.restore();
555
+ }
556
+ }
557
+ }
558
+ // ── Column Stacked ──────────────────────────────────────────────────────────
559
+ drawColumnStacked(data, opts, progress) {
560
+ const { ctx } = this;
561
+ const { plotLeft, plotTop, plotBottom, plotRight, plotW, plotH } = this.getPlotArea(opts, data);
562
+ const stackTotals = data.labels.map((_, gi) => data.datasets.reduce((sum, ds) => sum + (ds.data[gi] ?? 0), 0));
563
+ const rawMax = Math.max(...stackTotals, 0);
564
+ const maxVal = this.niceMax(rawMax);
565
+ if (opts.showGrid)
566
+ this.drawGridLines(plotLeft, plotTop, plotW, plotH, maxVal, opts);
567
+ else
568
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
569
+ const nGroups = data.labels.length;
570
+ const groupWidth = plotW / Math.max(nGroups, 1);
571
+ const barW = groupWidth * opts.barWidth;
572
+ const labelStep = this.getLabelStep(nGroups, plotW, opts.fontSize);
573
+ const labelAlpha = Math.min(1, progress * 2.5);
574
+ for (let gi = 0; gi < nGroups; gi++) {
575
+ const barX = plotLeft + gi * groupWidth + (groupWidth - barW) / 2;
576
+ let currentY = plotBottom;
577
+ for (let di = 0; di < data.datasets.length; di++) {
578
+ const value = data.datasets[di].data[gi] ?? 0;
579
+ const segH = (value / maxVal) * plotH * progress;
580
+ const segY = currentY - segH;
581
+ ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
582
+ ctx.beginPath();
583
+ if (di === data.datasets.length - 1) {
584
+ ctx.roundRect(barX, segY, Math.max(barW, 1), Math.max(segH, 0), [3, 3, 0, 0]);
585
+ }
586
+ else {
587
+ ctx.rect(barX, segY, Math.max(barW, 1), Math.max(segH, 0));
588
+ }
589
+ ctx.fill();
590
+ currentY -= segH;
591
+ }
592
+ if (gi % labelStep === 0) {
593
+ ctx.globalAlpha = labelAlpha;
594
+ ctx.fillStyle = opts.textColor;
595
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
596
+ ctx.textAlign = 'center';
597
+ ctx.fillText(this.truncate(data.labels[gi], 12), barX + barW / 2, plotBottom + 14);
598
+ ctx.globalAlpha = 1;
599
+ }
600
+ }
601
+ }
602
+ // ── Column 100% Stacked ─────────────────────────────────────────────────────
603
+ drawColumn100Stacked(data, opts, progress) {
604
+ const { ctx } = this;
605
+ const { plotLeft, plotTop, plotBottom, plotRight, plotW, plotH } = this.getPlotArea(opts, data);
606
+ if (opts.showGrid)
607
+ this.drawGridLines(plotLeft, plotTop, plotW, plotH, 100, opts);
608
+ else
609
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
610
+ const nGroups = data.labels.length;
611
+ const groupWidth = plotW / Math.max(nGroups, 1);
612
+ const barW = groupWidth * opts.barWidth;
613
+ const labelStep = this.getLabelStep(nGroups, plotW, opts.fontSize);
614
+ const labelAlpha = Math.min(1, progress * 2.5);
615
+ for (let gi = 0; gi < nGroups; gi++) {
616
+ const total = data.datasets.reduce((sum, ds) => sum + (ds.data[gi] ?? 0), 0) || 1;
617
+ const barX = plotLeft + gi * groupWidth + (groupWidth - barW) / 2;
618
+ let currentY = plotBottom;
619
+ for (let di = 0; di < data.datasets.length; di++) {
620
+ const value = data.datasets[di].data[gi] ?? 0;
621
+ const segH = (value / total) * plotH * progress;
622
+ const segY = currentY - segH;
623
+ ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
624
+ ctx.beginPath();
625
+ if (di === data.datasets.length - 1) {
626
+ ctx.roundRect(barX, segY, Math.max(barW, 1), Math.max(segH, 0), [3, 3, 0, 0]);
627
+ }
628
+ else {
629
+ ctx.rect(barX, segY, Math.max(barW, 1), Math.max(segH, 0));
630
+ }
631
+ ctx.fill();
632
+ currentY -= segH;
633
+ }
634
+ if (gi % labelStep === 0) {
635
+ ctx.globalAlpha = labelAlpha;
636
+ ctx.fillStyle = opts.textColor;
637
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
638
+ ctx.textAlign = 'center';
639
+ ctx.fillText(this.truncate(data.labels[gi], 12), barX + barW / 2, plotBottom + 14);
640
+ ctx.globalAlpha = 1;
641
+ }
642
+ }
643
+ }
644
+ // ── Bar Grouped (horizontal) ────────────────────────────────────────────────
645
+ drawBarGrouped(data, opts, progress) {
646
+ const { ctx } = this;
647
+ const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
648
+ const labelAreaW = 80;
649
+ const plotLeft = labelAreaW;
650
+ const plotTop = 10;
651
+ const plotRight = opts.width - 20;
652
+ const plotBottom = opts.height - legendH - 30;
653
+ const plotW = plotRight - plotLeft;
654
+ const plotH = plotBottom - plotTop;
655
+ const nDatasets = data.datasets.length;
656
+ const usePerSeries = nDatasets > 1;
657
+ const seriesMaxes = data.datasets.map((ds) => this.niceMax(Math.max(...ds.data, 0)));
658
+ const globalRawMax = Math.max(...data.datasets.flatMap((d) => d.data), 0);
659
+ const globalMax = this.niceMax(globalRawMax);
660
+ const labelAlpha = Math.min(1, progress * 2.5);
661
+ if (opts.showGrid) {
662
+ const steps = 5;
663
+ ctx.strokeStyle = opts.gridColor;
664
+ ctx.lineWidth = 1;
665
+ ctx.setLineDash([4, 4]);
666
+ for (let i = 0; i <= steps; i++) {
667
+ const x = plotLeft + (i / steps) * plotW;
668
+ ctx.beginPath();
669
+ ctx.moveTo(x, plotTop);
670
+ ctx.lineTo(x, plotBottom);
671
+ ctx.stroke();
672
+ ctx.fillStyle = opts.textColor;
673
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
674
+ ctx.textAlign = 'center';
675
+ const xLabel = usePerSeries
676
+ ? `${Math.round((100 * i) / steps)}%`
677
+ : this.formatNum((globalMax * i) / steps);
678
+ ctx.fillText(xLabel, x, plotBottom + 14);
679
+ }
680
+ ctx.setLineDash([]);
681
+ ctx.strokeStyle = opts.gridColor;
682
+ ctx.lineWidth = 1.5;
683
+ ctx.beginPath();
684
+ ctx.moveTo(plotLeft, plotTop);
685
+ ctx.lineTo(plotLeft, plotBottom);
686
+ ctx.stroke();
687
+ ctx.beginPath();
688
+ ctx.moveTo(plotLeft, plotBottom);
689
+ ctx.lineTo(plotRight, plotBottom);
690
+ ctx.stroke();
691
+ }
692
+ else {
693
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
694
+ }
695
+ const nGroups = data.labels.length;
696
+ const groupH = plotH / Math.max(nGroups, 1);
697
+ const barH = (groupH * opts.barWidth) / nDatasets;
698
+ const labelStep = this.getLabelStep(nGroups, plotH, opts.fontSize);
699
+ for (let gi = 0; gi < nGroups; gi++) {
700
+ const groupY = plotTop + gi * groupH + (groupH * (1 - opts.barWidth)) / 2;
701
+ for (let di = 0; di < nDatasets; di++) {
702
+ const value = data.datasets[di].data[gi] ?? 0;
703
+ const maxForSeries = usePerSeries ? (seriesMaxes[di] || 1) : globalMax;
704
+ const seriesScale = this.seriesScales[di] ?? 1;
705
+ const barW = (value / maxForSeries) * plotW * progress * seriesScale;
706
+ const barY = groupY + di * barH;
707
+ ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
708
+ ctx.beginPath();
709
+ ctx.roundRect(plotLeft, barY, Math.max(barW, 0), Math.max(barH - 1, 1), [0, 3, 3, 0]);
710
+ ctx.fill();
711
+ }
712
+ if (gi % labelStep === 0) {
713
+ ctx.globalAlpha = labelAlpha;
714
+ ctx.fillStyle = opts.textColor;
715
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
716
+ ctx.textAlign = 'right';
717
+ ctx.fillText(this.truncate(data.labels[gi], 10), labelAreaW - 6, groupY + groupH * opts.barWidth * 0.5 + opts.fontSize / 3);
718
+ ctx.globalAlpha = 1;
719
+ }
720
+ }
721
+ }
722
+ // ── Bar Stacked (horizontal) ────────────────────────────────────────────────
723
+ drawBarStacked(data, opts, progress) {
724
+ const { ctx } = this;
725
+ const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
726
+ const labelAreaW = 80;
727
+ const plotLeft = labelAreaW;
728
+ const plotTop = 10;
729
+ const plotRight = opts.width - 20;
730
+ const plotBottom = opts.height - legendH - 30;
731
+ const plotW = plotRight - plotLeft;
732
+ const plotH = plotBottom - plotTop;
733
+ const stackTotals = data.labels.map((_, gi) => data.datasets.reduce((sum, ds) => sum + (ds.data[gi] ?? 0), 0));
734
+ const rawMax = Math.max(...stackTotals, 0);
735
+ const maxVal = this.niceMax(rawMax);
736
+ const labelAlpha = Math.min(1, progress * 2.5);
737
+ if (opts.showGrid) {
738
+ const steps = 5;
739
+ ctx.strokeStyle = opts.gridColor;
740
+ ctx.lineWidth = 1;
741
+ ctx.setLineDash([4, 4]);
742
+ for (let i = 0; i <= steps; i++) {
743
+ const x = plotLeft + (i / steps) * plotW;
744
+ ctx.beginPath();
745
+ ctx.moveTo(x, plotTop);
746
+ ctx.lineTo(x, plotBottom);
747
+ ctx.stroke();
748
+ ctx.fillStyle = opts.textColor;
749
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
750
+ ctx.textAlign = 'center';
751
+ ctx.fillText(this.formatNum((maxVal * i) / steps), x, plotBottom + 14);
752
+ }
753
+ ctx.setLineDash([]);
754
+ ctx.strokeStyle = opts.gridColor;
755
+ ctx.lineWidth = 1.5;
756
+ ctx.beginPath();
757
+ ctx.moveTo(plotLeft, plotTop);
758
+ ctx.lineTo(plotLeft, plotBottom);
759
+ ctx.stroke();
760
+ ctx.beginPath();
761
+ ctx.moveTo(plotLeft, plotBottom);
762
+ ctx.lineTo(plotRight, plotBottom);
763
+ ctx.stroke();
764
+ }
765
+ else {
766
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
767
+ }
768
+ const nGroups = data.labels.length;
769
+ const groupH = plotH / Math.max(nGroups, 1);
770
+ const barH = groupH * opts.barWidth;
771
+ const labelStep = this.getLabelStep(nGroups, plotH, opts.fontSize);
772
+ for (let gi = 0; gi < nGroups; gi++) {
773
+ const barY = plotTop + gi * groupH + (groupH - barH) / 2;
774
+ let currentX = plotLeft;
775
+ for (let di = 0; di < data.datasets.length; di++) {
776
+ const value = data.datasets[di].data[gi] ?? 0;
777
+ const segW = (value / maxVal) * plotW * progress;
778
+ ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
779
+ ctx.beginPath();
780
+ if (di === data.datasets.length - 1) {
781
+ ctx.roundRect(currentX, barY, Math.max(segW, 0), Math.max(barH, 1), [0, 3, 3, 0]);
782
+ }
783
+ else {
784
+ ctx.rect(currentX, barY, Math.max(segW, 0), Math.max(barH, 1));
785
+ }
786
+ ctx.fill();
787
+ currentX += segW;
788
+ }
789
+ if (gi % labelStep === 0) {
790
+ ctx.globalAlpha = labelAlpha;
791
+ ctx.fillStyle = opts.textColor;
792
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
793
+ ctx.textAlign = 'right';
794
+ ctx.fillText(this.truncate(data.labels[gi], 10), labelAreaW - 6, barY + barH / 2 + opts.fontSize / 3);
795
+ ctx.globalAlpha = 1;
796
+ }
797
+ }
798
+ }
799
+ // ── Bar 100% Stacked (horizontal) ──────────────────────────────────────────
800
+ drawBar100Stacked(data, opts, progress) {
801
+ const { ctx } = this;
802
+ const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
803
+ const labelAreaW = 80;
804
+ const plotLeft = labelAreaW;
805
+ const plotTop = 10;
806
+ const plotRight = opts.width - 20;
807
+ const plotBottom = opts.height - legendH - 30;
808
+ const plotW = plotRight - plotLeft;
809
+ const plotH = plotBottom - plotTop;
810
+ const labelAlpha = Math.min(1, progress * 2.5);
811
+ if (opts.showGrid) {
812
+ const steps = 5;
813
+ ctx.strokeStyle = opts.gridColor;
814
+ ctx.lineWidth = 1;
815
+ ctx.setLineDash([4, 4]);
816
+ for (let i = 0; i <= steps; i++) {
817
+ const x = plotLeft + (i / steps) * plotW;
818
+ ctx.beginPath();
819
+ ctx.moveTo(x, plotTop);
820
+ ctx.lineTo(x, plotBottom);
821
+ ctx.stroke();
822
+ ctx.fillStyle = opts.textColor;
823
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
824
+ ctx.textAlign = 'center';
825
+ ctx.fillText(`${(100 * i) / steps}%`, x, plotBottom + 14);
826
+ }
827
+ ctx.setLineDash([]);
828
+ ctx.strokeStyle = opts.gridColor;
829
+ ctx.lineWidth = 1.5;
830
+ ctx.beginPath();
831
+ ctx.moveTo(plotLeft, plotTop);
832
+ ctx.lineTo(plotLeft, plotBottom);
833
+ ctx.stroke();
834
+ ctx.beginPath();
835
+ ctx.moveTo(plotLeft, plotBottom);
836
+ ctx.lineTo(plotRight, plotBottom);
837
+ ctx.stroke();
838
+ }
839
+ else {
840
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
841
+ }
842
+ const nGroups = data.labels.length;
843
+ const groupH = plotH / Math.max(nGroups, 1);
844
+ const barH = groupH * opts.barWidth;
845
+ const labelStep = this.getLabelStep(nGroups, plotH, opts.fontSize);
846
+ for (let gi = 0; gi < nGroups; gi++) {
847
+ const total = data.datasets.reduce((sum, ds) => sum + (ds.data[gi] ?? 0), 0) || 1;
848
+ const barY = plotTop + gi * groupH + (groupH - barH) / 2;
849
+ let currentX = plotLeft;
850
+ for (let di = 0; di < data.datasets.length; di++) {
851
+ const value = data.datasets[di].data[gi] ?? 0;
852
+ const segW = (value / total) * plotW * progress;
853
+ ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
854
+ ctx.beginPath();
855
+ if (di === data.datasets.length - 1) {
856
+ ctx.roundRect(currentX, barY, Math.max(segW, 0), Math.max(barH, 1), [0, 3, 3, 0]);
857
+ }
858
+ else {
859
+ ctx.rect(currentX, barY, Math.max(segW, 0), Math.max(barH, 1));
860
+ }
861
+ ctx.fill();
862
+ currentX += segW;
863
+ }
864
+ if (gi % labelStep === 0) {
865
+ ctx.globalAlpha = labelAlpha;
866
+ ctx.fillStyle = opts.textColor;
867
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
868
+ ctx.textAlign = 'right';
869
+ ctx.fillText(this.truncate(data.labels[gi], 10), labelAreaW - 6, barY + barH / 2 + opts.fontSize / 3);
870
+ ctx.globalAlpha = 1;
871
+ }
872
+ }
873
+ }
874
+ // ── Line ────────────────────────────────────────────────────────────────────
875
+ drawLine(data, opts, progress) {
876
+ const { ctx } = this;
877
+ const { plotLeft, plotTop, plotBottom, plotRight, plotW, plotH } = this.getPlotArea(opts, data);
878
+ const rawMax = Math.max(...data.datasets.flatMap((d) => d.data), 0);
879
+ const maxVal = this.niceMax(rawMax);
880
+ const nPoints = data.labels.length;
881
+ const labelAlpha = Math.min(1, progress * 2.5);
882
+ if (opts.showGrid)
883
+ this.drawGridLines(plotLeft, plotTop, plotW, plotH, maxVal, opts);
884
+ else
885
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
886
+ const getX = (i) => plotLeft + (i / Math.max(1, nPoints - 1)) * plotW;
887
+ const getY = (v) => plotBottom - (v / maxVal) * plotH;
888
+ const visibleCount = Math.max(2, Math.round(nPoints * progress));
889
+ for (const ds of data.datasets) {
890
+ ctx.save();
891
+ ctx.beginPath();
892
+ ctx.rect(plotLeft, plotTop, plotW, plotH + 1);
893
+ ctx.clip();
894
+ ctx.strokeStyle = ds.color ?? APEX_COLORS[0];
895
+ ctx.lineWidth = opts.lineWidth;
896
+ ctx.lineCap = 'round';
897
+ ctx.lineJoin = 'round';
898
+ ctx.beginPath();
899
+ for (let i = 0; i < Math.min(visibleCount, nPoints); i++) {
900
+ const x = getX(i);
901
+ const y = getY(ds.data[i] ?? 0);
902
+ if (i === 0)
903
+ ctx.moveTo(x, y);
904
+ else
905
+ ctx.lineTo(x, y);
906
+ }
907
+ ctx.stroke();
908
+ for (let i = 0; i < Math.min(visibleCount, nPoints); i++) {
909
+ ctx.beginPath();
910
+ ctx.arc(getX(i), getY(ds.data[i] ?? 0), 3, 0, Math.PI * 2);
911
+ ctx.fillStyle = ds.color ?? APEX_COLORS[0];
912
+ ctx.fill();
913
+ ctx.strokeStyle = '#fff';
914
+ ctx.lineWidth = 1.5;
915
+ ctx.stroke();
916
+ }
917
+ ctx.restore();
918
+ }
919
+ const labelStep = this.getLabelStep(nPoints, plotW, opts.fontSize);
920
+ ctx.globalAlpha = labelAlpha;
921
+ ctx.fillStyle = opts.textColor;
922
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
923
+ ctx.textAlign = 'center';
924
+ for (let i = 0; i < nPoints; i++) {
925
+ if (i % labelStep === 0)
926
+ ctx.fillText(this.truncate(data.labels[i], 10), getX(i), plotBottom + 16);
927
+ }
928
+ ctx.globalAlpha = 1;
929
+ }
930
+ // ── Area ────────────────────────────────────────────────────────────────────
931
+ drawArea(data, opts, progress) {
932
+ const { ctx } = this;
933
+ const { plotLeft, plotTop, plotBottom, plotRight, plotW, plotH } = this.getPlotArea(opts, data);
934
+ const rawMax = Math.max(...data.datasets.flatMap((d) => d.data), 0);
935
+ const maxVal = this.niceMax(rawMax);
936
+ const nPoints = data.labels.length;
937
+ const labelAlpha = Math.min(1, progress * 2.5);
938
+ if (opts.showGrid)
939
+ this.drawGridLines(plotLeft, plotTop, plotW, plotH, maxVal, opts);
940
+ else
941
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
942
+ const getX = (i) => plotLeft + (i / Math.max(1, nPoints - 1)) * plotW;
943
+ const getY = (v) => plotBottom - (v / maxVal) * plotH;
944
+ const visibleCount = Math.max(2, Math.round(nPoints * progress));
945
+ for (const ds of data.datasets) {
946
+ ctx.save();
947
+ ctx.beginPath();
948
+ ctx.rect(plotLeft, plotTop, plotW, plotH + 1);
949
+ ctx.clip();
950
+ const color = ds.color ?? APEX_COLORS[0];
951
+ ctx.beginPath();
952
+ for (let i = 0; i < Math.min(visibleCount, nPoints); i++) {
953
+ const x = getX(i);
954
+ const y = getY(ds.data[i] ?? 0);
955
+ if (i === 0)
956
+ ctx.moveTo(x, y);
957
+ else
958
+ ctx.lineTo(x, y);
959
+ }
960
+ ctx.lineTo(getX(Math.min(visibleCount, nPoints) - 1), plotBottom);
961
+ ctx.lineTo(getX(0), plotBottom);
962
+ ctx.closePath();
963
+ ctx.fillStyle = color + '26';
964
+ ctx.fill();
965
+ ctx.strokeStyle = color;
966
+ ctx.lineWidth = opts.lineWidth;
967
+ ctx.lineCap = 'round';
968
+ ctx.lineJoin = 'round';
969
+ ctx.beginPath();
970
+ for (let i = 0; i < Math.min(visibleCount, nPoints); i++) {
971
+ const x = getX(i);
972
+ const y = getY(ds.data[i] ?? 0);
973
+ if (i === 0)
974
+ ctx.moveTo(x, y);
975
+ else
976
+ ctx.lineTo(x, y);
977
+ }
978
+ ctx.stroke();
979
+ ctx.restore();
980
+ }
981
+ const labelStep = this.getLabelStep(nPoints, plotW, opts.fontSize);
982
+ ctx.globalAlpha = labelAlpha;
983
+ ctx.fillStyle = opts.textColor;
984
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
985
+ ctx.textAlign = 'center';
986
+ for (let i = 0; i < nPoints; i++) {
987
+ if (i % labelStep === 0)
988
+ ctx.fillText(this.truncate(data.labels[i], 10), getX(i), plotBottom + 16);
989
+ }
990
+ ctx.globalAlpha = 1;
991
+ }
992
+ // ── Pie / Doughnut ──────────────────────────────────────────────────────────
993
+ drawPie(data, opts, progress, isDoughnut) {
994
+ const { ctx } = this;
995
+ const legendH = opts.showLegend ? 40 : 0;
996
+ const cx = opts.width / 2;
997
+ const cy = (opts.height - legendH) / 2;
998
+ const radius = Math.min(opts.width, opts.height - legendH) / 2 - 20;
999
+ const innerRadius = isDoughnut ? radius * 0.5 : 0;
1000
+ const values = data.datasets[0]?.data ?? [];
1001
+ const total = values.reduce((a, b) => a + b, 0) || 1;
1002
+ let startAngle = -Math.PI / 2;
1003
+ for (let i = 0; i < values.length; i++) {
1004
+ const sliceAngle = (values[i] / total) * Math.PI * 2 * progress;
1005
+ const color = APEX_COLORS[i % APEX_COLORS.length];
1006
+ ctx.beginPath();
1007
+ ctx.moveTo(cx, cy);
1008
+ ctx.arc(cx, cy, radius, startAngle, startAngle + sliceAngle);
1009
+ if (innerRadius > 0)
1010
+ ctx.arc(cx, cy, innerRadius, startAngle + sliceAngle, startAngle, true);
1011
+ ctx.closePath();
1012
+ ctx.fillStyle = color;
1013
+ ctx.fill();
1014
+ ctx.strokeStyle = '#fff';
1015
+ ctx.lineWidth = 2;
1016
+ ctx.stroke();
1017
+ if (progress === 1 && values[i] / total > 0.05) {
1018
+ const mid = startAngle + sliceAngle / 2;
1019
+ const lr = isDoughnut ? (radius + innerRadius) / 2 : radius * 0.65;
1020
+ ctx.fillStyle = '#fff';
1021
+ ctx.font = `bold ${opts.fontSize}px ${opts.fontFamily}`;
1022
+ ctx.textAlign = 'center';
1023
+ ctx.textBaseline = 'middle';
1024
+ ctx.fillText(`${Math.round((values[i] / total) * 100)}%`, cx + Math.cos(mid) * lr, cy + Math.sin(mid) * lr);
1025
+ ctx.textBaseline = 'alphabetic';
1026
+ }
1027
+ startAngle += sliceAngle;
1028
+ }
1029
+ if (progress > 0.9 && opts.showLegend && data.labels.length <= 8) {
1030
+ const alpha = (progress - 0.9) / 0.1;
1031
+ ctx.globalAlpha = alpha;
1032
+ startAngle = -Math.PI / 2;
1033
+ for (let i = 0; i < values.length; i++) {
1034
+ const sliceAngle = (values[i] / total) * Math.PI * 2;
1035
+ const mid = startAngle + sliceAngle / 2;
1036
+ const lx = cx + Math.cos(mid) * (radius + 16);
1037
+ const ly = cy + Math.sin(mid) * (radius + 16);
1038
+ ctx.fillStyle = opts.textColor;
1039
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1040
+ ctx.textAlign = Math.cos(mid) > 0 ? 'left' : 'right';
1041
+ ctx.textBaseline = 'middle';
1042
+ ctx.fillText(this.truncate(data.labels[i], 12), lx, ly);
1043
+ ctx.textBaseline = 'alphabetic';
1044
+ startAngle += sliceAngle;
1045
+ }
1046
+ ctx.globalAlpha = 1;
1047
+ }
1048
+ }
1049
+ // ── Scatter ─────────────────────────────────────────────────────────────────
1050
+ drawScatter(data, opts, progress) {
1051
+ const { ctx } = this;
1052
+ const { plotLeft, plotTop, plotBottom, plotRight, plotW, plotH } = this.getPlotArea(opts, data);
1053
+ const rawMax = Math.max(...data.datasets.flatMap((d) => d.data), 0);
1054
+ const maxVal = this.niceMax(rawMax);
1055
+ const nPoints = data.labels.length;
1056
+ const labelAlpha = Math.min(1, progress * 2.5);
1057
+ if (opts.showGrid)
1058
+ this.drawGridLines(plotLeft, plotTop, plotW, plotH, maxVal, opts);
1059
+ else
1060
+ this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
1061
+ const getX = (i) => plotLeft + (i / Math.max(1, nPoints - 1)) * plotW;
1062
+ const getY = (v) => plotBottom - (v / maxVal) * plotH;
1063
+ const visibleCount = Math.round(nPoints * progress);
1064
+ for (let di = 0; di < data.datasets.length; di++) {
1065
+ const ds = data.datasets[di];
1066
+ const color = ds.color ?? APEX_COLORS[di % APEX_COLORS.length];
1067
+ for (let i = 0; i < Math.min(visibleCount, nPoints); i++) {
1068
+ ctx.beginPath();
1069
+ ctx.arc(getX(i), getY(ds.data[i] ?? 0), 5, 0, Math.PI * 2);
1070
+ ctx.fillStyle = color + 'CC';
1071
+ ctx.fill();
1072
+ ctx.strokeStyle = color;
1073
+ ctx.lineWidth = 1.5;
1074
+ ctx.stroke();
1075
+ }
1076
+ }
1077
+ const labelStep = this.getLabelStep(nPoints, plotW, opts.fontSize);
1078
+ ctx.globalAlpha = labelAlpha;
1079
+ ctx.fillStyle = opts.textColor;
1080
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1081
+ ctx.textAlign = 'center';
1082
+ for (let i = 0; i < nPoints; i++) {
1083
+ if (i % labelStep === 0)
1084
+ ctx.fillText(this.truncate(data.labels[i], 10), getX(i), plotBottom + 16);
1085
+ }
1086
+ ctx.globalAlpha = 1;
1087
+ }
1088
+ // ── Polar / Radar ────────────────────────────────────────────────────────────
1089
+ drawPolar(data, opts, progress) {
1090
+ const { ctx } = this;
1091
+ const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
1092
+ const cx = opts.width / 2;
1093
+ const cy = (opts.height - legendH) / 2;
1094
+ const radius = Math.min(opts.width, opts.height - legendH) / 2 - 30;
1095
+ const labels = data.labels;
1096
+ const nSpokes = labels.length;
1097
+ if (nSpokes < 3)
1098
+ return;
1099
+ const allValues = data.datasets.flatMap((d) => d.data);
1100
+ const maxVal = this.niceMax(Math.max(...allValues, 0));
1101
+ ctx.strokeStyle = opts.gridColor;
1102
+ ctx.lineWidth = 1;
1103
+ ctx.setLineDash([4, 4]);
1104
+ for (let ring = 1; ring <= 4; ring++) {
1105
+ ctx.beginPath();
1106
+ const r = (ring / 4) * radius;
1107
+ for (let i = 0; i < nSpokes; i++) {
1108
+ const angle = (i / nSpokes) * Math.PI * 2 - Math.PI / 2;
1109
+ const x = cx + Math.cos(angle) * r;
1110
+ const y = cy + Math.sin(angle) * r;
1111
+ if (i === 0)
1112
+ ctx.moveTo(x, y);
1113
+ else
1114
+ ctx.lineTo(x, y);
1115
+ }
1116
+ ctx.closePath();
1117
+ ctx.stroke();
1118
+ }
1119
+ ctx.setLineDash([]);
1120
+ for (let i = 0; i < nSpokes; i++) {
1121
+ const angle = (i / nSpokes) * Math.PI * 2 - Math.PI / 2;
1122
+ ctx.strokeStyle = opts.gridColor;
1123
+ ctx.lineWidth = 1;
1124
+ ctx.beginPath();
1125
+ ctx.moveTo(cx, cy);
1126
+ ctx.lineTo(cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius);
1127
+ ctx.stroke();
1128
+ ctx.globalAlpha = Math.min(1, progress * 2.5);
1129
+ ctx.fillStyle = opts.textColor;
1130
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1131
+ ctx.textAlign = 'center';
1132
+ ctx.textBaseline = 'middle';
1133
+ ctx.fillText(this.truncate(labels[i], 10), cx + Math.cos(angle) * (radius + 14), cy + Math.sin(angle) * (radius + 14));
1134
+ ctx.textBaseline = 'alphabetic';
1135
+ ctx.globalAlpha = 1;
1136
+ }
1137
+ for (let di = 0; di < data.datasets.length; di++) {
1138
+ const ds = data.datasets[di];
1139
+ const color = ds.color ?? APEX_COLORS[di % APEX_COLORS.length];
1140
+ ctx.beginPath();
1141
+ for (let i = 0; i < nSpokes; i++) {
1142
+ const angle = (i / nSpokes) * Math.PI * 2 - Math.PI / 2;
1143
+ const val = ((ds.data[i] ?? 0) / maxVal) * radius * progress;
1144
+ const x = cx + Math.cos(angle) * val;
1145
+ const y = cy + Math.sin(angle) * val;
1146
+ if (i === 0)
1147
+ ctx.moveTo(x, y);
1148
+ else
1149
+ ctx.lineTo(x, y);
1150
+ }
1151
+ ctx.closePath();
1152
+ ctx.fillStyle = color + '33';
1153
+ ctx.fill();
1154
+ ctx.strokeStyle = color;
1155
+ ctx.lineWidth = 2;
1156
+ ctx.stroke();
1157
+ }
1158
+ }
1159
+ // ── Funnel ──────────────────────────────────────────────────────────────────
1160
+ drawFunnel(data, opts, progress) {
1161
+ const { ctx } = this;
1162
+ const labels = data.labels;
1163
+ const values = data.datasets[0]?.data ?? [];
1164
+ const maxVal = Math.max(...values, 0) || 1;
1165
+ const n = labels.length;
1166
+ const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
1167
+ const labelAreaW = 90;
1168
+ const plotLeft = labelAreaW;
1169
+ const plotRight = opts.width - 20;
1170
+ const plotWidth = plotRight - plotLeft;
1171
+ const plotTop = 10;
1172
+ const plotBottom = opts.height - legendH - 10;
1173
+ const plotH = plotBottom - plotTop;
1174
+ const itemH = Math.max((plotH / Math.max(n, 1)) - 2, 1);
1175
+ const cx = plotLeft + plotWidth / 2;
1176
+ const labelAlpha = Math.min(1, progress * 2.5);
1177
+ for (let i = 0; i < n; i++) {
1178
+ const pct = (values[i] ?? 0) / maxVal;
1179
+ const barW = pct * plotWidth * progress;
1180
+ const barX = cx - barW / 2;
1181
+ const barY = plotTop + i * (itemH + 2);
1182
+ const color = APEX_COLORS[i % APEX_COLORS.length];
1183
+ ctx.fillStyle = color;
1184
+ ctx.beginPath();
1185
+ ctx.roundRect(barX, barY, Math.max(barW, 0), itemH, 3);
1186
+ ctx.fill();
1187
+ ctx.globalAlpha = labelAlpha;
1188
+ ctx.fillStyle = opts.textColor;
1189
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1190
+ ctx.textAlign = 'right';
1191
+ ctx.fillText(this.truncate(labels[i], 12), labelAreaW - 6, barY + itemH / 2 + opts.fontSize / 3);
1192
+ ctx.globalAlpha = 1;
1193
+ if (progress === 1 && barW > 50) {
1194
+ ctx.fillStyle = '#fff';
1195
+ ctx.textAlign = 'center';
1196
+ ctx.fillText(this.formatNum(values[i] ?? 0), cx, barY + itemH / 2 + opts.fontSize / 3);
1197
+ }
1198
+ }
1199
+ }
1200
+ // ── Grid Lines ──────────────────────────────────────────────────────────────
1201
+ drawGridLines(left, top, width, height, maxVal, opts) {
1202
+ const { ctx } = this;
1203
+ const steps = 5;
1204
+ ctx.strokeStyle = opts.gridColor;
1205
+ ctx.lineWidth = 1;
1206
+ ctx.setLineDash([4, 4]);
1207
+ for (let i = 0; i <= steps; i++) {
1208
+ const y = top + height - (i / steps) * height;
1209
+ ctx.beginPath();
1210
+ ctx.moveTo(left, y);
1211
+ ctx.lineTo(left + width, y);
1212
+ ctx.stroke();
1213
+ ctx.fillStyle = opts.textColor;
1214
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1215
+ ctx.textAlign = 'right';
1216
+ ctx.fillText(this.formatNum((maxVal * i) / steps), left - 6, y + 4);
1217
+ }
1218
+ ctx.setLineDash([]);
1219
+ ctx.strokeStyle = opts.gridColor;
1220
+ ctx.lineWidth = 1.5;
1221
+ ctx.beginPath();
1222
+ ctx.moveTo(left, top);
1223
+ ctx.lineTo(left, top + height);
1224
+ ctx.stroke();
1225
+ ctx.beginPath();
1226
+ ctx.moveTo(left, top + height);
1227
+ ctx.lineTo(left + width, top + height);
1228
+ ctx.stroke();
1229
+ }
1230
+ /**
1231
+ * Draws relative (0–100 %) Y-axis gridlines for multi-series charts where each
1232
+ * dataset is normalised to its own maximum, making the shared axis unitless.
1233
+ */
1234
+ drawGridLinesRelative(left, top, width, height, opts) {
1235
+ const { ctx } = this;
1236
+ const steps = 4;
1237
+ ctx.strokeStyle = opts.gridColor;
1238
+ ctx.lineWidth = 1;
1239
+ ctx.setLineDash([4, 4]);
1240
+ for (let i = 0; i <= steps; i++) {
1241
+ const y = top + height - (i / steps) * height;
1242
+ ctx.beginPath();
1243
+ ctx.moveTo(left, y);
1244
+ ctx.lineTo(left + width, y);
1245
+ ctx.stroke();
1246
+ ctx.fillStyle = opts.textColor;
1247
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1248
+ ctx.textAlign = 'right';
1249
+ ctx.fillText(`${Math.round((100 * i) / steps)}%`, left - 6, y + 4);
1250
+ }
1251
+ ctx.setLineDash([]);
1252
+ ctx.strokeStyle = opts.gridColor;
1253
+ ctx.lineWidth = 1.5;
1254
+ ctx.beginPath();
1255
+ ctx.moveTo(left, top);
1256
+ ctx.lineTo(left, top + height);
1257
+ ctx.stroke();
1258
+ ctx.beginPath();
1259
+ ctx.moveTo(left, top + height);
1260
+ ctx.lineTo(left + width, top + height);
1261
+ ctx.stroke();
1262
+ }
1263
+ // ── Legend ──────────────────────────────────────────────────────────────────
1264
+ drawLegend(data, opts) {
1265
+ const { ctx } = this;
1266
+ const y = opts.height - 20;
1267
+ const totalItems = data.datasets.length;
1268
+ const itemWidth = opts.width / totalItems;
1269
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1270
+ ctx.textAlign = 'left';
1271
+ ctx.textBaseline = 'middle';
1272
+ for (let i = 0; i < totalItems; i++) {
1273
+ const x = i * itemWidth + (itemWidth - 80) / 2;
1274
+ const color = data.datasets[i].color ?? APEX_COLORS[i % APEX_COLORS.length];
1275
+ ctx.fillStyle = color;
1276
+ ctx.beginPath();
1277
+ ctx.roundRect(x, y - 5, 12, 10, 2);
1278
+ ctx.fill();
1279
+ ctx.fillStyle = opts.textColor;
1280
+ ctx.fillText(this.truncate(data.datasets[i].label, 14), x + 16, y);
1281
+ }
1282
+ ctx.textBaseline = 'alphabetic';
1283
+ }
1284
+ // ── Helpers ─────────────────────────────────────────────────────────────────
1285
+ drawAxes(left, top, right, bottom, opts) {
1286
+ const { ctx } = this;
1287
+ ctx.setLineDash([]);
1288
+ ctx.strokeStyle = opts.gridColor;
1289
+ ctx.lineWidth = 1.5;
1290
+ ctx.beginPath();
1291
+ ctx.moveTo(left, top);
1292
+ ctx.lineTo(left, bottom);
1293
+ ctx.stroke();
1294
+ ctx.beginPath();
1295
+ ctx.moveTo(left, bottom);
1296
+ ctx.lineTo(right, bottom);
1297
+ ctx.stroke();
1298
+ }
1299
+ niceMax(rawMax) {
1300
+ if (rawMax <= 0)
1301
+ return 10;
1302
+ const magnitude = Math.pow(10, Math.floor(Math.log10(rawMax)));
1303
+ const steps = [1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 10];
1304
+ for (const s of steps) {
1305
+ if (s * magnitude >= rawMax)
1306
+ return s * magnitude;
1307
+ }
1308
+ return 10 * magnitude;
1309
+ }
1310
+ getLabelStep(count, availablePixels, fontSize) {
1311
+ const approxLabelW = fontSize * 5.5;
1312
+ return Math.max(1, Math.ceil((count * approxLabelW) / availablePixels));
1313
+ }
1314
+ easeOutQuart(t) {
1315
+ return 1 - Math.pow(1 - t, 4);
1316
+ }
1317
+ truncate(str, max) {
1318
+ return str.length > max ? str.slice(0, max - 1) + '…' : str;
1319
+ }
1320
+ formatNum(n) {
1321
+ if (n >= 1e6)
1322
+ return (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1) + 'M';
1323
+ if (n >= 1e3)
1324
+ return (n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1) + 'K';
1325
+ if (n !== Math.floor(n))
1326
+ return n.toFixed(1);
1327
+ return String(Math.round(n));
1328
+ }
1329
+ }
1330
+ //# sourceMappingURL=chart-renderer.js.map