@dodlhuat/basix 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/README.md +706 -482
  2. package/css/accordion.scss +86 -87
  3. package/css/alert.scss +137 -137
  4. package/css/badge.scss +104 -0
  5. package/css/bottom-sheet.scss +192 -0
  6. package/css/breadcrumb.scss +158 -0
  7. package/css/button.scss +48 -0
  8. package/css/calendar.scss +957 -0
  9. package/css/card.scss +65 -65
  10. package/css/chart.scss +270 -157
  11. package/css/chat-bubbles.scss +134 -68
  12. package/css/chips.scss +109 -19
  13. package/css/colors.scss +32 -32
  14. package/css/context-menu.scss +182 -0
  15. package/css/datepicker.scss +336 -336
  16. package/css/defaults.scss +90 -90
  17. package/css/docs.scss +529 -0
  18. package/css/editor.scss +664 -461
  19. package/css/file-uploader.scss +1 -1
  20. package/css/flyout-menu.scss +361 -361
  21. package/css/form.scss +124 -0
  22. package/css/gallery.scss +65 -6
  23. package/css/grid.scss +41 -40
  24. package/css/group-picker.scss +345 -0
  25. package/css/guitar-chords.css +250 -250
  26. package/css/icons.scss +330 -330
  27. package/css/parameters.scss +3 -3
  28. package/css/placeholder.scss +33 -33
  29. package/css/popover.scss +206 -0
  30. package/css/progress.scss +76 -32
  31. package/css/properties.scss +51 -36
  32. package/css/push-menu.scss +302 -174
  33. package/css/reset.scss +39 -39
  34. package/css/scrollbar.scss +62 -5
  35. package/css/sidebar-nav.scss +92 -0
  36. package/css/spinner.scss +65 -65
  37. package/css/stepper.scss +248 -0
  38. package/css/style.css +4603 -273
  39. package/css/style.css.map +1 -1
  40. package/css/style.min.css +1 -1
  41. package/css/style.scss +51 -39
  42. package/css/table.scss +199 -199
  43. package/css/tabs.scss +154 -123
  44. package/css/timeline.scss +83 -38
  45. package/css/timepicker.scss +100 -5
  46. package/css/toast.scss +81 -81
  47. package/css/typography.scss +194 -161
  48. package/css/virtual-dropdown.scss +35 -29
  49. package/js/bottom-sheet.js +173 -0
  50. package/js/bottom-sheet.ts +222 -0
  51. package/js/calendar.js +532 -0
  52. package/js/calendar.ts +706 -0
  53. package/js/carousel.js +26 -13
  54. package/js/chart.js +573 -257
  55. package/js/chart.ts +692 -0
  56. package/js/code-viewer.js +10 -10
  57. package/js/code-viewer.ts +188 -188
  58. package/js/context-menu.js +212 -0
  59. package/js/context-menu.ts +252 -0
  60. package/js/datepicker.ts +627 -627
  61. package/js/docs-nav.js +204 -0
  62. package/js/dropdown.ts +179 -179
  63. package/js/editor.js +96 -38
  64. package/js/editor.ts +483 -425
  65. package/js/file-uploader.js +1 -0
  66. package/js/file-uploader.ts +1 -0
  67. package/js/flyout-menu.js +14 -14
  68. package/js/flyout-menu.ts +249 -249
  69. package/js/form-builder.js +106 -106
  70. package/js/gallery.js +13 -6
  71. package/js/gallery.ts +245 -236
  72. package/js/group-picker.js +342 -0
  73. package/js/group-picker.ts +447 -0
  74. package/js/guitar-chords.js +268 -268
  75. package/js/lazy-loader.js +121 -121
  76. package/js/modal.ts +166 -166
  77. package/js/popover.js +163 -0
  78. package/js/popover.ts +219 -0
  79. package/js/position.js +108 -0
  80. package/js/position.ts +111 -0
  81. package/js/push-menu.js +226 -113
  82. package/js/push-menu.ts +284 -145
  83. package/js/request.js +50 -50
  84. package/js/scroll.ts +47 -47
  85. package/js/scrollbar.js +13 -0
  86. package/js/scrollbar.ts +324 -307
  87. package/js/select.ts +216 -216
  88. package/js/sidebar-nav.js +41 -0
  89. package/js/sidebar-nav.ts +66 -0
  90. package/js/stepper.js +80 -0
  91. package/js/stepper.ts +104 -0
  92. package/js/table.ts +452 -452
  93. package/js/tabs.ts +279 -279
  94. package/js/theme.js +17 -6
  95. package/js/theme.ts +234 -224
  96. package/js/timepicker.js +21 -8
  97. package/js/toast.ts +137 -137
  98. package/js/tooltip.js +6 -60
  99. package/js/tooltip.ts +184 -251
  100. package/js/tsconfig.json +18 -18
  101. package/js/utils.ts +83 -83
  102. package/js/virtual-dropdown.js +25 -25
  103. package/js/virtual-dropdown.ts +365 -365
  104. package/package.json +39 -39
  105. package/fonts/Outfit-VariableFont_wght.woff +0 -0
  106. package/fonts/material-icons.woff2 +0 -0
  107. package/icons/activity-outline.svg +0 -1
  108. package/icons/alert-circle-outline.svg +0 -1
  109. package/icons/alert-triangle-outline.svg +0 -1
  110. package/icons/archive-outline.svg +0 -1
  111. package/icons/arrow-back-outline.svg +0 -1
  112. package/icons/arrow-circle-down-outline.svg +0 -1
  113. package/icons/arrow-circle-left-outline.svg +0 -1
  114. package/icons/arrow-circle-right-outline.svg +0 -1
  115. package/icons/arrow-circle-up-outline.svg +0 -1
  116. package/icons/arrow-down-outline.svg +0 -1
  117. package/icons/arrow-downward-outline.svg +0 -1
  118. package/icons/arrow-forward-outline.svg +0 -1
  119. package/icons/arrow-ios-back-outline.svg +0 -1
  120. package/icons/arrow-ios-downward-outline.svg +0 -1
  121. package/icons/arrow-ios-forward-outline.svg +0 -1
  122. package/icons/arrow-ios-upward-outline.svg +0 -1
  123. package/icons/arrow-left-outline.svg +0 -1
  124. package/icons/arrow-right-outline.svg +0 -1
  125. package/icons/arrow-up-outline.svg +0 -1
  126. package/icons/arrow-upward-outline.svg +0 -1
  127. package/icons/arrowhead-down-outline.svg +0 -1
  128. package/icons/arrowhead-left-outline.svg +0 -1
  129. package/icons/arrowhead-right-outline.svg +0 -1
  130. package/icons/arrowhead-up-outline.svg +0 -1
  131. package/icons/at-outline.svg +0 -1
  132. package/icons/attach-2-outline.svg +0 -1
  133. package/icons/attach-outline.svg +0 -1
  134. package/icons/award-outline.svg +0 -1
  135. package/icons/backspace-outline.svg +0 -1
  136. package/icons/bar-chart-2-outline.svg +0 -1
  137. package/icons/bar-chart-outline.svg +0 -1
  138. package/icons/battery-outline.svg +0 -1
  139. package/icons/behance-outline.svg +0 -1
  140. package/icons/bell-off-outline.svg +0 -1
  141. package/icons/bell-outline.svg +0 -1
  142. package/icons/bluetooth-outline.svg +0 -1
  143. package/icons/book-open-outline.svg +0 -1
  144. package/icons/book-outline.svg +0 -1
  145. package/icons/bookmark-outline.svg +0 -1
  146. package/icons/briefcase-outline.svg +0 -1
  147. package/icons/browser-outline.svg +0 -1
  148. package/icons/brush-outline.svg +0 -1
  149. package/icons/bulb-outline.svg +0 -1
  150. package/icons/calendar-outline.svg +0 -1
  151. package/icons/camera-outline.svg +0 -1
  152. package/icons/car-outline.svg +0 -1
  153. package/icons/cast-outline.svg +0 -1
  154. package/icons/charging-outline.svg +0 -1
  155. package/icons/checkmark-circle-2-outline.svg +0 -1
  156. package/icons/checkmark-circle-outline.svg +0 -1
  157. package/icons/checkmark-outline.svg +0 -1
  158. package/icons/checkmark-square-2-outline.svg +0 -1
  159. package/icons/checkmark-square-outline.svg +0 -1
  160. package/icons/chevron-down-outline.svg +0 -1
  161. package/icons/chevron-left-outline.svg +0 -1
  162. package/icons/chevron-right-outline.svg +0 -1
  163. package/icons/chevron-up-outline.svg +0 -1
  164. package/icons/clipboard-outline.svg +0 -1
  165. package/icons/clock-outline.svg +0 -1
  166. package/icons/close-circle-outline.svg +0 -1
  167. package/icons/close-outline.svg +0 -1
  168. package/icons/close-square-outline.svg +0 -1
  169. package/icons/cloud-download-outline.svg +0 -1
  170. package/icons/cloud-upload-outline.svg +0 -1
  171. package/icons/code-download-outline.svg +0 -1
  172. package/icons/code-outline.svg +0 -1
  173. package/icons/collapse-outline.svg +0 -1
  174. package/icons/color-palette-outline.svg +0 -1
  175. package/icons/color-picker-outline.svg +0 -1
  176. package/icons/compass-outline.svg +0 -1
  177. package/icons/copy-outline.svg +0 -1
  178. package/icons/corner-down-left-outline.svg +0 -1
  179. package/icons/corner-down-right-outline.svg +0 -1
  180. package/icons/corner-left-down-outline.svg +0 -1
  181. package/icons/corner-left-up-outline.svg +0 -1
  182. package/icons/corner-right-down-outline.svg +0 -1
  183. package/icons/corner-right-up-outline.svg +0 -1
  184. package/icons/corner-up-left-outline.svg +0 -1
  185. package/icons/corner-up-right-outline.svg +0 -1
  186. package/icons/credit-card-outline.svg +0 -1
  187. package/icons/crop-outline.svg +0 -1
  188. package/icons/cube-outline.svg +0 -1
  189. package/icons/diagonal-arrow-left-down-outline.svg +0 -1
  190. package/icons/diagonal-arrow-left-up-outline.svg +0 -1
  191. package/icons/diagonal-arrow-right-down-outline.svg +0 -1
  192. package/icons/diagonal-arrow-right-up-outline.svg +0 -1
  193. package/icons/done-all-outline.svg +0 -1
  194. package/icons/download-outline.svg +0 -1
  195. package/icons/droplet-off-outline.svg +0 -1
  196. package/icons/droplet-outline.svg +0 -1
  197. package/icons/edit-2-outline.svg +0 -1
  198. package/icons/edit-outline.svg +0 -1
  199. package/icons/email-outline.svg +0 -1
  200. package/icons/expand-outline.svg +0 -1
  201. package/icons/external-link-outline.svg +0 -1
  202. package/icons/eye-off-2-outline.svg +0 -1
  203. package/icons/eye-off-outline.svg +0 -1
  204. package/icons/eye-outline.svg +0 -1
  205. package/icons/facebook-outline.svg +0 -1
  206. package/icons/file-add-outline.svg +0 -1
  207. package/icons/file-outline.svg +0 -1
  208. package/icons/file-remove-outline.svg +0 -1
  209. package/icons/file-text-outline.svg +0 -1
  210. package/icons/film-outline.svg +0 -1
  211. package/icons/flag-outline.svg +0 -1
  212. package/icons/flash-off-outline.svg +0 -1
  213. package/icons/flash-outline.svg +0 -1
  214. package/icons/flip-2-outline.svg +0 -1
  215. package/icons/flip-outline.svg +0 -1
  216. package/icons/folder-add-outline.svg +0 -1
  217. package/icons/folder-outline.svg +0 -1
  218. package/icons/folder-remove-outline.svg +0 -1
  219. package/icons/funnel-outline.svg +0 -1
  220. package/icons/gift-outline.svg +0 -1
  221. package/icons/github-outline.svg +0 -1
  222. package/icons/globe-2-outline.svg +0 -1
  223. package/icons/globe-outline.svg +0 -1
  224. package/icons/google-outline.svg +0 -1
  225. package/icons/grid-outline.svg +0 -1
  226. package/icons/hard-drive-outline.svg +0 -1
  227. package/icons/hash-outline.svg +0 -1
  228. package/icons/headphones-outline.svg +0 -1
  229. package/icons/heart-outline.svg +0 -1
  230. package/icons/home-outline.svg +0 -1
  231. package/icons/image-outline.svg +0 -1
  232. package/icons/inbox-outline.svg +0 -1
  233. package/icons/info-outline.svg +0 -1
  234. package/icons/keypad-outline.svg +0 -1
  235. package/icons/layers-outline.svg +0 -1
  236. package/icons/layout-outline.svg +0 -1
  237. package/icons/link-2-outline.svg +0 -1
  238. package/icons/link-outline.svg +0 -1
  239. package/icons/linkedin-outline.svg +0 -1
  240. package/icons/list-outline.svg +0 -1
  241. package/icons/loader-outline.svg +0 -1
  242. package/icons/lock-outline.svg +0 -1
  243. package/icons/log-in-outline.svg +0 -1
  244. package/icons/log-out-outline.svg +0 -1
  245. package/icons/map-outline.svg +0 -1
  246. package/icons/maximize-outline.svg +0 -1
  247. package/icons/menu-2-outline.svg +0 -1
  248. package/icons/menu-arrow-outline.svg +0 -1
  249. package/icons/menu-outline.svg +0 -1
  250. package/icons/message-circle-outline.svg +0 -1
  251. package/icons/message-square-outline.svg +0 -1
  252. package/icons/mic-off-outline.svg +0 -1
  253. package/icons/mic-outline.svg +0 -1
  254. package/icons/minimize-outline.svg +0 -1
  255. package/icons/minus-circle-outline.svg +0 -1
  256. package/icons/minus-outline.svg +0 -1
  257. package/icons/minus-square-outline.svg +0 -1
  258. package/icons/monitor-outline.svg +0 -1
  259. package/icons/moon-outline.svg +0 -1
  260. package/icons/more-horizontal-outline.svg +0 -1
  261. package/icons/more-vertical-outline.svg +0 -1
  262. package/icons/move-outline.svg +0 -1
  263. package/icons/music-outline.svg +0 -1
  264. package/icons/navigation-2-outline.svg +0 -1
  265. package/icons/navigation-outline.svg +0 -1
  266. package/icons/npm-outline.svg +0 -1
  267. package/icons/options-2-outline.svg +0 -1
  268. package/icons/options-outline.svg +0 -1
  269. package/icons/pantone-outline.svg +0 -1
  270. package/icons/paper-plane-outline.svg +0 -1
  271. package/icons/pause-circle-outline.svg +0 -1
  272. package/icons/people-outline.svg +0 -1
  273. package/icons/percent-outline.svg +0 -1
  274. package/icons/person-add-outline.svg +0 -1
  275. package/icons/person-delete-outline.svg +0 -1
  276. package/icons/person-done-outline.svg +0 -1
  277. package/icons/person-outline.svg +0 -1
  278. package/icons/person-remove-outline.svg +0 -1
  279. package/icons/phone-call-outline.svg +0 -1
  280. package/icons/phone-missed-outline.svg +0 -1
  281. package/icons/phone-off-outline.svg +0 -1
  282. package/icons/phone-outline.svg +0 -1
  283. package/icons/pie-chart-outline.svg +0 -1
  284. package/icons/pin-outline.svg +0 -1
  285. package/icons/play-circle-outline.svg +0 -1
  286. package/icons/plus-circle-outline.svg +0 -1
  287. package/icons/plus-outline.svg +0 -1
  288. package/icons/plus-square-outline.svg +0 -1
  289. package/icons/power-outline.svg +0 -1
  290. package/icons/pricetags-outline.svg +0 -1
  291. package/icons/printer-outline.svg +0 -1
  292. package/icons/question-mark-circle-outline.svg +0 -1
  293. package/icons/question-mark-outline.svg +0 -1
  294. package/icons/radio-button-off-outline.svg +0 -1
  295. package/icons/radio-button-on-outline.svg +0 -1
  296. package/icons/radio-outline.svg +0 -1
  297. package/icons/recording-outline.svg +0 -1
  298. package/icons/refresh-outline.svg +0 -1
  299. package/icons/repeat-outline.svg +0 -1
  300. package/icons/rewind-left-outline.svg +0 -1
  301. package/icons/rewind-right-outline.svg +0 -1
  302. package/icons/save-outline.svg +0 -1
  303. package/icons/scissors-outline.svg +0 -1
  304. package/icons/search-outline.svg +0 -1
  305. package/icons/settings-2-outline.svg +0 -1
  306. package/icons/settings-outline.svg +0 -1
  307. package/icons/shake-outline.svg +0 -1
  308. package/icons/share-outline.svg +0 -1
  309. package/icons/shield-off-outline.svg +0 -1
  310. package/icons/shield-outline.svg +0 -1
  311. package/icons/shopping-bag-outline.svg +0 -1
  312. package/icons/shopping-cart-outline.svg +0 -1
  313. package/icons/shuffle-2-outline.svg +0 -1
  314. package/icons/shuffle-outline.svg +0 -1
  315. package/icons/skip-back-outline.svg +0 -1
  316. package/icons/skip-forward-outline.svg +0 -1
  317. package/icons/slash-outline.svg +0 -1
  318. package/icons/smartphone-outline.svg +0 -1
  319. package/icons/smiling-face-outline.svg +0 -1
  320. package/icons/speaker-outline.svg +0 -1
  321. package/icons/square-outline.svg +0 -1
  322. package/icons/star-outline.svg +0 -1
  323. package/icons/stop-circle-outline.svg +0 -1
  324. package/icons/sun-outline.svg +0 -1
  325. package/icons/swap-outline.svg +0 -1
  326. package/icons/sync-outline.svg +0 -1
  327. package/icons/text-outline.svg +0 -1
  328. package/icons/thermometer-minus-outline.svg +0 -1
  329. package/icons/thermometer-outline.svg +0 -1
  330. package/icons/thermometer-plus-outline.svg +0 -1
  331. package/icons/toggle-left-outline.svg +0 -1
  332. package/icons/toggle-right-outline.svg +0 -1
  333. package/icons/trash-2-outline.svg +0 -1
  334. package/icons/trash-outline.svg +0 -1
  335. package/icons/trending-down-outline.svg +0 -1
  336. package/icons/trending-up-outline.svg +0 -1
  337. package/icons/tv-outline.svg +0 -1
  338. package/icons/twitter-outline.svg +0 -1
  339. package/icons/umbrella-outline.svg +0 -1
  340. package/icons/undo-outline.svg +0 -1
  341. package/icons/unlock-outline.svg +0 -1
  342. package/icons/upload-outline.svg +0 -1
  343. package/icons/video-off-outline.svg +0 -1
  344. package/icons/video-outline.svg +0 -1
  345. package/icons/volume-down-outline.svg +0 -1
  346. package/icons/volume-mute-outline.svg +0 -1
  347. package/icons/volume-off-outline.svg +0 -1
  348. package/icons/volume-up-outline.svg +0 -1
  349. package/icons/wifi-off-outline.svg +0 -1
  350. package/icons/wifi-outline.svg +0 -1
  351. package/js/index.js +0 -718
  352. package/js/index.ts +0 -873
package/js/chart.js CHANGED
@@ -1,257 +1,573 @@
1
- class ChartConfig {
2
- constructor() {
3
- this.minVal = 19;
4
- this.maxVal = 81;
5
- this.gridLines = [19, 27, 34, 38, 41, 45, 47, 50, 53, 55, 59, 62, 66, 73, 81];
6
- this.zoneBoundaries = [43, 55]; // Left < 43, 43 <= Mid <= 55, Right > 55
7
- this.rowHeight = 40; // Must match CSS .chart-row height
8
- this.headerHeight = 40; // Must match CSS #chart-container padding-top
9
- this.infoColumnWidth = 350; // Must match CSS .row-info width
10
- this.valueSpans = [
11
- {min: 19, max: 23, text: "Bad"},
12
- {min: 24, max: 38, text: "Better"},
13
- {min: 39, max: 44, text: "Ok"},
14
- {min: 45, max: 55, text: "Good"},
15
- {min: 56, max: 81, text: "Excellent"}
16
- ];
17
- }
18
- }
19
-
20
- class Row {
21
- constructor(data) {
22
- this.id = data.id;
23
- this.position = data.position;
24
- this.name = data.name;
25
- this.description = data.description || '';
26
- this.value = data.value;
27
- }
28
-
29
- render() {
30
- const div = document.createElement('div');
31
- div.className = 'chart-row';
32
- div.innerHTML = `
33
- <div class="row-info">
34
- <div class="row-name">${this.name}</div>
35
- <div class="row-meta">${this.description ? this.description : ''}</div>
36
- </div>
37
- `;
38
- return div;
39
- }
40
- }
41
-
42
- class Chart {
43
- constructor(containerId, data) {
44
- this.container = document.getElementById(containerId);
45
- this.data = data.map(d => new Row(d));
46
- this.config = new ChartConfig();
47
-
48
- // Sort data by position just in case
49
- this.data.sort((a, b) => a.position - b.position);
50
-
51
- // Create tooltip element
52
- this.tooltip = document.createElement('div');
53
- this.tooltip.className = 'chart-tooltip';
54
- this.tooltip.style.display = 'none';
55
- document.body.appendChild(this.tooltip);
56
-
57
- // Handle resize
58
- window.addEventListener('resize', () => {
59
- this.render();
60
- });
61
- }
62
-
63
- // Helper to map value to percentage width
64
- // The chart area starts after the info column.
65
- // But wait, the prompt implies the values 19-81 are the positions.
66
- // If the grid lines are fixed at 19...81, then the X axis represents this range.
67
- // So 19 is 0% (or left edge of graph area) and 81 is 100% (or right edge).
68
- getPercentage(value) {
69
- const range = this.config.maxVal - this.config.minVal;
70
- // Clamp value
71
- const clamped = Math.max(this.config.minVal, Math.min(this.config.maxVal, value));
72
- return ((clamped - this.config.minVal) / range) * 100;
73
- }
74
-
75
- updateRowValue(id, newValue) {
76
- const row = this.data.find(r => r.id === id);
77
- if (row) {
78
- row.value = Math.max(this.config.minVal, Math.min(this.config.maxVal, newValue));
79
- this.render(); // Re-render to update line and dots
80
- }
81
- }
82
-
83
- render() {
84
- this.container.innerHTML = '';
85
-
86
- // 1. Render Background Zones
87
- this.renderBackgroundZones();
88
-
89
- // 2. Render Grid Lines
90
- this.renderGridLines();
91
-
92
- // 3. Render Content Rows
93
- const contentLayer = document.createElement('div');
94
- contentLayer.className = 'chart-content';
95
- this.data.forEach(row => {
96
- contentLayer.appendChild(row.render());
97
- });
98
- this.container.appendChild(contentLayer);
99
-
100
- // 4. Render Connecting Line (SVG)
101
- this.renderLine();
102
- }
103
-
104
- renderBackgroundZones() {
105
- const bgLayer = document.createElement('div');
106
- bgLayer.className = 'chart-background';
107
-
108
- // Calculate widths based on boundaries
109
- // Range: 19 to 81.
110
- // Zone 1: 19 to 43
111
- // Zone 2: 43 to 55
112
- // Zone 3: 55 to 81
113
-
114
- const width1 = this.getPercentage(this.config.zoneBoundaries[0]);
115
- const width2 = this.getPercentage(this.config.zoneBoundaries[1]) - width1;
116
- const width3 = 100 - (width1 + width2);
117
-
118
- // We need to account for the info column width which is not part of the graph area?
119
- // Actually, usually in these charts, the grid is the background of the whole row or just the right side?
120
- // Let's assume the graph area is to the right of the info column.
121
- // We will use CSS calc or absolute positioning.
122
- // To make it simple, let's make the background layer only cover the graph area.
123
- // The graph area width is calc(100% - 200px).
124
-
125
- bgLayer.style.left = `${this.config.infoColumnWidth}px`;
126
- bgLayer.style.width = `calc(100% - ${this.config.infoColumnWidth}px)`;
127
-
128
- const zone1 = document.createElement('div');
129
- zone1.className = 'zone zone-left';
130
- zone1.style.width = `${width1}%`;
131
-
132
- const zone2 = document.createElement('div');
133
- zone2.className = 'zone zone-mid';
134
- zone2.style.width = `${width2}%`;
135
-
136
- const zone3 = document.createElement('div');
137
- zone3.className = 'zone zone-right';
138
- zone3.style.width = `${width3}%`;
139
-
140
- bgLayer.appendChild(zone1);
141
- bgLayer.appendChild(zone2);
142
- bgLayer.appendChild(zone3);
143
-
144
- this.container.appendChild(bgLayer);
145
- }
146
-
147
- renderGridLines() {
148
- const gridLayer = document.createElement('div');
149
- gridLayer.className = 'grid-lines';
150
- gridLayer.style.left = `${this.config.infoColumnWidth}px`;
151
- gridLayer.style.width = `calc(100% - ${this.config.infoColumnWidth}px)`;
152
-
153
- this.config.gridLines.forEach(val => {
154
- const line = document.createElement('div');
155
- line.className = 'grid-line';
156
- line.style.left = `${this.getPercentage(val)}%`;
157
-
158
- const label = document.createElement('div');
159
- label.className = 'grid-label';
160
- label.innerText = val;
161
- label.style.left = `${this.getPercentage(val)}%`;
162
-
163
- gridLayer.appendChild(line);
164
- gridLayer.appendChild(label);
165
- });
166
-
167
- this.container.appendChild(gridLayer);
168
- }
169
-
170
- renderLine() {
171
- const svgLayer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
172
- svgLayer.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
173
- svgLayer.setAttribute('class', 'chart-svg-layer');
174
- // The SVG covers the whole container, but we need to map points to the graph area.
175
-
176
- const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
177
- polyline.classList.add('chart-polyline');
178
-
179
- const points = [];
180
-
181
- // We need the actual width of the graph area to calculate X coordinates in pixels
182
- // or we can use percentage coordinates if we set viewBox?
183
- // Let's use getBoundingClientRect for simplicity to get pixels,
184
- // but that requires the element to be in DOM. It is.
185
-
186
- // Wait for layout? No, we can just calculate based on percentages if we use percentage in SVG?
187
- // SVG doesn't support percentage points easily in polyline without viewBox.
188
- // Let's use a resize observer or just calculate once.
189
-
190
- const graphAreaWidth = this.container.clientWidth - this.config.infoColumnWidth;
191
-
192
- this.data.forEach((row, index) => {
193
- // X calculation
194
- const pct = this.getPercentage(row.value);
195
- const x = this.config.infoColumnWidth + (graphAreaWidth * (pct / 100));
196
-
197
- // Y calculation: headerHeight + (index * rowHeight) + (rowHeight / 2)
198
- const y = this.config.headerHeight + (index * this.config.rowHeight) + (this.config.rowHeight / 2);
199
-
200
- points.push(`${x},${y}`);
201
-
202
- // Draw circle for point
203
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
204
- circle.classList.add('chart-point');
205
- circle.setAttribute('cx', x);
206
- circle.setAttribute('cy', y);
207
- circle.setAttribute('r', 10); // Increased radius
208
- circle.setAttribute('fill', '#d32f2f'); // Direct fill color
209
- circle.setAttribute('stroke', 'white');
210
- circle.setAttribute('stroke-width', '2');
211
- circle.style.pointerEvents = 'all'; // Enable pointer events for hover
212
-
213
- // Add hover events
214
- circle.addEventListener('mouseover', (e) => this.showTooltip(e, row.value));
215
- circle.addEventListener('mouseout', () => this.hideTooltip());
216
-
217
- // Add title for hover (native tooltip as backup)
218
- const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
219
- title.textContent = `Value: ${row.value}`;
220
- circle.appendChild(title);
221
-
222
- svgLayer.appendChild(circle);
223
-
224
- // Draw text for value
225
- const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
226
- text.setAttribute('x', x); // Centered horizontally
227
- text.setAttribute('y', y + 4); // Centered vertically (approx)
228
- text.textContent = row.value;
229
- text.setAttribute('fill', 'white'); // White text
230
- text.setAttribute('font-size', '10px');
231
- text.setAttribute('font-family', 'sans-serif');
232
- text.setAttribute('font-weight', 'bold');
233
- text.setAttribute('text-anchor', 'middle'); // Center text
234
- text.style.pointerEvents = 'none'; // Let clicks pass through to circle
235
- svgLayer.appendChild(text);
236
- });
237
-
238
- polyline.setAttribute('points', points.join(' '));
239
- svgLayer.prepend(polyline); // Put line behind points
240
-
241
- this.container.appendChild(svgLayer);
242
- }
243
-
244
- showTooltip(e, value) {
245
- const span = this.config.valueSpans.find(s => value >= s.min && value <= s.max);
246
- if (span) {
247
- this.tooltip.innerText = `${span.text} (${value})`;
248
- this.tooltip.style.display = 'block';
249
- this.tooltip.style.left = `${e.pageX + 10}px`;
250
- this.tooltip.style.top = `${e.pageY + 10}px`;
251
- }
252
- }
253
-
254
- hideTooltip() {
255
- this.tooltip.style.display = 'none';
256
- }
257
- }
1
+ // ─── Types ──────────────────────────────────────────────────────────────────
2
+ const MARGIN_XY = { top: 16, right: 24, bottom: 44, left: 52 };
3
+ const MARGIN_BAR = { top: 8, right: 52, bottom: 24, left: 120 };
4
+ const MARGIN_PIE = { top: 8, right: 8, bottom: 8, left: 8 };
5
+ const FALLBACK_COLORS = [
6
+ '#3D63DD', '#2E8B57', '#C28A00', '#D64545',
7
+ '#8B5CF6', '#06B6D4', '#F97316', '#EC4899',
8
+ ];
9
+ const SVG_NS = 'http://www.w3.org/2000/svg';
10
+ // ─── Chart ──────────────────────────────────────────────────────────────────
11
+ class Chart {
12
+ constructor(selector, options) {
13
+ this.colors = [];
14
+ this.abortController = new AbortController();
15
+ this.resizeTimer = null;
16
+ this.resizeObserver = null;
17
+ const el = typeof selector === 'string'
18
+ ? document.querySelector(selector)
19
+ : selector;
20
+ if (!el)
21
+ throw new Error(`Chart: element not found for "${selector}"`);
22
+ this.container = el;
23
+ this.opts = {
24
+ type: options.type,
25
+ series: options.series,
26
+ title: options.title ?? '',
27
+ subtitle: options.subtitle ?? '',
28
+ height: options.height ?? 280,
29
+ showLegend: options.showLegend ?? true,
30
+ showGrid: options.showGrid ?? true,
31
+ animate: options.animate ?? true,
32
+ curve: options.curve ?? 'smooth',
33
+ yMin: options.yMin ?? 0,
34
+ yMax: options.yMax ?? 0,
35
+ onPointClick: options.onPointClick ?? (() => { }),
36
+ };
37
+ this.render();
38
+ this.attachResizeObserver();
39
+ }
40
+ // ── Render ──────────────────────────────────────────────────────────────
41
+ render() {
42
+ this.abortController.abort();
43
+ this.abortController = new AbortController();
44
+ this.container.innerHTML = '';
45
+ this.container.classList.add('chart');
46
+ this.resolveColors();
47
+ if (this.opts.title || this.opts.subtitle) {
48
+ this.container.appendChild(this.buildHeader());
49
+ }
50
+ const canvas = this.div('chart-canvas');
51
+ this.container.appendChild(canvas);
52
+ this.tooltip = this.div('chart-tooltip');
53
+ this.container.appendChild(this.tooltip);
54
+ switch (this.opts.type) {
55
+ case 'line':
56
+ this.renderLineOrArea(canvas, false);
57
+ break;
58
+ case 'area':
59
+ this.renderLineOrArea(canvas, true);
60
+ break;
61
+ case 'column':
62
+ this.renderColumn(canvas);
63
+ break;
64
+ case 'bar':
65
+ this.renderBar(canvas);
66
+ break;
67
+ case 'pie':
68
+ this.renderPie(canvas);
69
+ break;
70
+ }
71
+ if (this.opts.showLegend && this.opts.type !== 'pie') {
72
+ this.container.appendChild(this.buildLegend());
73
+ }
74
+ }
75
+ // ── Line / Area ──────────────────────────────────────────────────────────
76
+ renderLineOrArea(canvas, isArea) {
77
+ const { series, height, showGrid, animate, yMin } = this.opts;
78
+ if (!series.length || !series[0].data.length)
79
+ return;
80
+ const m = MARGIN_XY;
81
+ const svgW = canvas.clientWidth || 600;
82
+ const svgH = height + m.top + m.bottom;
83
+ const w = svgW - m.left - m.right;
84
+ const h = height;
85
+ const allValues = series.flatMap(s => s.data.map(d => d.value));
86
+ const yMax = this.opts.yMax || Math.max(...allValues) * 1.1;
87
+ const labels = series[0].data.map(d => d.label);
88
+ const svg = this.createSVG(canvas, svgW, svgH);
89
+ if (showGrid)
90
+ this.renderHGrid(svg, m, w, h, yMin, yMax);
91
+ this.renderXAxisLine(svg, m, w, h);
92
+ this.renderXLabels(svg, m, w, h, labels);
93
+ this.renderYLabels(svg, m, h, yMin, yMax);
94
+ series.forEach((s, si) => {
95
+ const color = this.colors[si];
96
+ const numPts = s.data.length;
97
+ const pts = s.data.map((d, i) => ({
98
+ x: m.left + (numPts > 1 ? (i / (numPts - 1)) * w : w / 2),
99
+ y: m.top + h - ((d.value - yMin) / (yMax - yMin)) * h,
100
+ }));
101
+ if (isArea) {
102
+ const areaD = `${this.buildPath(pts)} L ${pts[pts.length - 1].x} ${m.top + h} L ${pts[0].x} ${m.top + h} Z`;
103
+ svg.appendChild(this.svgEl('path', {
104
+ d: areaD, fill: color,
105
+ 'fill-opacity': '0.12', stroke: 'none',
106
+ class: 'chart-area',
107
+ }));
108
+ }
109
+ const linePath = this.svgEl('path', {
110
+ d: this.buildPath(pts), fill: 'none',
111
+ stroke: color, 'stroke-width': '2.5',
112
+ 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
113
+ class: 'chart-line',
114
+ });
115
+ if (animate) {
116
+ requestAnimationFrame(() => {
117
+ const len = linePath.getTotalLength();
118
+ linePath.style.setProperty('--path-length', String(Math.ceil(len)));
119
+ });
120
+ }
121
+ svg.appendChild(linePath);
122
+ // Data point markers
123
+ s.data.forEach((d, i) => {
124
+ const g = this.svgEl('g', {
125
+ class: 'chart-point-group',
126
+ style: animate ? `animation-delay: ${i * 40}ms` : '',
127
+ });
128
+ const { x, y } = pts[i];
129
+ g.appendChild(this.svgEl('circle', {
130
+ cx: x, cy: y, r: 14,
131
+ fill: 'transparent', class: 'chart-hit',
132
+ }));
133
+ g.appendChild(this.svgEl('circle', {
134
+ cx: x, cy: y, r: 7,
135
+ fill: 'none', stroke: color, 'stroke-width': '2',
136
+ class: 'chart-point-ring',
137
+ }));
138
+ g.appendChild(this.svgEl('circle', {
139
+ cx: x, cy: y, r: 4,
140
+ fill: color, stroke: 'var(--background)', 'stroke-width': '2',
141
+ class: 'chart-point-dot',
142
+ }));
143
+ this.onPoint(g, s, d, i);
144
+ svg.appendChild(g);
145
+ });
146
+ });
147
+ }
148
+ // ── Column ───────────────────────────────────────────────────────────────
149
+ renderColumn(canvas) {
150
+ const { series, height, showGrid, animate, yMin } = this.opts;
151
+ if (!series.length || !series[0].data.length)
152
+ return;
153
+ const m = MARGIN_XY;
154
+ const svgW = canvas.clientWidth || 600;
155
+ const svgH = height + m.top + m.bottom;
156
+ const w = svgW - m.left - m.right;
157
+ const h = height;
158
+ const allValues = series.flatMap(s => s.data.map(d => d.value));
159
+ const yMax = this.opts.yMax || Math.max(...allValues) * 1.1;
160
+ const labels = series[0].data.map(d => d.label);
161
+ const numPts = labels.length;
162
+ const numSeries = series.length;
163
+ const svg = this.createSVG(canvas, svgW, svgH);
164
+ if (showGrid)
165
+ this.renderHGrid(svg, m, w, h, yMin, yMax);
166
+ this.renderXAxisLine(svg, m, w, h);
167
+ this.renderXLabels(svg, m, w, h, labels);
168
+ this.renderYLabels(svg, m, h, yMin, yMax);
169
+ const groupW = w / numPts;
170
+ const innerPad = groupW * 0.18;
171
+ const barW = Math.max(2, (groupW - innerPad) / numSeries - 2);
172
+ series.forEach((s, si) => {
173
+ const color = this.colors[si];
174
+ s.data.forEach((d, i) => {
175
+ const barH = Math.max(0, ((d.value - yMin) / (yMax - yMin)) * h);
176
+ const x = m.left + i * groupW + innerPad / 2 + si * (barW + 2);
177
+ const y = m.top + h - barH;
178
+ const rect = this.svgEl('rect', {
179
+ x, y, width: barW, height: barH,
180
+ fill: color, rx: 3,
181
+ class: 'chart-bar chart-bar--vertical',
182
+ });
183
+ if (animate) {
184
+ const delay = (i * numSeries + si) * 50;
185
+ rect.style.setProperty('--animation-delay', `${delay}ms`);
186
+ rect.style.animationDelay = `${delay}ms`;
187
+ }
188
+ this.onBar(rect, s, d, i);
189
+ svg.appendChild(rect);
190
+ });
191
+ });
192
+ }
193
+ // ── Bar (horizontal) ─────────────────────────────────────────────────────
194
+ renderBar(canvas) {
195
+ const { series, height, animate } = this.opts;
196
+ if (!series.length || !series[0].data.length)
197
+ return;
198
+ const m = MARGIN_BAR;
199
+ const svgW = canvas.clientWidth || 600;
200
+ const svgH = height + m.top + m.bottom;
201
+ const w = svgW - m.left - m.right;
202
+ const h = height;
203
+ const allValues = series.flatMap(s => s.data.map(d => d.value));
204
+ const xMax = this.opts.yMax || Math.max(...allValues) * 1.1;
205
+ const labels = series[0].data.map(d => d.label);
206
+ const numPts = labels.length;
207
+ const numSeries = series.length;
208
+ const svg = this.createSVG(canvas, svgW, svgH);
209
+ // Vertical grid lines
210
+ const numTicks = 5;
211
+ for (let t = 0; t <= numTicks; t++) {
212
+ const x = m.left + (t / numTicks) * w;
213
+ svg.appendChild(this.svgEl('line', {
214
+ x1: x, x2: x, y1: m.top, y2: m.top + h,
215
+ stroke: 'var(--divider)', 'stroke-width': '1',
216
+ 'stroke-dasharray': t === 0 ? 'none' : '3 4',
217
+ class: 'chart-grid-line',
218
+ }));
219
+ const label = this.svgEl('text', {
220
+ x, y: m.top + h + 14,
221
+ 'text-anchor': 'middle', class: 'chart-axis-label',
222
+ });
223
+ label.textContent = this.fmt(xMax * t / numTicks);
224
+ svg.appendChild(label);
225
+ }
226
+ // Category labels on Y axis
227
+ const groupH = h / numPts;
228
+ labels.forEach((label, i) => {
229
+ const y = m.top + i * groupH + groupH / 2;
230
+ const text = this.svgEl('text', {
231
+ x: m.left - 10, y,
232
+ 'text-anchor': 'end', 'dominant-baseline': 'middle',
233
+ class: 'chart-axis-label',
234
+ });
235
+ text.textContent = label;
236
+ svg.appendChild(text);
237
+ });
238
+ // Bars
239
+ const innerPad = groupH * 0.18;
240
+ const barH = Math.max(2, (groupH - innerPad) / numSeries - 2);
241
+ series.forEach((s, si) => {
242
+ const color = this.colors[si];
243
+ s.data.forEach((d, i) => {
244
+ const barW = Math.max(0, (d.value / xMax) * w);
245
+ const x = m.left;
246
+ const y = m.top + i * groupH + innerPad / 2 + si * (barH + 2);
247
+ const rect = this.svgEl('rect', {
248
+ x, y, width: barW, height: barH,
249
+ fill: color, rx: 3,
250
+ class: 'chart-bar chart-bar--horizontal',
251
+ });
252
+ if (animate) {
253
+ const delay = (i * numSeries + si) * 50;
254
+ rect.style.setProperty('--animation-delay', `${delay}ms`);
255
+ rect.style.animationDelay = `${delay}ms`;
256
+ }
257
+ this.onBar(rect, s, d, i);
258
+ svg.appendChild(rect);
259
+ });
260
+ });
261
+ }
262
+ // ── Pie ──────────────────────────────────────────────────────────────────
263
+ renderPie(canvas) {
264
+ const { series, height, animate, showLegend } = this.opts;
265
+ const s = series[0];
266
+ if (!s || !s.data.length)
267
+ return;
268
+ const svgW = canvas.clientWidth || 400;
269
+ const m = MARGIN_PIE;
270
+ const svgH = height + m.top + m.bottom;
271
+ const cx = svgW / 2;
272
+ const cy = svgH / 2;
273
+ const r = Math.min(svgW, svgH) / 2 - Math.max(m.top, m.left) - 8;
274
+ const total = s.data.reduce((sum, d) => sum + d.value, 0);
275
+ const svg = this.createSVG(canvas, svgW, svgH);
276
+ let startAngle = -90; // start at 12 o'clock
277
+ s.data.forEach((d, i) => {
278
+ const color = this.colors[i % this.colors.length];
279
+ const sweep = (d.value / total) * 360;
280
+ const endAngle = startAngle + sweep;
281
+ const midAngle = startAngle + sweep / 2;
282
+ const path = this.svgEl('path', {
283
+ d: this.arcPath(cx, cy, r, startAngle, endAngle),
284
+ fill: color,
285
+ stroke: 'var(--background)',
286
+ 'stroke-width': '2',
287
+ class: 'chart-slice',
288
+ });
289
+ if (animate) {
290
+ const delay = i * 70;
291
+ path.style.animationDelay = `${delay}ms`;
292
+ }
293
+ // Hover: nudge slice outward
294
+ const { x: dx, y: dy } = this.polar(0, 0, 8, midAngle);
295
+ path.addEventListener('mouseenter', (e) => {
296
+ path.style.transform = `translate(${dx}px, ${dy}px)`;
297
+ this.showTooltip(e, `<strong>${d.label}</strong>${this.fmt(d.value)} &nbsp;·&nbsp; ${((d.value / total) * 100).toFixed(1)}%`);
298
+ }, { signal: this.abortController.signal });
299
+ path.addEventListener('mouseleave', () => {
300
+ path.style.transform = '';
301
+ this.hideTooltip();
302
+ }, { signal: this.abortController.signal });
303
+ path.addEventListener('click', () => {
304
+ this.opts.onPointClick(s, d, i);
305
+ }, { signal: this.abortController.signal });
306
+ svg.appendChild(path);
307
+ startAngle = endAngle;
308
+ });
309
+ if (showLegend) {
310
+ this.container.appendChild(this.buildPieLegend(s, total));
311
+ }
312
+ }
313
+ // ── Axis helpers ─────────────────────────────────────────────────────────
314
+ renderHGrid(svg, m, w, h, yMin, yMax) {
315
+ const numTicks = 5;
316
+ for (let i = 0; i <= numTicks; i++) {
317
+ const y = m.top + h - (i / numTicks) * h;
318
+ svg.appendChild(this.svgEl('line', {
319
+ x1: m.left, x2: m.left + w, y1: y, y2: y,
320
+ class: i === 0 ? 'chart-axis-line' : 'chart-grid-line',
321
+ }));
322
+ }
323
+ }
324
+ renderXAxisLine(svg, m, w, h) {
325
+ svg.appendChild(this.svgEl('line', {
326
+ x1: m.left, x2: m.left + w,
327
+ y1: m.top + h, y2: m.top + h,
328
+ class: 'chart-axis-line',
329
+ }));
330
+ }
331
+ renderXLabels(svg, m, w, h, labels) {
332
+ const n = labels.length;
333
+ const step = n > 1 ? w / (n - 1) : w / 2;
334
+ labels.forEach((label, i) => {
335
+ const x = m.left + (n > 1 ? i * step : w / 2);
336
+ const text = this.svgEl('text', {
337
+ x, y: m.top + h + 18,
338
+ 'text-anchor': 'middle', class: 'chart-axis-label',
339
+ });
340
+ text.textContent = label;
341
+ svg.appendChild(text);
342
+ });
343
+ }
344
+ renderYLabels(svg, m, h, yMin, yMax) {
345
+ const numTicks = 5;
346
+ for (let i = 0; i <= numTicks; i++) {
347
+ const val = yMin + (yMax - yMin) * (i / numTicks);
348
+ const y = m.top + h - (i / numTicks) * h;
349
+ const text = this.svgEl('text', {
350
+ x: m.left - 8, y,
351
+ 'text-anchor': 'end', 'dominant-baseline': 'middle',
352
+ class: 'chart-axis-label',
353
+ });
354
+ text.textContent = this.fmt(val);
355
+ svg.appendChild(text);
356
+ }
357
+ }
358
+ // ── Geometry helpers ─────────────────────────────────────────────────────
359
+ buildPath(pts) {
360
+ switch (this.opts.curve) {
361
+ case 'linear': return this.linearPath(pts);
362
+ case 'step': return this.stepPath(pts);
363
+ default: return this.smoothPath(pts);
364
+ }
365
+ }
366
+ linearPath(pts) {
367
+ if (pts.length === 0)
368
+ return '';
369
+ return pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
370
+ }
371
+ stepPath(pts) {
372
+ if (pts.length === 0)
373
+ return '';
374
+ let d = `M ${pts[0].x} ${pts[0].y}`;
375
+ for (let i = 1; i < pts.length; i++) {
376
+ d += ` H ${pts[i].x} V ${pts[i].y}`;
377
+ }
378
+ return d;
379
+ }
380
+ /** Smooth cubic bezier path through points (Catmull-Rom → cubic bezier) */
381
+ smoothPath(pts) {
382
+ if (pts.length === 0)
383
+ return '';
384
+ if (pts.length === 1)
385
+ return `M ${pts[0].x} ${pts[0].y}`;
386
+ if (pts.length === 2)
387
+ return `M ${pts[0].x} ${pts[0].y} L ${pts[1].x} ${pts[1].y}`;
388
+ const t = 0.35;
389
+ let d = `M ${pts[0].x} ${pts[0].y}`;
390
+ for (let i = 0; i < pts.length - 1; i++) {
391
+ const p0 = pts[Math.max(0, i - 1)];
392
+ const p1 = pts[i];
393
+ const p2 = pts[i + 1];
394
+ const p3 = pts[Math.min(pts.length - 1, i + 2)];
395
+ const cp1x = p1.x + (p2.x - p0.x) * t;
396
+ const cp1y = p1.y + (p2.y - p0.y) * t;
397
+ const cp2x = p2.x - (p3.x - p1.x) * t;
398
+ const cp2y = p2.y - (p3.y - p1.y) * t;
399
+ d += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${p2.x} ${p2.y}`;
400
+ }
401
+ return d;
402
+ }
403
+ arcPath(cx, cy, r, startDeg, endDeg) {
404
+ const start = this.polar(cx, cy, r, startDeg);
405
+ const end = this.polar(cx, cy, r, endDeg);
406
+ const large = (endDeg - startDeg) > 180 ? 1 : 0;
407
+ return `M ${cx} ${cy} L ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${r} ${r} 0 ${large} 1 ${end.x.toFixed(2)} ${end.y.toFixed(2)} Z`;
408
+ }
409
+ polar(cx, cy, r, deg) {
410
+ const rad = deg * Math.PI / 180;
411
+ return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
412
+ }
413
+ // ── Legend builders ──────────────────────────────────────────────────────
414
+ buildHeader() {
415
+ const el = this.div('chart-header');
416
+ if (this.opts.title) {
417
+ const t = this.div('chart-title');
418
+ t.textContent = this.opts.title;
419
+ el.appendChild(t);
420
+ }
421
+ if (this.opts.subtitle) {
422
+ const s = this.div('chart-subtitle');
423
+ s.textContent = this.opts.subtitle;
424
+ el.appendChild(s);
425
+ }
426
+ return el;
427
+ }
428
+ buildLegend() {
429
+ const el = this.div('chart-legend');
430
+ this.opts.series.forEach((s, i) => {
431
+ const item = this.div('chart-legend-item');
432
+ const swatch = this.div('chart-legend-swatch');
433
+ swatch.style.background = this.colors[i];
434
+ const label = document.createElement('span');
435
+ label.textContent = s.name;
436
+ item.append(swatch, label);
437
+ el.appendChild(item);
438
+ });
439
+ return el;
440
+ }
441
+ buildPieLegend(s, total) {
442
+ const el = this.div('chart-pie-legend');
443
+ s.data.forEach((d, i) => {
444
+ const color = this.colors[i % this.colors.length];
445
+ const item = this.div('chart-pie-legend-item');
446
+ const swatch = this.div('chart-pie-legend-swatch');
447
+ swatch.style.background = color;
448
+ const label = document.createElement('span');
449
+ label.textContent = d.label;
450
+ const value = this.div('chart-pie-legend-value');
451
+ value.textContent = `${((d.value / total) * 100).toFixed(1)}%`;
452
+ item.append(swatch, label, value);
453
+ el.appendChild(item);
454
+ });
455
+ return el;
456
+ }
457
+ // ── Tooltip ──────────────────────────────────────────────────────────────
458
+ showTooltip(e, html) {
459
+ this.tooltip.innerHTML = html;
460
+ this.tooltip.classList.add('is-visible');
461
+ this.moveTooltip(e);
462
+ }
463
+ moveTooltip(e) {
464
+ const tt = this.tooltip;
465
+ const vw = window.innerWidth;
466
+ const vh = window.innerHeight;
467
+ let x = e.clientX + 14;
468
+ let y = e.clientY - 36;
469
+ // Keep inside viewport
470
+ if (x + 200 > vw)
471
+ x = e.clientX - 14 - tt.offsetWidth;
472
+ if (y < 0)
473
+ y = e.clientY + 14;
474
+ if (y + tt.offsetHeight > vh)
475
+ y = vh - tt.offsetHeight - 8;
476
+ tt.style.left = `${x}px`;
477
+ tt.style.top = `${y}px`;
478
+ }
479
+ hideTooltip() {
480
+ this.tooltip.classList.remove('is-visible');
481
+ }
482
+ // ── Event wiring ─────────────────────────────────────────────────────────
483
+ onPoint(g, s, d, i) {
484
+ const sig = { signal: this.abortController.signal };
485
+ g.addEventListener('mouseenter', (e) => {
486
+ this.showTooltip(e, `<strong>${d.label}</strong>${s.name}: ${this.fmt(d.value)}`);
487
+ }, sig);
488
+ g.addEventListener('mousemove', (e) => this.moveTooltip(e), sig);
489
+ g.addEventListener('mouseleave', () => this.hideTooltip(), sig);
490
+ g.addEventListener('click', () => this.opts.onPointClick(s, d, i), sig);
491
+ }
492
+ onBar(rect, s, d, i) {
493
+ const sig = { signal: this.abortController.signal };
494
+ rect.style.cursor = 'pointer';
495
+ rect.addEventListener('mouseenter', (e) => {
496
+ this.showTooltip(e, `<strong>${d.label}</strong>${s.name}: ${this.fmt(d.value)}`);
497
+ }, sig);
498
+ rect.addEventListener('mousemove', (e) => this.moveTooltip(e), sig);
499
+ rect.addEventListener('mouseleave', () => this.hideTooltip(), sig);
500
+ rect.addEventListener('click', () => this.opts.onPointClick(s, d, i), sig);
501
+ }
502
+ // ── Color resolution ─────────────────────────────────────────────────────
503
+ resolveColors() {
504
+ const style = getComputedStyle(this.container);
505
+ this.colors = (this.opts.type === 'pie' ? this.opts.series[0]?.data ?? [] : this.opts.series)
506
+ .map((_, i) => {
507
+ const css = style.getPropertyValue(`--chart-color-${i + 1}`).trim();
508
+ return css || FALLBACK_COLORS[i % FALLBACK_COLORS.length];
509
+ });
510
+ // Allow per-series color override (not pie)
511
+ if (this.opts.type !== 'pie') {
512
+ this.opts.series.forEach((s, i) => {
513
+ if (s.color)
514
+ this.colors[i] = s.color;
515
+ });
516
+ }
517
+ }
518
+ // ── DOM & SVG helpers ────────────────────────────────────────────────────
519
+ div(className) {
520
+ const el = document.createElement('div');
521
+ el.className = className;
522
+ return el;
523
+ }
524
+ createSVG(parent, w, h) {
525
+ const svg = document.createElementNS(SVG_NS, 'svg');
526
+ svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
527
+ svg.setAttribute('height', String(h));
528
+ svg.setAttribute('preserveAspectRatio', 'none');
529
+ svg.classList.add('chart-svg');
530
+ parent.appendChild(svg);
531
+ return svg;
532
+ }
533
+ svgEl(tag, attrs = {}) {
534
+ const el = document.createElementNS(SVG_NS, tag);
535
+ for (const [k, v] of Object.entries(attrs))
536
+ el.setAttribute(k, String(v));
537
+ return el;
538
+ }
539
+ fmt(v) {
540
+ if (v >= 1000000)
541
+ return `${(v / 1000000).toFixed(1)}M`;
542
+ if (v >= 1000)
543
+ return `${(v / 1000).toFixed(1)}K`;
544
+ return v % 1 === 0 ? String(Math.round(v)) : v.toFixed(1);
545
+ }
546
+ // ── Resize ───────────────────────────────────────────────────────────────
547
+ attachResizeObserver() {
548
+ this.resizeObserver = new ResizeObserver(() => {
549
+ if (this.resizeTimer)
550
+ clearTimeout(this.resizeTimer);
551
+ this.resizeTimer = setTimeout(() => this.render(), 100);
552
+ });
553
+ this.resizeObserver.observe(this.container);
554
+ }
555
+ // ── Public API ───────────────────────────────────────────────────────────
556
+ update(series) {
557
+ this.opts.series = series;
558
+ this.render();
559
+ }
560
+ setType(type) {
561
+ this.opts.type = type;
562
+ this.render();
563
+ }
564
+ destroy() {
565
+ this.abortController.abort();
566
+ this.resizeObserver?.disconnect();
567
+ if (this.resizeTimer)
568
+ clearTimeout(this.resizeTimer);
569
+ this.container.innerHTML = '';
570
+ this.container.classList.remove('chart');
571
+ }
572
+ }
573
+ export { Chart };