@exegia/corpora-ui 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (587) hide show
  1. package/README.md +20 -0
  2. package/dist-lib/button-CIRkQXA6.js +152 -0
  3. package/dist-lib/button.js +3 -0
  4. package/dist-lib/card.js +150 -0
  5. package/dist-lib/components/atoms/text/default.d.ts +2 -1143
  6. package/dist-lib/components/blocks/shell/index.d.ts +1 -0
  7. package/dist-lib/components/ui/badge.d.ts +2 -2
  8. package/dist-lib/components/ui/button.d.ts +2 -2
  9. package/dist-lib/components/ui/glasscn/lazy-glass-surface.d.ts +6 -0
  10. package/dist-lib/components/ui/modal.d.ts +10 -0
  11. package/dist-lib/components/ui/overlays.d.ts +4 -0
  12. package/dist-lib/components/ui/select.d.ts +1 -1
  13. package/dist-lib/components/ui/toast.d.ts +2 -0
  14. package/dist-lib/ease-C9At8rnB.js +120 -0
  15. package/dist-lib/exegia-provider-DNTuLljX.js +124 -0
  16. package/dist-lib/index.css +3 -0
  17. package/dist-lib/index.d.ts +3 -0
  18. package/dist-lib/index.js +6482 -8894
  19. package/dist-lib/input.js +28 -0
  20. package/dist-lib/label.js +19 -0
  21. package/dist-lib/lib/segmented-control.d.ts +1 -1
  22. package/dist-lib/lib/state/exegia-provider.d.ts +17 -8
  23. package/dist-lib/lib/state/index.d.ts +2 -0
  24. package/dist-lib/lib/state/portal-context.d.ts +5 -0
  25. package/dist-lib/lib/state/sound-loading.d.ts +4 -0
  26. package/dist-lib/liquid-glass-BihU6nMl.js +50 -0
  27. package/dist-lib/modal-DLaOK-s1.js +82 -0
  28. package/dist-lib/overlays.js +3 -0
  29. package/dist-lib/rolldown-runtime-Dy4uBu1J.js +11 -0
  30. package/dist-lib/scaffold-Qv4hs5Pf.js +925 -0
  31. package/dist-lib/scaffold.js +2 -0
  32. package/dist-lib/shell-BQ9cjIEu.js +722 -0
  33. package/dist-lib/shell.js +2 -0
  34. package/dist-lib/sound-knGH4lj-.js +19 -0
  35. package/dist-lib/state.js +2 -0
  36. package/dist-lib/toast-CataVXHN.js +193 -0
  37. package/dist-lib/utils-BO0qfoA6.js +61 -0
  38. package/package.json +64 -23
  39. package/dist-lib/components/atoms/_layout.d.ts +0 -4
  40. package/dist-lib/components/atoms/bubble/__tests__/bubble.test.d.ts +0 -1
  41. package/dist-lib/components/atoms/bubble/__tests__/reference.test.d.ts +0 -1
  42. package/dist-lib/components/atoms/text/__tests__/text.test.d.ts +0 -1
  43. package/dist-lib/components/atoms/text-selection/__tests__/click-popover.test.d.ts +0 -1
  44. package/dist-lib/components/atoms/text-selection/__tests__/text-selection.test.d.ts +0 -1
  45. package/dist-lib/components/blocks/auth/__tests__/auth-flow-block.test.d.ts +0 -1
  46. package/dist-lib/components/blocks/auth/__tests__/auth-state-atom.test.d.ts +0 -1
  47. package/dist-lib/components/blocks/auth/__tests__/linked-accounts-block.test.d.ts +0 -1
  48. package/dist-lib/components/blocks/auth/__tests__/onboarding-block.test.d.ts +0 -1
  49. package/dist-lib/components/blocks/auth/__tests__/passkey-manager-block.test.d.ts +0 -1
  50. package/dist-lib/components/blocks/auth/__tests__/passkey-sign-in-block.test.d.ts +0 -1
  51. package/dist-lib/components/blocks/auth/__tests__/signup-block.test.d.ts +0 -1
  52. package/dist-lib/components/blocks/auth/__tests__/update-password-block.test.d.ts +0 -1
  53. package/dist-lib/components/blocks/chat/__tests__/ai-panel.test.d.ts +0 -1
  54. package/dist-lib/components/blocks/profile/__tests__/profile-card-block.test.d.ts +0 -1
  55. package/dist-lib/components/blocks/scaffold/__tests__/scaffold-atom.test.d.ts +0 -1
  56. package/dist-lib/components/blocks/scaffold/__tests__/scaffold.test.d.ts +0 -1
  57. package/dist-lib/components/blocks/shell/__tests__/shell-fit-atom.test.d.ts +0 -1
  58. package/dist-lib/components/blocks/shell/__tests__/shell-layout.test.d.ts +0 -1
  59. package/dist-lib/components/blocks/shell/__tests__/shell-metrics.test.d.ts +0 -1
  60. package/dist-lib/components/blocks/sidebar/__tests__/ai-sidebar-atom.test.d.ts +0 -1
  61. package/dist-lib/components/blocks/sidebar/__tests__/ai-sidebar.test.d.ts +0 -1
  62. package/dist-lib/components/blocks/sidebar/__tests__/use-ai-sidebar.test.d.ts +0 -1
  63. package/dist-lib/components/composed/__tests__/logo.test.d.ts +0 -1
  64. package/dist-lib/components/composed/action-bar/__tests__/action-bar.test.d.ts +0 -1
  65. package/dist-lib/components/composed/ai/__tests__/ai.test.d.ts +0 -1
  66. package/dist-lib/components/composed/chat/__tests__/attachment.test.d.ts +0 -1
  67. package/dist-lib/components/composed/chat/__tests__/beautiful-ui.test.d.ts +0 -1
  68. package/dist-lib/components/composed/chat/__tests__/chart.test.d.ts +0 -1
  69. package/dist-lib/components/composed/chat/__tests__/code-block.test.d.ts +0 -1
  70. package/dist-lib/components/composed/chat/__tests__/composer-attachments.test.d.ts +0 -1
  71. package/dist-lib/components/composed/chat/__tests__/composer.test.d.ts +0 -1
  72. package/dist-lib/components/composed/chat/__tests__/flowchart.test.d.ts +0 -1
  73. package/dist-lib/components/composed/chat/__tests__/glass-material.test.d.ts +0 -1
  74. package/dist-lib/components/composed/chat/__tests__/markdown.test.d.ts +0 -1
  75. package/dist-lib/components/composed/chat/__tests__/menu-command.test.d.ts +0 -1
  76. package/dist-lib/components/composed/chat/__tests__/recommendation.test.d.ts +0 -1
  77. package/dist-lib/components/composed/tree/__tests__/tree-atom.test.d.ts +0 -1
  78. package/dist-lib/components/composed/tree/__tests__/tree.test.d.ts +0 -1
  79. package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +0 -1
  80. package/dist-lib/components/composed/user/__tests__/message.test.d.ts +0 -1
  81. package/dist-lib/components/composed/verse/__tests__/verse.test.d.ts +0 -1
  82. package/dist-lib/components/docs/atoms-gallery.d.ts +0 -12
  83. package/dist-lib/components/docs/block-demo-stage.d.ts +0 -7
  84. package/dist-lib/components/docs/blocks-gallery.d.ts +0 -2
  85. package/dist-lib/components/docs/browser.d.ts +0 -10
  86. package/dist-lib/components/docs/code-block.d.ts +0 -4
  87. package/dist-lib/components/docs/components-gallery.d.ts +0 -2
  88. package/dist-lib/components/docs/demo-controls.d.ts +0 -21
  89. package/dist-lib/components/docs/hero.d.ts +0 -5
  90. package/dist-lib/components/docs/mermaid.d.ts +0 -3
  91. package/dist-lib/components/icons/__tests__/icons.test.d.ts +0 -1
  92. package/dist-lib/components/stories/attachment.story.d.ts +0 -11
  93. package/dist-lib/components/stories/auth-flow-block.story.d.ts +0 -2
  94. package/dist-lib/components/stories/avatar.story.d.ts +0 -6
  95. package/dist-lib/components/stories/base.story.d.ts +0 -6
  96. package/dist-lib/components/stories/bubble.story.d.ts +0 -6
  97. package/dist-lib/components/stories/button.story.d.ts +0 -9
  98. package/dist-lib/components/stories/card.story.d.ts +0 -8
  99. package/dist-lib/components/stories/chart.story.d.ts +0 -71
  100. package/dist-lib/components/stories/checkbox.story.d.ts +0 -6
  101. package/dist-lib/components/stories/code-auth-block.story.d.ts +0 -2
  102. package/dist-lib/components/stories/code-block.story.d.ts +0 -12
  103. package/dist-lib/components/stories/composed-password-input.story.d.ts +0 -6
  104. package/dist-lib/components/stories/context-cards.story.d.ts +0 -3
  105. package/dist-lib/components/stories/emoji-action-bar.story.d.ts +0 -3
  106. package/dist-lib/components/stories/field.story.d.ts +0 -6
  107. package/dist-lib/components/stories/file-icons.story.d.ts +0 -5
  108. package/dist-lib/components/stories/filter-table.story.d.ts +0 -21
  109. package/dist-lib/components/stories/flowchart.story.d.ts +0 -3
  110. package/dist-lib/components/stories/forgot-password-block.story.d.ts +0 -2
  111. package/dist-lib/components/stories/frame.story.d.ts +0 -8
  112. package/dist-lib/components/stories/index.d.ts +0 -54
  113. package/dist-lib/components/stories/input-group.story.d.ts +0 -6
  114. package/dist-lib/components/stories/input.story.d.ts +0 -8
  115. package/dist-lib/components/stories/insight-cards.story.d.ts +0 -74
  116. package/dist-lib/components/stories/label.story.d.ts +0 -8
  117. package/dist-lib/components/stories/linked-accounts-block.story.d.ts +0 -2
  118. package/dist-lib/components/stories/login-block.story.d.ts +0 -2
  119. package/dist-lib/components/stories/logo.story.d.ts +0 -3
  120. package/dist-lib/components/stories/markdown.story.d.ts +0 -7
  121. package/dist-lib/components/stories/menu-command.story.d.ts +0 -13
  122. package/dist-lib/components/stories/onboarding-block.story.d.ts +0 -2
  123. package/dist-lib/components/stories/otp-field.story.d.ts +0 -8
  124. package/dist-lib/components/stories/passkey-manager-block.story.d.ts +0 -2
  125. package/dist-lib/components/stories/passkey-sign-in-block.story.d.ts +0 -2
  126. package/dist-lib/components/stories/password-input.story.d.ts +0 -9
  127. package/dist-lib/components/stories/profile-card-block.story.d.ts +0 -6
  128. package/dist-lib/components/stories/recommendation-card.story.d.ts +0 -3
  129. package/dist-lib/components/stories/records-table.story.d.ts +0 -23
  130. package/dist-lib/components/stories/reference.story.d.ts +0 -9
  131. package/dist-lib/components/stories/research-answer.story.d.ts +0 -10
  132. package/dist-lib/components/stories/scaffold-root.story.d.ts +0 -6
  133. package/dist-lib/components/stories/separator.story.d.ts +0 -6
  134. package/dist-lib/components/stories/shell-layout.story.d.ts +0 -2
  135. package/dist-lib/components/stories/signup-block.story.d.ts +0 -2
  136. package/dist-lib/components/stories/social-providers.story.d.ts +0 -3
  137. package/dist-lib/components/stories/streaming-text.story.d.ts +0 -3
  138. package/dist-lib/components/stories/text.story.d.ts +0 -8
  139. package/dist-lib/components/stories/textarea.story.d.ts +0 -6
  140. package/dist-lib/components/stories/tree.story.d.ts +0 -4
  141. package/dist-lib/components/stories/update-password-block.story.d.ts +0 -2
  142. package/dist-lib/components/stories/user.story.d.ts +0 -8
  143. package/dist-lib/components/stories/verse.story.d.ts +0 -6
  144. package/dist-lib/components/ui/chat/__tests__/segmented-toggle.test.d.ts +0 -1
  145. package/dist-lib/index.js.map +0 -1
  146. package/dist-lib/lib/state/__tests__/sound.test.d.ts +0 -1
  147. package/dist-lib/lib/utils.test.d.ts +0 -1
  148. package/src/assets/css/base.css +0 -62
  149. package/src/assets/css/chat.css +0 -229
  150. package/src/assets/css/shadows.css +0 -157
  151. package/src/assets/css/theme.css +0 -321
  152. package/src/components/AGENTS.md +0 -0
  153. package/src/components/atoms/_layout.tsx +0 -9
  154. package/src/components/atoms/avatar/base.tsx +0 -70
  155. package/src/components/atoms/avatar/index.ts +0 -10
  156. package/src/components/atoms/avatar/types.ts +0 -24
  157. package/src/components/atoms/avatar/utils.ts +0 -54
  158. package/src/components/atoms/avatar/verified.tsx +0 -16
  159. package/src/components/atoms/background/index.ts +0 -8
  160. package/src/components/atoms/background/texture.tsx +0 -29
  161. package/src/components/atoms/background/type.ts +0 -10
  162. package/src/components/atoms/background/utils.ts +0 -13
  163. package/src/components/atoms/bubble/__tests__/bubble.test.tsx +0 -193
  164. package/src/components/atoms/bubble/__tests__/reference.test.tsx +0 -25
  165. package/src/components/atoms/bubble/actions.tsx +0 -28
  166. package/src/components/atoms/bubble/context.ts +0 -11
  167. package/src/components/atoms/bubble/default.tsx +0 -46
  168. package/src/components/atoms/bubble/header.tsx +0 -36
  169. package/src/components/atoms/bubble/index.ts +0 -37
  170. package/src/components/atoms/bubble/message.tsx +0 -32
  171. package/src/components/atoms/bubble/reactions.tsx +0 -191
  172. package/src/components/atoms/bubble/types.ts +0 -82
  173. package/src/components/atoms/bubble/utils.ts +0 -46
  174. package/src/components/atoms/chip.tsx +0 -61
  175. package/src/components/atoms/field.tsx +0 -43
  176. package/src/components/atoms/index.ts +0 -26
  177. package/src/components/atoms/loader.tsx +0 -33
  178. package/src/components/atoms/reference.tsx +0 -41
  179. package/src/components/atoms/text/__tests__/text.test.tsx +0 -47
  180. package/src/components/atoms/text/default.tsx +0 -59
  181. package/src/components/atoms/text/heading.tsx +0 -58
  182. package/src/components/atoms/text/index.ts +0 -28
  183. package/src/components/atoms/text/label.tsx +0 -16
  184. package/src/components/atoms/text/paragraph.tsx +0 -55
  185. package/src/components/atoms/text/span.tsx +0 -14
  186. package/src/components/atoms/text/types.ts +0 -41
  187. package/src/components/atoms/text/utils.ts +0 -16
  188. package/src/components/atoms/text-selection/__tests__/click-popover.test.tsx +0 -61
  189. package/src/components/atoms/text-selection/__tests__/text-selection.test.tsx +0 -90
  190. package/src/components/atoms/text-selection/click-popover.tsx +0 -62
  191. package/src/components/atoms/text-selection/index.ts +0 -30
  192. package/src/components/atoms/text-selection/popover.tsx +0 -103
  193. package/src/components/atoms/text-selection/selection-atom.ts +0 -51
  194. package/src/components/atoms/text-selection/selection.tsx +0 -56
  195. package/src/components/atoms/text-selection/types.ts +0 -125
  196. package/src/components/atoms/text-selection/use-selection.ts +0 -84
  197. package/src/components/atoms/types.ts +0 -46
  198. package/src/components/blocks/.gitkeep +0 -0
  199. package/src/components/blocks/auth/__tests__/auth-flow-block.test.tsx +0 -183
  200. package/src/components/blocks/auth/__tests__/auth-state-atom.test.tsx +0 -247
  201. package/src/components/blocks/auth/__tests__/linked-accounts-block.test.tsx +0 -128
  202. package/src/components/blocks/auth/__tests__/onboarding-block.test.tsx +0 -183
  203. package/src/components/blocks/auth/__tests__/passkey-manager-block.test.tsx +0 -131
  204. package/src/components/blocks/auth/__tests__/passkey-sign-in-block.test.tsx +0 -59
  205. package/src/components/blocks/auth/__tests__/signup-block.test.tsx +0 -176
  206. package/src/components/blocks/auth/__tests__/update-password-block.test.tsx +0 -95
  207. package/src/components/blocks/auth/auth-flow-atom.ts +0 -238
  208. package/src/components/blocks/auth/auth-flow-block.tsx +0 -211
  209. package/src/components/blocks/auth/auth-session-atom.ts +0 -82
  210. package/src/components/blocks/auth/auth-shell.tsx +0 -265
  211. package/src/components/blocks/auth/auth-state.ts +0 -40
  212. package/src/components/blocks/auth/code-auth-block.tsx +0 -176
  213. package/src/components/blocks/auth/forgot-password-block.tsx +0 -112
  214. package/src/components/blocks/auth/linked-accounts-block.tsx +0 -203
  215. package/src/components/blocks/auth/login-block.tsx +0 -217
  216. package/src/components/blocks/auth/onboarding/profile-step.tsx +0 -216
  217. package/src/components/blocks/auth/onboarding-block.tsx +0 -183
  218. package/src/components/blocks/auth/passkey-manager-block.tsx +0 -316
  219. package/src/components/blocks/auth/passkey-sign-in-block.tsx +0 -67
  220. package/src/components/blocks/auth/signup-block.tsx +0 -256
  221. package/src/components/blocks/auth/type.ts +0 -436
  222. package/src/components/blocks/auth/update-password-block.tsx +0 -139
  223. package/src/components/blocks/auth/use-auth-state.ts +0 -127
  224. package/src/components/blocks/chat/__tests__/ai-panel.test.tsx +0 -65
  225. package/src/components/blocks/chat/banners.tsx +0 -61
  226. package/src/components/blocks/chat/base.tsx +0 -84
  227. package/src/components/blocks/chat/header.tsx +0 -60
  228. package/src/components/blocks/chat/index.ts +0 -37
  229. package/src/components/blocks/chat/recommendation-stack.tsx +0 -41
  230. package/src/components/blocks/chat/shared.tsx +0 -27
  231. package/src/components/blocks/chat/suggested-prompts.tsx +0 -45
  232. package/src/components/blocks/chat/type.ts +0 -96
  233. package/src/components/blocks/chat/version-history-record.tsx +0 -37
  234. package/src/components/blocks/layout.ts +0 -35
  235. package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +0 -362
  236. package/src/components/blocks/profile/index.ts +0 -28
  237. package/src/components/blocks/profile/profile-card-atom.ts +0 -247
  238. package/src/components/blocks/profile/profile-card-block.tsx +0 -369
  239. package/src/components/blocks/profile/type.ts +0 -93
  240. package/src/components/blocks/profile/use-profile-card-state.ts +0 -67
  241. package/src/components/blocks/profile/use-profile-card.ts +0 -132
  242. package/src/components/blocks/scaffold/__tests__/scaffold-atom.test.tsx +0 -236
  243. package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +0 -524
  244. package/src/components/blocks/scaffold/constants.ts +0 -39
  245. package/src/components/blocks/scaffold/index.ts +0 -60
  246. package/src/components/blocks/scaffold/panel-close-button.tsx +0 -36
  247. package/src/components/blocks/scaffold/panel-menu-button.tsx +0 -221
  248. package/src/components/blocks/scaffold/scaffold-actions.tsx +0 -92
  249. package/src/components/blocks/scaffold/scaffold-atom.ts +0 -427
  250. package/src/components/blocks/scaffold/scaffold-canvas.tsx +0 -162
  251. package/src/components/blocks/scaffold/scaffold-context.ts +0 -20
  252. package/src/components/blocks/scaffold/scaffold-inspector.tsx +0 -137
  253. package/src/components/blocks/scaffold/scaffold-main.tsx +0 -28
  254. package/src/components/blocks/scaffold/scaffold-panel.tsx +0 -186
  255. package/src/components/blocks/scaffold/scaffold-root.tsx +0 -120
  256. package/src/components/blocks/scaffold/scaffold-sidebar.tsx +0 -31
  257. package/src/components/blocks/scaffold/scaffold-sub-panel.tsx +0 -122
  258. package/src/components/blocks/scaffold/scaffold-tab.tsx +0 -108
  259. package/src/components/blocks/scaffold/type.ts +0 -264
  260. package/src/components/blocks/scaffold/use-scaffold-state.ts +0 -85
  261. package/src/components/blocks/scaffold/use-scaffold.ts +0 -59
  262. package/src/components/blocks/scaffold/utils.ts +0 -112
  263. package/src/components/blocks/shell/__tests__/shell-fit-atom.test.tsx +0 -362
  264. package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +0 -596
  265. package/src/components/blocks/shell/__tests__/shell-metrics.test.ts +0 -154
  266. package/src/components/blocks/shell/animated-panel-inset.tsx +0 -27
  267. package/src/components/blocks/shell/animated-panel-provider.tsx +0 -269
  268. package/src/components/blocks/shell/animated-panel-trigger.tsx +0 -51
  269. package/src/components/blocks/shell/animated-panel.tsx +0 -180
  270. package/src/components/blocks/shell/index.ts +0 -30
  271. package/src/components/blocks/shell/shell-fit-atom.ts +0 -246
  272. package/src/components/blocks/shell/shell-layout.tsx +0 -139
  273. package/src/components/blocks/shell/shell-metrics.ts +0 -133
  274. package/src/components/blocks/shell/shell-panel-atom.ts +0 -74
  275. package/src/components/blocks/shell/type.ts +0 -361
  276. package/src/components/blocks/shell/use-shell-fit-state.ts +0 -49
  277. package/src/components/blocks/shell/use-shell-fit.ts +0 -153
  278. package/src/components/blocks/shell/use-shell-panels.ts +0 -126
  279. package/src/components/blocks/shell/utils.ts +0 -187
  280. package/src/components/blocks/sidebar/__tests__/ai-sidebar-atom.test.tsx +0 -325
  281. package/src/components/blocks/sidebar/__tests__/ai-sidebar.test.tsx +0 -93
  282. package/src/components/blocks/sidebar/__tests__/use-ai-sidebar.test.tsx +0 -310
  283. package/src/components/blocks/sidebar/ai-sidebar-atom.ts +0 -698
  284. package/src/components/blocks/sidebar/ai-sidebar.tsx +0 -150
  285. package/src/components/blocks/sidebar/index.ts +0 -67
  286. package/src/components/blocks/sidebar/marquee-label.tsx +0 -59
  287. package/src/components/blocks/sidebar/sidebar-context.ts +0 -15
  288. package/src/components/blocks/sidebar/sidebar-icon.tsx +0 -14
  289. package/src/components/blocks/sidebar/sidebar-row.tsx +0 -378
  290. package/src/components/blocks/sidebar/type.ts +0 -294
  291. package/src/components/blocks/sidebar/use-ai-sidebar-state.ts +0 -123
  292. package/src/components/blocks/sidebar/use-ai-sidebar.ts +0 -619
  293. package/src/components/blocks/sidebar/utils.ts +0 -170
  294. package/src/components/blocks/types.ts +0 -6
  295. package/src/components/composed/.gitkeep +0 -0
  296. package/src/components/composed/__tests__/logo.test.tsx +0 -60
  297. package/src/components/composed/action-bar/__tests__/action-bar.test.tsx +0 -125
  298. package/src/components/composed/action-bar/action.tsx +0 -77
  299. package/src/components/composed/action-bar/emojis.tsx +0 -97
  300. package/src/components/composed/action-bar/index.ts +0 -24
  301. package/src/components/composed/action-bar/toolbar.tsx +0 -66
  302. package/src/components/composed/action-bar/types.ts +0 -58
  303. package/src/components/composed/action-bar/utils.ts +0 -140
  304. package/src/components/composed/ai/__tests__/ai.test.tsx +0 -66
  305. package/src/components/composed/ai/avatar.tsx +0 -19
  306. package/src/components/composed/ai/index.ts +0 -36
  307. package/src/components/composed/ai/message.tsx +0 -49
  308. package/src/components/composed/ai/research-answer.tsx +0 -157
  309. package/src/components/composed/ai/shared.tsx +0 -94
  310. package/src/components/composed/ai/streaming-text.tsx +0 -94
  311. package/src/components/composed/ai/suggested-prompt.tsx +0 -183
  312. package/src/components/composed/ai/types.ts +0 -81
  313. package/src/components/composed/chat/__tests__/attachment.test.tsx +0 -218
  314. package/src/components/composed/chat/__tests__/beautiful-ui.test.tsx +0 -57
  315. package/src/components/composed/chat/__tests__/chart.test.tsx +0 -316
  316. package/src/components/composed/chat/__tests__/code-block.test.tsx +0 -12
  317. package/src/components/composed/chat/__tests__/composer-attachments.test.tsx +0 -101
  318. package/src/components/composed/chat/__tests__/composer.test.tsx +0 -248
  319. package/src/components/composed/chat/__tests__/flowchart.test.tsx +0 -145
  320. package/src/components/composed/chat/__tests__/glass-material.test.tsx +0 -54
  321. package/src/components/composed/chat/__tests__/markdown.test.tsx +0 -25
  322. package/src/components/composed/chat/__tests__/menu-command.test.tsx +0 -35
  323. package/src/components/composed/chat/__tests__/recommendation.test.tsx +0 -43
  324. package/src/components/composed/chat/actions.tsx +0 -81
  325. package/src/components/composed/chat/attachment-file.ts +0 -82
  326. package/src/components/composed/chat/attachment.tsx +0 -498
  327. package/src/components/composed/chat/chart/chart-atom.ts +0 -55
  328. package/src/components/composed/chat/chart/chart-context.ts +0 -9
  329. package/src/components/composed/chat/chart/chart-legend.tsx +0 -75
  330. package/src/components/composed/chat/chart/chart-plot.tsx +0 -233
  331. package/src/components/composed/chat/chart/chart-root.tsx +0 -229
  332. package/src/components/composed/chat/chart/chart-tooltip.tsx +0 -74
  333. package/src/components/composed/chat/chart/constants.ts +0 -8
  334. package/src/components/composed/chat/chart/evilcharts/README.md +0 -8
  335. package/src/components/composed/chat/chart/evilcharts/echarts-area-chart.tsx +0 -2358
  336. package/src/components/composed/chat/chart/evilcharts/echarts-bar-chart.tsx +0 -2261
  337. package/src/components/composed/chat/chart/evilcharts/echarts-brush.tsx +0 -233
  338. package/src/components/composed/chat/chart/evilcharts/echarts-chart.tsx +0 -214
  339. package/src/components/composed/chat/chart/evilcharts/echarts-dot.tsx +0 -111
  340. package/src/components/composed/chat/chart/evilcharts/echarts-legend.tsx +0 -131
  341. package/src/components/composed/chat/chart/evilcharts/echarts-line-chart.tsx +0 -2112
  342. package/src/components/composed/chat/chart/evilcharts/echarts-pie-chart.tsx +0 -1235
  343. package/src/components/composed/chat/chart/evilcharts/echarts-tooltip.tsx +0 -125
  344. package/src/components/composed/chat/chart/index.ts +0 -13
  345. package/src/components/composed/chat/chart/type.ts +0 -88
  346. package/src/components/composed/chat/chart/use-chart-state.ts +0 -22
  347. package/src/components/composed/chat/chart/use-chart.ts +0 -70
  348. package/src/components/composed/chat/chart/utils.ts +0 -47
  349. package/src/components/composed/chat/chart.tsx +0 -3
  350. package/src/components/composed/chat/code-block.tsx +0 -123
  351. package/src/components/composed/chat/composer/add-button.tsx +0 -29
  352. package/src/components/composed/chat/composer/base.tsx +0 -573
  353. package/src/components/composed/chat/composer/command-menu.tsx +0 -47
  354. package/src/components/composed/chat/composer/hooks.ts +0 -29
  355. package/src/components/composed/chat/composer/index.ts +0 -19
  356. package/src/components/composed/chat/composer/send-button.tsx +0 -36
  357. package/src/components/composed/chat/composer/suggestions.tsx +0 -115
  358. package/src/components/composed/chat/composer/use-dictation.ts +0 -123
  359. package/src/components/composed/chat/composer/utils.ts +0 -74
  360. package/src/components/composed/chat/context-cards.tsx +0 -63
  361. package/src/components/composed/chat/filter-table.tsx +0 -119
  362. package/src/components/composed/chat/flowchart/chart-node.tsx +0 -162
  363. package/src/components/composed/chat/flowchart/connector.tsx +0 -32
  364. package/src/components/composed/chat/flowchart/constant.ts +0 -26
  365. package/src/components/composed/chat/flowchart/default.tsx +0 -230
  366. package/src/components/composed/chat/flowchart/edge-toolbar.tsx +0 -73
  367. package/src/components/composed/chat/flowchart/hooks.tsx +0 -347
  368. package/src/components/composed/chat/flowchart/index.ts +0 -14
  369. package/src/components/composed/chat/flowchart/step-body.tsx +0 -82
  370. package/src/components/composed/chat/flowchart/types.ts +0 -89
  371. package/src/components/composed/chat/flowchart/utils.ts +0 -78
  372. package/src/components/composed/chat/hint.tsx +0 -59
  373. package/src/components/composed/chat/index.ts +0 -92
  374. package/src/components/composed/chat/insight-cards.tsx +0 -199
  375. package/src/components/composed/chat/markdown-atom.ts +0 -15
  376. package/src/components/composed/chat/markdown.tsx +0 -213
  377. package/src/components/composed/chat/recommendation/checkbox.tsx +0 -122
  378. package/src/components/composed/chat/recommendation/constant.ts +0 -54
  379. package/src/components/composed/chat/recommendation/default.tsx +0 -25
  380. package/src/components/composed/chat/recommendation/group.tsx +0 -36
  381. package/src/components/composed/chat/recommendation/index.ts +0 -17
  382. package/src/components/composed/chat/recommendation/item.tsx +0 -215
  383. package/src/components/composed/chat/recommendation/types.ts +0 -76
  384. package/src/components/composed/chat/records-table.tsx +0 -130
  385. package/src/components/composed/chat/type.ts +0 -157
  386. package/src/components/composed/logo.tsx +0 -171
  387. package/src/components/composed/password-input.tsx +0 -179
  388. package/src/components/composed/reader/applied-mark.tsx +0 -32
  389. package/src/components/composed/reader/apply-toast.tsx +0 -49
  390. package/src/components/composed/reader/index.ts +0 -10
  391. package/src/components/composed/reader/selection-highlight.tsx +0 -26
  392. package/src/components/composed/reader/selection-popover.tsx +0 -149
  393. package/src/components/composed/reader/type.ts +0 -48
  394. package/src/components/composed/social-providers.tsx +0 -154
  395. package/src/components/composed/tree/CLAUDE.md +0 -145
  396. package/src/components/composed/tree/__tests__/tree-atom.test.tsx +0 -217
  397. package/src/components/composed/tree/__tests__/tree.test.tsx +0 -522
  398. package/src/components/composed/tree/__tests__/use-tree.test.tsx +0 -333
  399. package/src/components/composed/tree/constants.ts +0 -55
  400. package/src/components/composed/tree/index.ts +0 -51
  401. package/src/components/composed/tree/tree-atom.ts +0 -590
  402. package/src/components/composed/tree/tree-context.ts +0 -13
  403. package/src/components/composed/tree/tree-node.tsx +0 -496
  404. package/src/components/composed/tree/tree.tsx +0 -233
  405. package/src/components/composed/tree/type.ts +0 -322
  406. package/src/components/composed/tree/use-tree-dnd.ts +0 -179
  407. package/src/components/composed/tree/use-tree-state.ts +0 -105
  408. package/src/components/composed/tree/use-tree.ts +0 -311
  409. package/src/components/composed/tree/utils.ts +0 -172
  410. package/src/components/composed/types.ts +0 -9
  411. package/src/components/composed/user/__tests__/message.test.tsx +0 -79
  412. package/src/components/composed/user/index.ts +0 -14
  413. package/src/components/composed/user/info.tsx +0 -56
  414. package/src/components/composed/user/message.tsx +0 -36
  415. package/src/components/composed/user/pill.tsx +0 -20
  416. package/src/components/composed/user/types.ts +0 -47
  417. package/src/components/composed/user/utils.ts +0 -0
  418. package/src/components/composed/verse/__tests__/verse.test.tsx +0 -57
  419. package/src/components/composed/verse/index.ts +0 -2
  420. package/src/components/composed/verse/types.ts +0 -24
  421. package/src/components/composed/verse/verse.tsx +0 -100
  422. package/src/components/docs/atoms-gallery.tsx +0 -286
  423. package/src/components/docs/block-demo-stage.tsx +0 -38
  424. package/src/components/docs/blocks-gallery.tsx +0 -224
  425. package/src/components/docs/browser.tsx +0 -87
  426. package/src/components/docs/code-block.tsx +0 -10
  427. package/src/components/docs/components-gallery.tsx +0 -182
  428. package/src/components/docs/demo-controls.tsx +0 -142
  429. package/src/components/docs/hero.tsx +0 -197
  430. package/src/components/docs/mermaid.tsx +0 -55
  431. package/src/components/icons/README.md +0 -91
  432. package/src/components/icons/__tests__/icons.test.tsx +0 -58
  433. package/src/components/icons/file-badge-cfm.tsx +0 -615
  434. package/src/components/icons/file-badge-corpus.tsx +0 -604
  435. package/src/components/icons/file-badge-pdf.tsx +0 -570
  436. package/src/components/icons/file-badge-tei.tsx +0 -524
  437. package/src/components/icons/file-badge-tf.tsx +0 -620
  438. package/src/components/icons/file-badge-txt.tsx +0 -607
  439. package/src/components/icons/file-badge-xml.tsx +0 -576
  440. package/src/components/icons/file-wordmark-cfm.tsx +0 -424
  441. package/src/components/icons/file-wordmark-corpus.tsx +0 -454
  442. package/src/components/icons/file-wordmark-pdf.tsx +0 -470
  443. package/src/components/icons/file-wordmark-tei.tsx +0 -424
  444. package/src/components/icons/file-wordmark-tf.tsx +0 -406
  445. package/src/components/icons/file-wordmark-txt.tsx +0 -421
  446. package/src/components/icons/file-wordmark-xml.tsx +0 -424
  447. package/src/components/icons/index.ts +0 -16
  448. package/src/components/icons/types.ts +0 -8
  449. package/src/components/motion/index.ts +0 -0
  450. package/src/components/motion/overflow-actions.tsx +0 -376
  451. package/src/components/motion/popover-morph.tsx +0 -292
  452. package/src/components/motion/popover-position.ts +0 -86
  453. package/src/components/motion/shared-layout-bg.tsx +0 -151
  454. package/src/components/stories/attachment.story.tsx +0 -40
  455. package/src/components/stories/auth-flow-block.story.tsx +0 -17
  456. package/src/components/stories/avatar.story.tsx +0 -33
  457. package/src/components/stories/base.story.tsx +0 -70
  458. package/src/components/stories/bubble.story.tsx +0 -61
  459. package/src/components/stories/button.story.tsx +0 -32
  460. package/src/components/stories/card.story.tsx +0 -36
  461. package/src/components/stories/chart.story.tsx +0 -69
  462. package/src/components/stories/checkbox.story.tsx +0 -30
  463. package/src/components/stories/code-auth-block.story.tsx +0 -20
  464. package/src/components/stories/code-block.story.tsx +0 -35
  465. package/src/components/stories/composed-password-input.story.tsx +0 -41
  466. package/src/components/stories/context-cards.story.tsx +0 -30
  467. package/src/components/stories/emoji-action-bar.story.tsx +0 -20
  468. package/src/components/stories/field.story.tsx +0 -32
  469. package/src/components/stories/file-icons.story.tsx +0 -20
  470. package/src/components/stories/filter-table.story.tsx +0 -63
  471. package/src/components/stories/flowchart.story.tsx +0 -25
  472. package/src/components/stories/forgot-password-block.story.tsx +0 -14
  473. package/src/components/stories/frame.story.tsx +0 -32
  474. package/src/components/stories/index.ts +0 -57
  475. package/src/components/stories/input-group.story.tsx +0 -27
  476. package/src/components/stories/input.story.tsx +0 -42
  477. package/src/components/stories/insight-cards.story.tsx +0 -67
  478. package/src/components/stories/label.story.tsx +0 -32
  479. package/src/components/stories/linked-accounts-block.story.tsx +0 -23
  480. package/src/components/stories/login-block.story.tsx +0 -18
  481. package/src/components/stories/logo.story.tsx +0 -21
  482. package/src/components/stories/markdown.story.tsx +0 -31
  483. package/src/components/stories/menu-command.story.tsx +0 -43
  484. package/src/components/stories/onboarding-block.story.tsx +0 -14
  485. package/src/components/stories/otp-field.story.tsx +0 -35
  486. package/src/components/stories/passkey-manager-block.story.tsx +0 -18
  487. package/src/components/stories/passkey-sign-in-block.story.tsx +0 -15
  488. package/src/components/stories/password-input.story.tsx +0 -35
  489. package/src/components/stories/profile-card-block.story.tsx +0 -29
  490. package/src/components/stories/recommendation-card.story.tsx +0 -37
  491. package/src/components/stories/records-table.story.tsx +0 -52
  492. package/src/components/stories/reference.story.tsx +0 -33
  493. package/src/components/stories/research-answer.story.tsx +0 -32
  494. package/src/components/stories/scaffold-root.story.tsx +0 -47
  495. package/src/components/stories/separator.story.tsx +0 -37
  496. package/src/components/stories/shell-layout.story.tsx +0 -19
  497. package/src/components/stories/signup-block.story.tsx +0 -18
  498. package/src/components/stories/social-providers.story.tsx +0 -26
  499. package/src/components/stories/streaming-text.story.tsx +0 -25
  500. package/src/components/stories/text.story.tsx +0 -31
  501. package/src/components/stories/textarea.story.tsx +0 -34
  502. package/src/components/stories/tree.story.tsx +0 -31
  503. package/src/components/stories/update-password-block.story.tsx +0 -14
  504. package/src/components/stories/user.story.tsx +0 -32
  505. package/src/components/stories/verse.story.tsx +0 -42
  506. package/src/components/theme-provider.tsx +0 -230
  507. package/src/components/types.ts +0 -106
  508. package/src/components/ui/accordion.tsx +0 -68
  509. package/src/components/ui/alert-dialog.tsx +0 -167
  510. package/src/components/ui/alert.tsx +0 -84
  511. package/src/components/ui/autocomplete.tsx +0 -303
  512. package/src/components/ui/avatar.tsx +0 -109
  513. package/src/components/ui/badge.tsx +0 -101
  514. package/src/components/ui/button-group.tsx +0 -88
  515. package/src/components/ui/button.tsx +0 -192
  516. package/src/components/ui/card.tsx +0 -253
  517. package/src/components/ui/chat/__tests__/segmented-toggle.test.tsx +0 -27
  518. package/src/components/ui/chat/avatar-handle.tsx +0 -30
  519. package/src/components/ui/chat/badges.tsx +0 -85
  520. package/src/components/ui/chat/buttons.tsx +0 -94
  521. package/src/components/ui/chat/chips.tsx +0 -106
  522. package/src/components/ui/chat/dot.tsx +0 -81
  523. package/src/components/ui/chat/icon-button.tsx +0 -43
  524. package/src/components/ui/chat/icon-tile.tsx +0 -33
  525. package/src/components/ui/chat/index.ts +0 -12
  526. package/src/components/ui/chat/inline-source.tsx +0 -94
  527. package/src/components/ui/chat/segmented-toggle.tsx +0 -99
  528. package/src/components/ui/chat/tag.tsx +0 -61
  529. package/src/components/ui/chat/thumbnail.tsx +0 -31
  530. package/src/components/ui/chat/waveform.tsx +0 -30
  531. package/src/components/ui/checkbox.tsx +0 -83
  532. package/src/components/ui/collapsible.tsx +0 -42
  533. package/src/components/ui/command.tsx +0 -263
  534. package/src/components/ui/context-menu.tsx +0 -342
  535. package/src/components/ui/emoji-picker.tsx +0 -161
  536. package/src/components/ui/field.tsx +0 -89
  537. package/src/components/ui/frame.tsx +0 -87
  538. package/src/components/ui/glasscn/glass-button-group.tsx +0 -34
  539. package/src/components/ui/glasscn/glass-button.tsx +0 -25
  540. package/src/components/ui/glasscn/glass-container.tsx +0 -94
  541. package/src/components/ui/glasscn/glass-separator.tsx +0 -39
  542. package/src/components/ui/glasscn/liquid-glass.tsx +0 -59
  543. package/src/components/ui/group.tsx +0 -92
  544. package/src/components/ui/input-group.tsx +0 -105
  545. package/src/components/ui/input.tsx +0 -70
  546. package/src/components/ui/kbd.tsx +0 -32
  547. package/src/components/ui/label.tsx +0 -36
  548. package/src/components/ui/menu-command.tsx +0 -156
  549. package/src/components/ui/menu.tsx +0 -352
  550. package/src/components/ui/otp-field.tsx +0 -88
  551. package/src/components/ui/popover-glass.tsx +0 -158
  552. package/src/components/ui/popover.tsx +0 -120
  553. package/src/components/ui/preview-card.tsx +0 -61
  554. package/src/components/ui/scroll-area.tsx +0 -80
  555. package/src/components/ui/select.tsx +0 -257
  556. package/src/components/ui/separator.tsx +0 -23
  557. package/src/components/ui/sheet.tsx +0 -135
  558. package/src/components/ui/skeleton.tsx +0 -18
  559. package/src/components/ui/spinner.tsx +0 -17
  560. package/src/components/ui/switch.tsx +0 -30
  561. package/src/components/ui/tabs.tsx +0 -120
  562. package/src/components/ui/textarea.tsx +0 -58
  563. package/src/components/ui/toast.tsx +0 -325
  564. package/src/components/ui/toolbar.tsx +0 -92
  565. package/src/components/ui/tooltip.tsx +0 -67
  566. package/src/index.css +0 -10
  567. package/src/index.ts +0 -102
  568. package/src/lib/attachment-content.ts +0 -28
  569. package/src/lib/auth-accent.ts +0 -49
  570. package/src/lib/ease.ts +0 -90
  571. package/src/lib/glass-variants.ts +0 -22
  572. package/src/lib/hooks/use-hover-capable.ts +0 -23
  573. package/src/lib/hooks/use-mobile.ts +0 -21
  574. package/src/lib/keyed-atom.ts +0 -60
  575. package/src/lib/segmented-control.ts +0 -42
  576. package/src/lib/sound.ts +0 -39
  577. package/src/lib/specular.ts +0 -10
  578. package/src/lib/state/__tests__/sound.test.tsx +0 -53
  579. package/src/lib/state/exegia-provider.tsx +0 -83
  580. package/src/lib/state/index.ts +0 -4
  581. package/src/lib/state/store.ts +0 -19
  582. package/src/lib/use-motion-panel.ts +0 -78
  583. package/src/lib/utils.test.ts +0 -22
  584. package/src/lib/utils.ts +0 -100
  585. package/src/specular.js +0 -4
  586. /package/{src/components/composed/chat/composer → dist-lib}/BEAUTIFUL-UI-LICENSE.txt +0 -0
  587. /package/{src/components/composed/chat/chart/evilcharts/LICENSE → dist-lib/EVILCHARTS-LICENSE.txt} +0 -0
@@ -1,2261 +0,0 @@
1
- "use client";
2
-
3
- import {
4
- DEFAULT_ECHARTS_RENDERER,
5
- buildChartCss,
6
- flattenColor,
7
- getColorsCount,
8
- resolveColors,
9
- withAlpha,
10
- type ChartConfig,
11
- type EChartsRenderer,
12
- type ResolvedColors,
13
- } from "./echarts-chart";
14
- import {
15
- tooltipBaseOption,
16
- tooltipIndicatorHtml,
17
- tooltipRow,
18
- tooltipShell,
19
- type TooltipPosition,
20
- type TooltipRoundness,
21
- type TooltipVariant,
22
- } from "./echarts-tooltip";
23
- import {
24
- Brush,
25
- buildBrushDataZoom,
26
- syncBrushOverlay,
27
- type BrushGeometry,
28
- type BrushOverlayElements,
29
- type BrushProps,
30
- type BrushRange,
31
- } from "./echarts-brush";
32
- import {
33
- DataZoomComponent,
34
- GridComponent,
35
- TooltipComponent,
36
- type DataZoomComponentOption,
37
- type GridComponentOption,
38
- type TooltipComponentOption,
39
- } from "echarts/components";
40
- import {
41
- Children,
42
- isValidElement,
43
- useCallback,
44
- useEffect,
45
- useId,
46
- useMemo,
47
- useRef,
48
- useState,
49
- type CSSProperties,
50
- type FC,
51
- type ReactNode,
52
- } from "react";
53
- import { LegendOverlay, type LegendVariant } from "./echarts-legend";
54
- import type { ComposeOption, ImagePatternObject } from "echarts/core";
55
- import { BarChart, type BarSeriesOption } from "echarts/charts";
56
- import { sampleGradient } from "./echarts-dot";
57
- import { motion, useReducedMotion } from "motion/react";
58
- import * as echarts from "echarts/core";
59
-
60
- // Re-export the shared types that were previously declared inline here, so
61
- // existing consumers/examples keep importing them from the chart module.
62
- export type {
63
- ChartConfig,
64
- EChartsRenderer,
65
- LegendVariant,
66
- TooltipPosition,
67
- TooltipRoundness,
68
- TooltipVariant,
69
- };
70
-
71
- // Modular registration keeps the bundle lean — only the pieces this chart needs.
72
- // `DataZoomComponent` bundles both the slider (brush footer) and inside (wheel/drag)
73
- // zoom. The brush's frame/handles/labels are raw zrender elements, not the
74
- // graphic component — see syncBrushOverlay. No LineChart: the main plot, the
75
- // loading skeleton, and the brush mini chart are ALL bar series.
76
- echarts.use([BarChart, GridComponent, TooltipComponent, DataZoomComponent]);
77
-
78
- type EChartsInstance = ReturnType<typeof echarts.init>;
79
-
80
- // The exact option surface this chart uses — bar series, grid, tooltip, and
81
- // dataZoom, plus the axis options they pull in as dependencies. Narrower than
82
- // echarts' full EChartsOption, so a misspelled key fails the compile instead of
83
- // silently reaching setOption.
84
- type EChartsOption = ComposeOption<
85
- BarSeriesOption | GridComponentOption | TooltipComponentOption | DataZoomComponentOption
86
- >;
87
-
88
- // Single-entry views of the composed option's array-or-single fields — the
89
- // modular entry points don't export the axis option types directly.
90
- type ArrayItem<T> = T extends readonly (infer U)[] ? U : T;
91
- type XAxisOption = ArrayItem<NonNullable<EChartsOption["xAxis"]>>;
92
- type YAxisOption = ArrayItem<NonNullable<EChartsOption["yAxis"]>>;
93
-
94
- // ─────────────────────────────────────────────────────────────────────────────
95
- // Constants
96
- // ─────────────────────────────────────────────────────────────────────────────
97
-
98
- const DEFAULT_BAR_RADIUS = 2;
99
- const STROKE_WIDTH = 1; // buffer-bar outline width
100
- const LOADING_ANIMATION_DURATION = 2000; // shimmer loop, in milliseconds
101
- const BAR_GROW_DURATION = 500; // per-bar grow-in length, in milliseconds
102
- const BAR_STAGGER = 50; // delay between consecutive bars in the reveal, in milliseconds
103
- const LOADING_DEFAULT_BARS = 12;
104
- // `revealEndsAt` marks when the intro grow-in finishes. The stripped-cap post-layout
105
- // correction (a notMerge repush) waits for it so it never lands mid-entrance and
106
- // stomps the grow — the same reason the area chart tracks this timestamp.
107
- const SELECTION_DIM = 0.3; // opacity of an unselected series while a selection is active
108
- const HOVER_BLUR = 0.3; // opacity of the non-hovered bars while hover-highlight is on
109
- // Soft outer glow — the canvas analogue of the Recharts feGaussianBlur filter
110
- // (stdDeviation 8, alpha 0.5). A generous shadowBlur keeps the halo soft with no
111
- // hard rim; the shadowColor is sampled PER BAR so a multi-stop gradient series
112
- // glows in its own colors across the plot instead of one flat tint.
113
- const GLOW_BLUR = 18; // shadowBlur radius, in device-independent pixels
114
- const GLOW_OPACITY = 0.65; // per-datum shadowColor alpha, × the sampled series color
115
-
116
- // The `blocks` variant renders each bar as a stack of segments instead of a solid
117
- // column: a repeating tile paints a BLOCK_SIZE band then leaves a BLOCK_GAP of
118
- // transparency. The same tile, in a muted tone, fills the column's unused space
119
- // via ECharts' native `showBackground`, so the empty part of every bar reads as a
120
- // dim grid of the same blocks. Both tile from the renderer origin, so the bands
121
- // line up across every column.
122
- // The `expandable` variant draws every bar at full width but fills only a narrow
123
- // centre strip, so it reads as a thin line; hovering one grows its strip out to
124
- // the full width and back on leave. The bar geometry never changes — only the
125
- // horizontal extent of its fill — so nothing re-lays out mid-hover.
126
- const EXPAND_COLLAPSED = 0.12; // resting strip width, as a fraction of the bar
127
- const EXPAND_TAU = 70; // ease time-constant, in milliseconds (exponential approach)
128
-
129
- const BLOCK_SIZE = 8; // filled segment height, in pixels
130
- const BLOCK_GAP = 4; // transparent gap between segments, in pixels
131
- const BLOCK_TRACK_OPACITY = 0.22; // unfilled block tone, x the muted-foreground alpha
132
- // Stacked segments would otherwise butt straight into each other and read as one
133
- // solid column. The separation is a REAL gap — transparent spacer series stacked
134
- // between the real ones — not a background-colored border: a border paints on all
135
- // four sides, so it outlines each segment (obvious the moment a bar glows) instead
136
- // of only parting them.
137
- const STACK_SEGMENT_GAP = 4; // separation between stacked segments, in pixels
138
- const MAX_HIGHLIGHT_DIM = 0.16; // non-winning columns under enableMaxValueHighlight, x muted-foreground
139
-
140
- // The `stripped` variant caps each bar with a small BRIGHT pill of CONSTANT pixel
141
- // height (Recharts draws a fixed ~2px strip on top of a dimmed body, identical on
142
- // tall and short bars). The cap is expressed PER DATUM as a fraction of that bar's
143
- // own pixel height, so a fixed pixel height maps to a shrinking fraction as the bar
144
- // grows — the fraction is derived at runtime from the measured value-axis
145
- // pixels-per-unit (see measureValuePxPerUnit). A canvas gradient alone can't do
146
- // this: its bright band is a fraction of the bounding box, so it would scale with
147
- // bar length (the bug this replaced).
148
- const STRIPPED_CAP_HEIGHT = 4; // bright cap height, in device-independent pixels
149
- const STRIPPED_BODY_ALPHA = 0.2; // dimmed bar body below the cap, × series color
150
- const STRIPPED_CAP_MAX_FRACTION = 0.85; // cap never swallows a whole (very short) bar
151
- const STRIPPED_FALLBACK_FRACTION = 0.12; // used before the axis geometry is measured
152
-
153
- // ─────────────────────────────────────────────────────────────────────────────
154
- // Theme knobs — every neutral line in the chart draws from these. Base colors
155
- // come from the consumer's CSS tokens (resolved from the live DOM), so only the
156
- // opacity factors live here. Factors MULTIPLY the token's own alpha — a border
157
- // token that is already 10%-white stays subtle. Tune here, not in the builder.
158
- // ─────────────────────────────────────────────────────────────────────────────
159
- // Recharts draws its grid at border/50, but SVG dashes render pixel-crisp while
160
- // canvas at 2× DPR spreads a 1px line across device pixels — roughly halving
161
- // perceived intensity. Using the border token's full alpha lands both engines at
162
- // the same apparent brightness.
163
- const GRID_LINE_OPACITY = 1; // dashed value-axis split lines, × border alpha
164
- // The skeleton is CLIPPED to a small sweeping window — only the bars inside it
165
- // exist, everything outside is fully transparent, like a spotlight sliding across.
166
- const LOADING_SHIMMER_MAX_OPACITY = 0.22; // gray bar fill inside the window, × foreground alpha
167
- const LOADING_SHIMMER_BAND = 0.2; // window half-width, fraction of the 45° sweep axis
168
- const LOADING_SHIMMER_FEATHER = 0.2; // eased edge softening of the clip window
169
- const BRUSH_FILL_OPACITY = 0.5; // mini-chart bar fill
170
- const BRUSH_FILLER_OPACITY = 0; // selected-range wash — evil-brush draws none
171
-
172
- // ─────────────────────────────────────────────────────────────────────────────
173
- // Public types
174
- // ─────────────────────────────────────────────────────────────────────────────
175
-
176
- export type BarVariant =
177
- | "default"
178
- | "hatched"
179
- | "duotone"
180
- | "duotone-reverse"
181
- | "gradient"
182
- | "stripped"
183
- | "blocks"
184
- | "expandable";
185
- export type StackType = "default" | "stacked" | "percent";
186
- export type BarLayout = "vertical" | "horizontal";
187
- export type BarAnimationType =
188
- | "none"
189
- | "left-to-right"
190
- | "right-to-left"
191
- | "center-out"
192
- | "edges-in";
193
- // TooltipVariant, TooltipRoundness, LegendVariant, and ChartConfig now live in
194
- // the shared ./echarts/* modules and are imported + re-exported at
195
- // the top of this file.
196
-
197
- export interface EChartsBarChartProps<TData extends Record<string, unknown>> {
198
- data: TData[]; // rows rendered by the chart
199
- config: ChartConfig; // series colors + labels
200
- renderer?: EChartsRenderer; // rendering engine — defaults to canvas
201
- xDataKey?: keyof TData & string; // category key — falls back to the axis dataKey / first free column
202
- className?: string; // extra classes for the chart container
203
- stackType?: StackType; // how multiple bars combine
204
- layout?: BarLayout; // orientation of the bars
205
- barRadius?: number; // default corner radius every <Bar> inherits
206
- animation?: boolean; // master switch for the intro grow-in — false renders instantly
207
- animationType?: BarAnimationType; // default grow-in order each <Bar> inherits
208
- barGap?: number; // gap between bars within the same category, in pixels
209
- barCategoryGap?: number; // gap between categories of bars, in pixels
210
- selectedDataKey?: string | null;
211
- defaultSelectedDataKey?: string | null; // series selected on first render
212
- onSelectionChange?: (key: string | null) => void; // fires when the selected series changes
213
- // Colors ONLY the tallest column and mutes the rest. With several series the
214
- // comparison is per COLUMN — the totals across every series at that category —
215
- // so a whole stack or group lights up together, not one bar inside it.
216
- enableMaxValueHighlight?: boolean;
217
- isLoading?: boolean; // shows the animated loading skeleton
218
- loadingBars?: number; // number of bars in the loading skeleton
219
- chartOptions?: Record<string, unknown>; // escape hatch merged over the built ECharts option
220
- children?: ReactNode; // declarative config — <Bar>, <XAxis>, <YAxis>, <Grid>, <Tooltip>, <Legend>, <Brush>
221
- }
222
-
223
- // ─────────────────────────────────────────────────────────────────────────────
224
- // Composible parts — DECLARATIVE CONFIG. Every part renders `null`; the root
225
- // walks `children` by reference (child.type === Bar, …) to collect its props.
226
- // Presence semantics mirror the Recharts twin: omit a child and that part does
227
- // not render. These are never mounted into the tree — they only carry props.
228
- // ─────────────────────────────────────────────────────────────────────────────
229
-
230
- export interface BarProps {
231
- dataKey: string; // series key — must exist on the data + config
232
- variant?: BarVariant; // fill style for this bar only
233
- radius?: number; // corner radius — falls back to the root barRadius
234
- animationType?: BarAnimationType; // grow-in order — falls back to the root animationType
235
- isClickable?: boolean; // lets this bar be selected by clicking it
236
- enableHoverHighlight?: boolean; // dims the other bars while one is hovered
237
- glowing?: boolean; // applies a soft outer glow to this bar
238
- bufferBar?: boolean; // renders the last data point as a hatched "buffer" bar
239
- }
240
-
241
- /**
242
- * A single bar series. Declares its own fill variant, radius, glow, buffer, and
243
- * clickability. Renders nothing — the root reads these props to build the
244
- * ECharts series.
245
- */
246
- const Bar: FC<BarProps> = () => null;
247
-
248
- export interface XAxisProps {
249
- dataKey?: string; // category key — overrides the root xDataKey (vertical layout)
250
- // Category values are stringified, so the formatter always sees a string —
251
- // letting examples share `(value) => value.substring(0, 3)` with the Recharts twin.
252
- tickFormatter?: (value: string, index: number) => string; // formats x tick labels
253
- label?: string; // axis title, centered below the x-position tick labels
254
- hideDots?: boolean; // hides the tick dots beside this axis's labels
255
- }
256
-
257
- /**
258
- * The x-axis. Category axis in the default (vertical) layout, value axis when
259
- * `layout="horizontal"`. Presence shows its tick labels. Renders nothing.
260
- */
261
- const XAxis: FC<XAxisProps> = () => null;
262
-
263
- export interface YAxisProps {
264
- dataKey?: string; // category key — overrides the root xDataKey (horizontal layout)
265
- tickFormatter?: (value: string, index: number) => string; // formats y tick labels
266
- label?: string; // axis title, rotated alongside the y-position tick labels
267
- hideDots?: boolean; // hides the tick dots beside this axis's labels
268
- }
269
-
270
- /**
271
- * The y-axis. Value axis in the default (vertical) layout, category axis when
272
- * `layout="horizontal"`. Presence shows its tick labels. Renders nothing.
273
- */
274
- const YAxis: FC<YAxisProps> = () => null;
275
-
276
- /** Presence shows the dashed split lines on the value axis. Renders nothing. */
277
- const Grid: FC = () => null;
278
-
279
- export interface TooltipProps {
280
- variant?: TooltipVariant; // visual style of the tooltip surface
281
- roundness?: TooltipRoundness; // border-radius of the tooltip
282
- defaultIndex?: number; // data index the tooltip shows by default, with no hover
283
- position?: TooltipPosition; // "variable" follows the pointer (default); "fixed" pins the tooltip near the top and only tracks the pointer's X
284
- }
285
-
286
- /** Presence enables the hover tooltip. Renders nothing. */
287
- const Tooltip: FC<TooltipProps> = () => null;
288
-
289
- export interface LegendProps {
290
- variant?: LegendVariant; // visual style of the legend indicators
291
- align?: "left" | "center" | "right"; // horizontal placement
292
- verticalAlign?: "top" | "middle" | "bottom"; // vertical placement
293
- isClickable?: boolean; // lets each entry toggle selection of its series
294
- }
295
-
296
- /** Presence enables the HTML legend overlay. Renders nothing. */
297
- const Legend: FC<LegendProps> = () => null;
298
-
299
- // ─────────────────────────────────────────────────────────────────────────────
300
- // Children collection — walk the declarative config into plain objects the
301
- // option builder consumes.
302
- // ─────────────────────────────────────────────────────────────────────────────
303
-
304
- type BarSeriesConfig = {
305
- dataKey: string;
306
- variant: BarVariant;
307
- radius?: number;
308
- animationType?: BarAnimationType;
309
- isClickable: boolean;
310
- enableHoverHighlight: boolean;
311
- glowing: boolean;
312
- bufferBar: boolean;
313
- };
314
-
315
- type AxisSlot = {
316
- present: boolean;
317
- dataKey?: string;
318
- tickFormatter?: (value: string, index: number) => string;
319
- label?: string;
320
- hideDots: boolean;
321
- };
322
- type TooltipSlot = {
323
- present: boolean;
324
- variant: TooltipVariant;
325
- roundness: TooltipRoundness;
326
- defaultIndex?: number;
327
- position: TooltipPosition;
328
- };
329
- type LegendSlot = {
330
- present: boolean;
331
- variant: LegendVariant;
332
- align: "left" | "center" | "right";
333
- verticalAlign: "top" | "middle" | "bottom";
334
- isClickable: boolean;
335
- };
336
- type BrushSlot = {
337
- present: boolean; // a <Brush> child was passed — replaces the old showBrush prop
338
- height?: number;
339
- formatLabel?: (value: string, index: number) => string;
340
- onChange?: (range: { startIndex: number; endIndex: number }) => void;
341
- };
342
-
343
- type CollectedConfig = {
344
- bars: BarSeriesConfig[];
345
- xAxis: AxisSlot;
346
- yAxis: AxisSlot;
347
- showGrid: boolean;
348
- tooltip: TooltipSlot;
349
- legend: LegendSlot;
350
- brush: BrushSlot;
351
- };
352
-
353
- function collectConfig(children: ReactNode): CollectedConfig {
354
- const bars: BarSeriesConfig[] = [];
355
- let xAxis: AxisSlot = { present: false, hideDots: false };
356
- let yAxis: AxisSlot = { present: false, hideDots: false };
357
- let showGrid = false;
358
- let tooltip: TooltipSlot = {
359
- present: false,
360
- variant: "default",
361
- roundness: "lg",
362
- position: "variable",
363
- };
364
- let legend: LegendSlot = {
365
- present: false,
366
- variant: "rounded-square",
367
- align: "right",
368
- verticalAlign: "top",
369
- isClickable: false,
370
- };
371
- let brush: BrushSlot = { present: false };
372
-
373
- Children.forEach(children, (child) => {
374
- if (!isValidElement(child)) return;
375
- const type = child.type;
376
-
377
- if (type === Bar) {
378
- const props = child.props as BarProps;
379
- bars.push({
380
- dataKey: props.dataKey,
381
- variant: props.variant ?? "default",
382
- radius: props.radius,
383
- animationType: props.animationType,
384
- isClickable: props.isClickable ?? false,
385
- enableHoverHighlight: props.enableHoverHighlight ?? false,
386
- glowing: props.glowing ?? false,
387
- bufferBar: props.bufferBar ?? false,
388
- });
389
- } else if (type === XAxis) {
390
- const props = child.props as XAxisProps;
391
- xAxis = {
392
- present: true,
393
- dataKey: props.dataKey,
394
- tickFormatter: props.tickFormatter,
395
- label: props.label,
396
- hideDots: props.hideDots ?? false,
397
- };
398
- } else if (type === YAxis) {
399
- const props = child.props as YAxisProps;
400
- yAxis = {
401
- present: true,
402
- dataKey: props.dataKey,
403
- tickFormatter: props.tickFormatter,
404
- label: props.label,
405
- hideDots: props.hideDots ?? false,
406
- };
407
- } else if (type === Grid) {
408
- showGrid = true;
409
- } else if (type === Tooltip) {
410
- const props = child.props as TooltipProps;
411
- tooltip = {
412
- present: true,
413
- variant: props.variant ?? "default",
414
- roundness: props.roundness ?? "lg",
415
- defaultIndex: props.defaultIndex,
416
- position: props.position ?? "variable",
417
- };
418
- } else if (type === Legend) {
419
- const props = child.props as LegendProps;
420
- legend = {
421
- present: true,
422
- variant: props.variant ?? "rounded-square",
423
- align: props.align ?? "right",
424
- verticalAlign: props.verticalAlign ?? "top",
425
- isClickable: props.isClickable ?? false,
426
- };
427
- } else if (type === Brush) {
428
- const props = child.props as BrushProps;
429
- brush = {
430
- present: true,
431
- height: props.height,
432
- formatLabel: props.formatLabel,
433
- onChange: props.onChange,
434
- };
435
- }
436
- });
437
-
438
- return { bars, xAxis, yAxis, showGrid, tooltip, legend, brush };
439
- }
440
-
441
- // Color plumbing (ChartConfig, getColorsCount, distributeColors, buildChartCss,
442
- // normalizeColor, withAlpha, ResolvedColors, resolveColors, flattenColor) plus
443
- // the theme keys now live in ./echarts-chart and are imported at the
444
- // top of this file.
445
-
446
- // ─────────────────────────────────────────────────────────────────────────────
447
- // Fill paints — the ECharts analogue of the Recharts bar fill variants. Unlike
448
- // the area chart's fills (which run the color gradient HORIZONTALLY), a bar's
449
- // base color gradient runs VERTICALLY top→bottom (Recharts `ColorGradient` uses
450
- // x1=x2=0), so each bar shows the full multi-stop gradient in its own box.
451
- // ─────────────────────────────────────────────────────────────────────────────
452
-
453
- const GRAY = "rgba(120, 120, 120, 1)";
454
-
455
- // sampleGradient (the color the series gradient shows at t ∈ [0, 1]) now lives in
456
- // ./echarts-dot and is imported at the top of this file.
457
-
458
- // Solid vertical top→bottom color for a series — a plain string when there is
459
- // one color, else a vertical multi-stop LinearGradient in each bar's own box.
460
- // The `default` variant paints from this at full alpha.
461
- function solidVerticalPaint(
462
- slots: string[],
463
- alpha: number,
464
- ): string | echarts.graphic.LinearGradient {
465
- if (slots.length <= 1) {
466
- const base = slots[0] ?? GRAY;
467
- return alpha === 1 ? base : withAlpha(base, alpha);
468
- }
469
- const stops = slots.map((color, i) => ({
470
- offset: i / (slots.length - 1),
471
- color: withAlpha(color, alpha),
472
- }));
473
- return new echarts.graphic.LinearGradient(0, 0, 0, 1, stops);
474
- }
475
-
476
- // The `gradient` variant: the vertical color gradient faded from solid at the
477
- // top to clear at the bottom. Recharts masks with white@1 at 20% → white@0 at
478
- // 90%, so the alpha holds full through the top fifth and vanishes by 90%.
479
- function verticalFadePaint(slots: string[]): echarts.graphic.LinearGradient {
480
- const offsets = [0, 0.2, 0.45, 0.7, 0.9, 1];
481
- const alphaAt = (t: number) => (t <= 0.2 ? 1 : t >= 0.9 ? 0 : 1 - (t - 0.2) / 0.7);
482
- const stops = offsets.map((t) => ({
483
- offset: t,
484
- color: withAlpha(sampleGradient(slots, t), alphaAt(t)),
485
- }));
486
- return new echarts.graphic.LinearGradient(0, 0, 0, 1, stops);
487
- }
488
-
489
- // The `duotone` family: a hard alpha split across the bar's short axis (its width
490
- // for vertical bars, its height for horizontal). Recharts splits at 50% via an
491
- // objectBoundingBox mask — exact for single-color series; multi-color duotone
492
- // falls back to the base color (an accepted approximation, matching the twin's
493
- // single-color examples).
494
- function duotoneSplitPaint(
495
- base: string,
496
- leftAlpha: number,
497
- rightAlpha: number,
498
- isHorizontal: boolean,
499
- ): echarts.graphic.LinearGradient {
500
- const stops = [
501
- { offset: 0, color: withAlpha(base, leftAlpha) },
502
- { offset: 0.5, color: withAlpha(base, leftAlpha) },
503
- { offset: 0.5, color: withAlpha(base, rightAlpha) },
504
- { offset: 1, color: withAlpha(base, rightAlpha) },
505
- ];
506
- // Split across the cross-axis: horizontal (0→1 in x) for vertical bars, and
507
- // vertical (0→1 in y) for horizontal bars, so it always reads across the bar.
508
- return isHorizontal
509
- ? new echarts.graphic.LinearGradient(0, 0, 0, 1, stops)
510
- : new echarts.graphic.LinearGradient(1, 0, 0, 0, stops);
511
- }
512
-
513
- // The `stripped` variant: a small BRIGHT cap sitting on top of a dimmed (20%) body
514
- // — the canvas twin of Recharts' fixed strip. The cap is baked into a per-datum
515
- // vertical gradient whose bright band spans exactly `capFraction` of the bar
516
- // (offset 0 = the tip). Because `capFraction` is passed in as
517
- // STRIPPED_CAP_HEIGHT / barPixelHeight (see strippedCapFraction), the cap reads the
518
- // SAME pixel height on every bar — the fraction shrinks as the bar grows. A hard
519
- // two-stop edge (coincident offsets at `capFraction`) keeps the cap a crisp pill
520
- // rather than a fade, and the bar's rounded top corners round the cap's top. The
521
- // cap sits at the tip: the top for vertical bars, the value end (right) for
522
- // horizontal.
523
- function strippedDatumPaint(
524
- slots: string[],
525
- isHorizontal: boolean,
526
- capFraction: number,
527
- ): echarts.graphic.LinearGradient {
528
- const f = Math.min(Math.max(capFraction, 0), 1);
529
- const cap = withAlpha(sampleGradient(slots, 0), 1);
530
- const bodyTop = withAlpha(sampleGradient(slots, f), STRIPPED_BODY_ALPHA);
531
- const bodyEnd = withAlpha(sampleGradient(slots, 1), STRIPPED_BODY_ALPHA);
532
- const stops = [
533
- { offset: 0, color: cap },
534
- { offset: f, color: cap },
535
- { offset: f, color: bodyTop },
536
- { offset: 1, color: bodyEnd },
537
- ];
538
- // Tip at offset 0: top (y 0→1) for vertical bars, right (x 1→0) for horizontal.
539
- return isHorizontal
540
- ? new echarts.graphic.LinearGradient(1, 0, 0, 0, stops)
541
- : new echarts.graphic.LinearGradient(0, 0, 0, 1, stops);
542
- }
543
-
544
- // The gradient fraction that renders a STRIPPED_CAP_HEIGHT-pixel cap on a bar whose
545
- // value-axis magnitude is `value`, given the measured pixels-per-unit. Falls back to
546
- // a small constant before the coordinate system has been measured (the very first
547
- // paint, corrected right after layout).
548
- function strippedCapFraction(value: number, valuePxPerUnit: number | null): number {
549
- if (valuePxPerUnit == null) return STRIPPED_FALLBACK_FRACTION;
550
- const barPx = Math.abs(value) * valuePxPerUnit;
551
- if (!(barPx > 0)) return STRIPPED_FALLBACK_FRACTION;
552
- return Math.min(STRIPPED_CAP_HEIGHT / barPx, STRIPPED_CAP_MAX_FRACTION);
553
- }
554
-
555
- // Pixels per one value-axis unit, read straight off the live coordinate system.
556
- // Returns null before the first layout (no coordinate system yet) — callers fall
557
- // back then. Turns the stripped cap's fixed pixel height into a per-bar gradient
558
- // fraction, so the cap stays constant as the value axis rescales on resize/zoom.
559
- function measureValuePxPerUnit(chart: EChartsInstance, isHorizontal: boolean): number | null {
560
- const finder = isHorizontal ? { xAxisIndex: 0 } : { yAxisIndex: 0 };
561
- // convertToPixel throws before the first setOption (no coordinate system yet) and
562
- // whenever the value axis isn't laid out — treat any failure as "not measurable".
563
- try {
564
- const p0 = chart.convertToPixel(finder, 0);
565
- const p1 = chart.convertToPixel(finder, 1);
566
- if (typeof p0 !== "number" || typeof p1 !== "number") return null;
567
- const delta = Math.abs(p1 - p0);
568
- return Number.isFinite(delta) && delta > 0 ? delta : null;
569
- } catch {
570
- return null;
571
- }
572
- }
573
-
574
- // Measures the rendered width of one bar, so the `blocks` variant can make its
575
- // segments square (their width IS the bar width, and only layout knows it). The
576
- // category pitch comes from the axis; the bar occupies that minus the category
577
- // gap — a px number when the consumer set one, else ECharts' own "20%" default.
578
- function measureBarWidthPx(
579
- chart: EChartsInstance,
580
- isHorizontal: boolean,
581
- barCategoryGap: number | undefined,
582
- ): number | null {
583
- const finder = isHorizontal ? { yAxisIndex: 0 } : { xAxisIndex: 0 };
584
- try {
585
- const p0 = chart.convertToPixel(finder, 0);
586
- const p1 = chart.convertToPixel(finder, 1);
587
- if (typeof p0 !== "number" || typeof p1 !== "number") return null;
588
- const pitch = Math.abs(p1 - p0);
589
- if (!Number.isFinite(pitch) || pitch <= 0) return null;
590
- const width = barCategoryGap != null ? pitch - barCategoryGap : pitch * 0.8;
591
- return width > 1 ? width : null;
592
- } catch {
593
- return null;
594
- }
595
- }
596
-
597
- // Tiling texture fills tinted with the series' first color. Stripes are drawn
598
- // STRAIGHT (trivially seamless) and the pattern itself is rotated — zrender
599
- // applies pattern transforms the same way ECharts decals do. Baking a diagonal
600
- // into a square tile clips the stroke at the corners, which reads as periodic
601
- // gaps once tiled. Tiles render at devicePixelRatio and scale back down so the
602
- // texture stays crisp on retina canvases.
603
- function patternFill(
604
- kind: "hatched" | "buffer" | "blocks",
605
- color: string,
606
- blockSize = BLOCK_SIZE,
607
- ): ImagePatternObject | null {
608
- if (typeof document === "undefined") return null;
609
- const dpr = Math.max(window.devicePixelRatio || 1, 1);
610
- const canvas = document.createElement("canvas");
611
- const ctx = canvas.getContext("2d");
612
- if (!ctx) return null;
613
-
614
- const size = (width: number, height: number) => {
615
- canvas.width = width * dpr;
616
- canvas.height = height * dpr;
617
- ctx.scale(dpr, dpr);
618
- };
619
- const pattern = (rotation = 0): ImagePatternObject => ({
620
- image: canvas,
621
- repeat: "repeat",
622
- rotation,
623
- scaleX: 1 / dpr,
624
- scaleY: 1 / dpr,
625
- });
626
-
627
- if (kind === "blocks") {
628
- // 1px-wide tile: it repeats horizontally into a full-width band, and
629
- // vertically into the stack of blocks.
630
- size(1, blockSize + BLOCK_GAP);
631
- ctx.fillStyle = withAlpha(color, 1);
632
- ctx.fillRect(0, 0, 1, blockSize);
633
- return pattern();
634
- }
635
-
636
- if (kind === "hatched") {
637
- // Recharts hatched: the color shown at 0.3 everywhere, punched to full along
638
- // a 1.5px stripe every 5px, leaning -45°.
639
- size(5, 5);
640
- ctx.fillStyle = withAlpha(color, 0.3);
641
- ctx.fillRect(0, 0, 5, 5);
642
- ctx.fillStyle = withAlpha(color, 1);
643
- ctx.fillRect(0, 0, 1.5, 5);
644
- return pattern(-Math.PI / 4);
645
- }
646
-
647
- // buffer: bare diagonal lines on a transparent ground (no body fill), for the
648
- // last "projected" bar.
649
- size(5, 5);
650
- ctx.fillStyle = withAlpha(color, 1);
651
- ctx.fillRect(0, 0, 1, 5);
652
- return pattern(-Math.PI / 4);
653
- }
654
-
655
- // The `expandable` fill at a given openness: a horizontal gradient with HARD
656
- // stops, transparent outside the centre strip and the series paint inside it.
657
- // Animating `fraction` slides those stops outward from the middle, which is the
658
- // expand; a real width change would relayout the bar group instead.
659
- function expandableDatumPaint(slots: string[], fraction: number): echarts.graphic.LinearGradient {
660
- const base = slots[0] ?? GRAY;
661
- const half = Math.max(0, Math.min(1, fraction)) / 2;
662
- const left = 0.5 - half;
663
- const right = 0.5 + half;
664
- const clear = withAlpha(base, 0);
665
- return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
666
- { offset: 0, color: clear },
667
- { offset: left, color: clear },
668
- { offset: left, color: base },
669
- { offset: right, color: base },
670
- { offset: right, color: clear },
671
- { offset: 1, color: clear },
672
- ]);
673
- }
674
-
675
- // Resolves a bar variant into an ECharts fill for its series. `base` is the
676
- // first color slot; `slots` the full vertical color run.
677
- function barFillPaint(
678
- variant: BarVariant,
679
- slots: string[],
680
- isHorizontal: boolean,
681
- blockSize = BLOCK_SIZE,
682
- ): string | echarts.graphic.LinearGradient | ImagePatternObject {
683
- const base = slots[0] ?? GRAY;
684
- switch (variant) {
685
- case "gradient":
686
- return verticalFadePaint(slots);
687
- case "duotone":
688
- return duotoneSplitPaint(base, 0.4, 1, isHorizontal);
689
- case "duotone-reverse":
690
- return duotoneSplitPaint(base, 1, 0.4, isHorizontal);
691
- case "hatched":
692
- return patternFill("hatched", base) ?? solidVerticalPaint(slots, 1);
693
- case "blocks":
694
- return patternFill("blocks", base, blockSize) ?? solidVerticalPaint(slots, 1);
695
- case "expandable":
696
- // Series-level fallback only; buildBarSeries gives every datum its own
697
- // openness so a single hovered bar can expand on its own.
698
- return expandableDatumPaint(slots, EXPAND_COLLAPSED);
699
- case "stripped":
700
- // Series-level fallback only; buildBarSeries overrides every stripped datum
701
- // with its own fixed-pixel cap fraction.
702
- return strippedDatumPaint(slots, isHorizontal, STRIPPED_FALLBACK_FRACTION);
703
- default:
704
- return solidVerticalPaint(slots, 1);
705
- }
706
- }
707
-
708
- // Border radius per variant/layout. Non-stripped bars round every corner
709
- // (Recharts passes a plain number); stripped rounds only the tip corners — the
710
- // top for vertical bars, the right end for horizontal.
711
- function barBorderRadius(
712
- radius: number,
713
- variant: BarVariant,
714
- isHorizontal: boolean,
715
- ): number | number[] {
716
- // Blocks draw their own square segments; a radius would clip the end one. An
717
- // expandable bar is a thin line at rest, where a radius would swallow it.
718
- if (variant === "blocks" || variant === "expandable") return 0;
719
- if (variant !== "stripped") return radius;
720
- // ECharts corner order: [top-left, top-right, bottom-right, bottom-left].
721
- return isHorizontal ? [0, radius, radius, 0] : [radius, radius, 0, 0];
722
- }
723
-
724
- // ─────────────────────────────────────────────────────────────────────────────
725
- // Selection + entrance helpers
726
- // ─────────────────────────────────────────────────────────────────────────────
727
-
728
- // A bar dims to SELECTION_DIM only when a DIFFERENT series is selected.
729
- function selectionOpacity(selected: string | null, key: string): number {
730
- return selected === null || selected === key ? 1 : SELECTION_DIM;
731
- }
732
-
733
- // How many stagger steps a bar at `index` waits before it grows in — the order
734
- // encoded by `animationType`. Bars are independent rectangles, so unlike the
735
- // area chart's single left-to-right clip, the direction values are honored here
736
- // via a per-datum `animationDelay`.
737
- function barStaggerDelay(type: BarAnimationType, index: number, count: number): number {
738
- if (type === "none" || count <= 0) return 0;
739
- const last = count - 1;
740
- const center = last / 2;
741
- let step: number;
742
- switch (type) {
743
- case "right-to-left":
744
- step = last - index;
745
- break;
746
- case "center-out":
747
- step = Math.abs(index - center);
748
- break;
749
- case "edges-in":
750
- step = center - Math.abs(index - center);
751
- break;
752
- default: // left-to-right
753
- step = index;
754
- }
755
- return step * BAR_STAGGER;
756
- }
757
-
758
- // The brush overlay primitives (BrushRange, BrushGeometry, BrushOverlayElements,
759
- // syncBrushOverlay) and the dataZoom builder (buildBrushDataZoom) now live in
760
- // ./echarts-brush and are imported at the top of this file. The
761
- // tooltip shell/row/styling (roundnessClass, tooltipVariantClass,
762
- // tooltipShell/tooltipRow/tooltipIndicatorHtml), the legend indicators
763
- // (LegendIndicator/LegendOverlay + fill/outline styles), and indicatorBackground
764
- // likewise live in the shared tooltip/legend/chart modules.
765
-
766
- // ─────────────────────────────────────────────────────────────────────────────
767
- // Option builders — pure functions from a snapshot context to ECharts option
768
- // fragments. The component reads its refs ONCE per build into this context;
769
- // nothing below touches React state or the chart instance, so each fragment can
770
- // be reasoned about in isolation.
771
- // ─────────────────────────────────────────────────────────────────────────────
772
-
773
- type OptionBuildContext = {
774
- data: Record<string, unknown>[];
775
- config: ChartConfig;
776
- bars: BarSeriesConfig[];
777
- seriesKeys: string[];
778
- animationType: BarAnimationType;
779
- barRadius: number;
780
- isHorizontal: boolean;
781
- isStacked: boolean;
782
- isPercent: boolean;
783
- selectedDataKey: string | null;
784
- hasSelection: boolean;
785
- showGrid: boolean;
786
- // Category axis is x (vertical layout) or y (horizontal); value axis the other.
787
- categorySlot: AxisSlot;
788
- valueSlot: AxisSlot;
789
- tooltipSlot: TooltipSlot;
790
- legendSlot: LegendSlot;
791
- isLoading: boolean;
792
- loadingData: () => number[];
793
- showBrush: boolean;
794
- brushHeight: number;
795
- barGap?: number;
796
- barCategoryGap?: number;
797
- resolved: ResolvedColors;
798
- categories: string[];
799
- brushRange: BrushRange; // zoom window carried through rebuilds
800
- valuePxPerUnit: number | null; // measured value-axis pixels-per-unit (null pre-layout)
801
- barWidthPx: number | null; // measured bar width — sizes the blocks variant's squares (null pre-layout)
802
- // Openness per bar index for the expandable variant, plus which one the pointer
803
- // is on — driven by the hover rAF, read at build.
804
- expand: { key: string | null; hovered: number | null; progress: Map<number, number> };
805
- maxHighlightIndex: number | null; // column to keep colored under enableMaxValueHighlight
806
- };
807
-
808
- // Grid insets plus the footer band reserved for the brush. ECharts 6 contains
809
- // axis labels automatically (the legacy `containLabel` flag now only triggers a
810
- // deprecation warning).
811
- function buildChartLayout({
812
- legendSlot,
813
- showBrush,
814
- brushHeight,
815
- isHorizontal,
816
- categorySlot,
817
- valueSlot,
818
- }: OptionBuildContext): {
819
- grid: GridComponentOption;
820
- brushBottom: number;
821
- } {
822
- const legendTop = legendSlot.present && legendSlot.verticalAlign === "top";
823
- const legendBottom = legendSlot.present && legendSlot.verticalAlign === "bottom";
824
- // Clearance covers the axis labels plus the same breathing room the Recharts
825
- // twin leaves between them and the brush. A bottom-axis TITLE renders below the
826
- // labels (nameGap), so it needs its own band above the brush frame. The brush
827
- // is vertical-layout only, where the bottom (x-position) axis is the category axis.
828
- const bottomAxisLabel = isHorizontal ? valueSlot.label : categorySlot.label;
829
- const brushGap = showBrush ? brushHeight + 30 + (bottomAxisLabel ? 22 : 0) : 0;
830
-
831
- return {
832
- grid: {
833
- left: 8,
834
- right: 8,
835
- top: legendTop ? 42 : 16,
836
- bottom: 8 + brushGap + (legendBottom ? 34 : 0),
837
- },
838
- brushBottom: legendBottom ? 34 : 6,
839
- };
840
- }
841
-
842
- // The category + value axes, laid onto x/y per layout. Vertical bars → x is
843
- // category, y is value; horizontal bars → x is value, y is category.
844
- function buildMainAxes(ctx: OptionBuildContext): { xAxis: XAxisOption; yAxis: YAxisOption } {
845
- const {
846
- isHorizontal,
847
- showGrid,
848
- isLoading,
849
- isPercent,
850
- categories,
851
- loadingData,
852
- categorySlot,
853
- valueSlot,
854
- } = ctx;
855
- const { tokens } = ctx.resolved;
856
-
857
- const axisLabelColor = tokens.mutedForeground;
858
- const splitLineColor = withAlpha(tokens.border, GRID_LINE_OPACITY);
859
- // Gridline gray as an opaque color — see flattenColor.
860
- const tickDotColor = flattenColor(splitLineColor, tokens.background);
861
- const catData = isLoading ? loadingData().map((_, i) => i) : categories;
862
- const catFormatter = categorySlot.tickFormatter;
863
- const valFormatter = valueSlot.tickFormatter;
864
-
865
- // The axis title (name) follows the axis PART, not its category/value role: the
866
- // category axis wears whichever <XAxis>/<YAxis> child renders it per layout, and
867
- // its label sits at that child's physical position. nameGap is 30 for the bottom
868
- // (x-position) axis and 38 for the side (y-position) one, so it swaps with the
869
- // layout alongside the axisLabel styling.
870
- const categoryNameGap = isHorizontal ? 38 : 30;
871
- const valueNameGap = isHorizontal ? 30 : 38;
872
-
873
- // NOTE: these are left un-annotated so their inferred literal type stays free
874
- // of an axis-specific `position` — the layout swap below assigns the category
875
- // axis to y (and the value axis to x) for horizontal bars, and XAxisOption vs
876
- // YAxisOption disagree on `position`, so a fixed annotation would reject one
877
- // branch. `type` is pinned with `as const` to satisfy the axis-kind union.
878
- const categoryAxis = {
879
- type: "category" as const,
880
- // Bars sit BETWEEN ticks — the opposite of the area chart's boundaryGap:false.
881
- boundaryGap: true,
882
- show: true,
883
- // The Recharts YAxis lists its first category at the TOP; ECharts' y category
884
- // axis defaults to bottom-up, so flip it when the category axis is on y.
885
- inverse: isHorizontal,
886
- data: catData,
887
- // Axis title — same size/color as the tick labels, pushed clear of them. The
888
- // category axis carries the label of whichever <XAxis>/<YAxis> child renders it.
889
- name: isLoading ? undefined : categorySlot.label,
890
- nameLocation: "middle" as const,
891
- nameGap: categoryNameGap,
892
- nameTextStyle: { color: axisLabelColor, fontSize: 10 },
893
- axisLine: { show: false },
894
- // Tick DOTS: a near-zero-length tick whose round caps form a true circle, in
895
- // the gridline gray (flattened opaque so the caps don't stack).
896
- axisTick: {
897
- show: !isLoading && categorySlot.present && !categorySlot.hideDots,
898
- length: 0.5,
899
- // Bars use boundaryGap, so ECharts would drop each tick on the BOUNDARY
900
- // between two categories — a dot floating between labels rather than under
901
- // one. Align them to the labels instead.
902
- alignWithLabel: true,
903
- lineStyle: { color: tickDotColor, width: 3, cap: "round" as const },
904
- },
905
- splitLine: { show: false },
906
- axisLabel: {
907
- show: !isLoading && categorySlot.present,
908
- color: axisLabelColor,
909
- fontSize: 10,
910
- margin: 8,
911
- formatter: catFormatter
912
- ? (value: string, index: number) => catFormatter(value, index)
913
- : undefined,
914
- },
915
- };
916
-
917
- // An ECharts axis with `show: false` hides its splitLines too, but Recharts'
918
- // <CartesianGrid> draws with or without a visible value axis. Keep the axis on
919
- // whenever <Grid/> is present and gate the LABELS on the slot instead.
920
- const valueAxis = {
921
- type: "value" as const,
922
- show: valueSlot.present || showGrid,
923
- max: isPercent ? 1 : undefined,
924
- // Axis title — same styling as the category axis; the value axis carries the
925
- // label of the other of the two <XAxis>/<YAxis> children.
926
- name: isLoading ? undefined : valueSlot.label,
927
- nameLocation: "middle" as const,
928
- nameGap: valueNameGap,
929
- nameTextStyle: { color: axisLabelColor, fontSize: 10 },
930
- axisLine: { show: false },
931
- // Same tick dots as the category axis, beside each value label.
932
- axisTick: {
933
- show: valueSlot.present && !isLoading && !valueSlot.hideDots,
934
- length: 0.5,
935
- // Inert here — ECharts only honors it for CATEGORY ticks, and this axis is
936
- // always type:"value". Carried so both axes' tick config stays identical.
937
- lineStyle: { color: tickDotColor, width: 3, cap: "round" as const },
938
- },
939
- splitLine: {
940
- // Hidden while loading — the skeleton floats on a clean canvas.
941
- show: showGrid && !isLoading,
942
- lineStyle: { color: splitLineColor, type: [3, 3] as [number, number], width: 1 },
943
- },
944
- axisLabel: {
945
- // Hidden while loading — skeleton values are meaningless, and the Recharts
946
- // axes unmount during loading too.
947
- show: valueSlot.present && !isLoading,
948
- color: axisLabelColor,
949
- fontSize: 10,
950
- margin: 8,
951
- formatter: isPercent
952
- ? (value: number) => `${Math.round(value * 100)}%`
953
- : valFormatter
954
- ? (value: number, index: number) => valFormatter(String(value), index)
955
- : undefined,
956
- },
957
- };
958
-
959
- return isHorizontal
960
- ? { xAxis: valueAxis, yAxis: categoryAxis }
961
- : { xAxis: categoryAxis, yAxis: valueAxis };
962
- }
963
-
964
- // Tooltip HTML builder, closed over the build context. Dims by the click
965
- // selection only — the Recharts twin passes `cursor={false}`, so there is no
966
- // axis-pointer line and hover-highlight never touches the tooltip.
967
- function createTooltipFormatter(ctx: OptionBuildContext) {
968
- const { config, selectedDataKey, tooltipSlot } = ctx;
969
-
970
- return (params: unknown): string => {
971
- const rows = Array.isArray(params) ? params : [params];
972
- if (!rows.length) return "";
973
-
974
- const first = rows[0] as { axisValue?: string | number; name?: string };
975
- // Label shows the RAW axis value — matches ChartTooltipContent (no tick formatter).
976
- const axisValue = first.axisValue ?? first.name ?? "";
977
- const label = String(axisValue);
978
-
979
- const body = rows
980
- .map((param) => {
981
- const p = param as {
982
- seriesId?: string;
983
- seriesName?: string;
984
- value?: number | string;
985
- };
986
- // Internal series (the brush's mini chart, the loading skeleton) never
987
- // surface in the tooltip.
988
- if (String(p.seriesId ?? "").startsWith("__")) return "";
989
- const key = p.seriesId ?? p.seriesName ?? "";
990
- const item = config[key];
991
- const colorsCount = item ? getColorsCount(item) : 1;
992
- const labelText = typeof item?.label === "string" ? item.label : (p.seriesName ?? key);
993
- const dimmed = selectedDataKey != null && selectedDataKey !== key ? " opacity-30" : "";
994
- const value =
995
- typeof p.value === "number" ? p.value.toLocaleString() : String(p.value ?? "");
996
-
997
- return tooltipRow({
998
- indicatorHtml: tooltipIndicatorHtml(key, colorsCount),
999
- labelText,
1000
- valueText: value,
1001
- dimmed,
1002
- });
1003
- })
1004
- .join("");
1005
-
1006
- return tooltipShell({
1007
- label,
1008
- body,
1009
- roundness: tooltipSlot.roundness,
1010
- variant: tooltipSlot.variant,
1011
- });
1012
- };
1013
- }
1014
-
1015
- function buildTooltipOption(ctx: OptionBuildContext): TooltipComponentOption {
1016
- const { tooltipSlot, isLoading } = ctx;
1017
- const { tokens } = ctx.resolved;
1018
-
1019
- return {
1020
- ...tooltipBaseOption({
1021
- present: tooltipSlot.present && !isLoading,
1022
- // The twin disables the cursor (`cursor={false}`) — no shadow, no line —
1023
- // so the axisPointer color/width below go unused; only `position` applies.
1024
- cursor: false,
1025
- tokens,
1026
- position: tooltipSlot.position,
1027
- axisPointerColor: tokens.border,
1028
- strokeWidth: STROKE_WIDTH,
1029
- }),
1030
- formatter: createTooltipFormatter(ctx),
1031
- };
1032
- }
1033
-
1034
- // ── Brush — the evil-brush "bar" look, canvas-style: a real mini chart of the
1035
- // full data in a second grid, with a transparent slider dataZoom laid over it.
1036
- // Both zoom entries target only the MAIN x-axis, so the mini chart never filters
1037
- // itself. Only called for the vertical layout, where the category axis is x.
1038
- function buildBrushOption(
1039
- ctx: OptionBuildContext,
1040
- brushBottom: number,
1041
- ): {
1042
- miniGrid: GridComponentOption;
1043
- miniXAxis: XAxisOption;
1044
- miniYAxis: YAxisOption;
1045
- miniSeries: BarSeriesOption[];
1046
- dataZoom: DataZoomComponentOption[];
1047
- } {
1048
- const { data, bars, isStacked, selectedDataKey, hasSelection, brushHeight, categories } = ctx;
1049
- const { tokens } = ctx.resolved;
1050
-
1051
- const miniGrid: GridComponentOption = {
1052
- left: 8,
1053
- right: 8,
1054
- bottom: brushBottom,
1055
- height: brushHeight,
1056
- // No visible axes here — opt out of label containment so the mini chart
1057
- // spans the full brush frame.
1058
- outerBoundsMode: "none",
1059
- };
1060
-
1061
- const miniXAxis: XAxisOption = {
1062
- type: "category",
1063
- gridIndex: 1,
1064
- boundaryGap: true,
1065
- show: false,
1066
- data: categories,
1067
- axisPointer: { show: false },
1068
- };
1069
-
1070
- const miniYAxis: YAxisOption = { type: "value", gridIndex: 1, show: false };
1071
-
1072
- const miniSeries: BarSeriesOption[] = bars.map((bar) => {
1073
- const key = bar.dataKey;
1074
- const base = (ctx.resolved.series[key] ?? [])[0] ?? GRAY;
1075
- // The mini chart mirrors the click selection: unselected series recede.
1076
- const dim = hasSelection && selectedDataKey !== key ? SELECTION_DIM : 1;
1077
-
1078
- return {
1079
- id: `__mini-${key}`,
1080
- type: "bar",
1081
- xAxisIndex: 1,
1082
- yAxisIndex: 1,
1083
- data: data.map((row) => Number(row[key]) || 0),
1084
- stack: isStacked ? "__mini-total" : undefined,
1085
- silent: true,
1086
- barCategoryGap: "20%",
1087
- emphasis: { disabled: true },
1088
- tooltip: { show: false },
1089
- itemStyle: { color: base, opacity: BRUSH_FILL_OPACITY * dim, borderRadius: 1 },
1090
- z: 0,
1091
- animation: false,
1092
- };
1093
- });
1094
-
1095
- const dataZoom = buildBrushDataZoom({
1096
- brushBottom,
1097
- brushHeight,
1098
- brushRange: ctx.brushRange,
1099
- fillerColor: withAlpha(tokens.foreground, BRUSH_FILLER_OPACITY),
1100
- });
1101
-
1102
- return { miniGrid, miniXAxis, miniYAxis, miniSeries, dataZoom };
1103
- }
1104
-
1105
- // Loading skeleton — ONE gray row of bars regardless of declared series (Recharts
1106
- // parity: its skeleton is a single LoadingBar), swept by the shimmer rAF.
1107
- function buildLoadingOption(
1108
- ctx: OptionBuildContext,
1109
- frame: { grid: GridComponentOption; xAxis: XAxisOption; yAxis: YAxisOption },
1110
- ): EChartsOption {
1111
- const { tokens } = ctx.resolved;
1112
-
1113
- return {
1114
- animation: false,
1115
- grid: frame.grid,
1116
- xAxis: frame.xAxis,
1117
- yAxis: frame.yAxis,
1118
- tooltip: { show: false },
1119
- series: [
1120
- {
1121
- id: "__loading",
1122
- type: "bar",
1123
- data: ctx.loadingData(),
1124
- barCategoryGap: "30%",
1125
- silent: true,
1126
- // Invisible until the first shimmer tick positions the clip window.
1127
- itemStyle: {
1128
- color: withAlpha(tokens.foreground, 0),
1129
- borderRadius: barBorderRadius(DEFAULT_BAR_RADIUS, "default", ctx.isHorizontal),
1130
- },
1131
- z: 1,
1132
- },
1133
- ],
1134
- };
1135
- }
1136
-
1137
- function buildBarSeries(ctx: OptionBuildContext): BarSeriesOption[] {
1138
- const {
1139
- data,
1140
- config,
1141
- bars,
1142
- seriesKeys,
1143
- animationType,
1144
- isHorizontal,
1145
- isStacked,
1146
- isPercent,
1147
- selectedDataKey,
1148
- hasSelection,
1149
- barGap,
1150
- barCategoryGap,
1151
- resolved,
1152
- } = ctx;
1153
-
1154
- const lastIndex = data.length - 1;
1155
-
1156
- // Optional per-row normalization for the percent (100%) stack.
1157
- const rowTotals = isPercent
1158
- ? data.map((row) => seriesKeys.reduce((sum, key) => sum + (Number(row[key]) || 0), 0))
1159
- : [];
1160
-
1161
- const series: BarSeriesOption[] = bars.map((bar) => {
1162
- const key = bar.dataKey;
1163
- const slots = resolved.series[key] ?? [GRAY];
1164
- const base = slots[0] ?? GRAY;
1165
- const isSelected = selectedDataKey === key;
1166
- const dim = selectionOpacity(selectedDataKey, key);
1167
- const resolvedRadius = bar.radius ?? ctx.barRadius;
1168
- const borderRadius = barBorderRadius(resolvedRadius, bar.variant, isHorizontal);
1169
- // Square segments: the tile's height matches the measured bar width, so each
1170
- // block is 1:1. Falls back to BLOCK_SIZE on the first push, before layout.
1171
- const blockSize = ctx.barWidthPx ?? BLOCK_SIZE;
1172
- const fill = barFillPaint(bar.variant, slots, isHorizontal, blockSize);
1173
- const barAnim = bar.animationType ?? animationType;
1174
- const isStripped = bar.variant === "stripped";
1175
- const isExpandable = bar.variant === "expandable";
1176
- // Under enableMaxValueHighlight every column except the tallest is muted, so a
1177
- // single flat tone replaces whatever fill the variant would have painted.
1178
- const mutedFill = withAlpha(resolved.tokens.mutedForeground, MAX_HIGHLIGHT_DIM);
1179
- const isMuted = (i: number) => ctx.maxHighlightIndex != null && i !== ctx.maxHighlightIndex;
1180
-
1181
- // Openness per datum, driven by the hover rAF. Bars not in the map are shut.
1182
- const expandOf = (i: number) =>
1183
- ctx.expand.key === key ? (ctx.expand.progress.get(i) ?? EXPAND_COLLAPSED) : EXPAND_COLLAPSED;
1184
- const expandHovered = ctx.expand.key === key ? ctx.expand.hovered : null;
1185
- // The unfilled part of a blocks bar: the same tile in a muted tone, drawn by
1186
- // ECharts' own bar background so it spans the column's full height.
1187
- const isBlocks = bar.variant === "blocks";
1188
- const blockTrack = isBlocks
1189
- ? patternFill(
1190
- "blocks",
1191
- withAlpha(resolved.tokens.mutedForeground, BLOCK_TRACK_OPACITY),
1192
- blockSize,
1193
- )
1194
- : null;
1195
-
1196
- const values = data.map((row, i) => {
1197
- const value = Number(row[key]) || 0;
1198
- if (!isPercent) return value;
1199
- const total = rowTotals[i];
1200
- return total ? value / total : 0;
1201
- });
1202
-
1203
- // Buffer bar: the last datum becomes a bare hatched rectangle with a
1204
- // series-colored outline, marking projected/incomplete data.
1205
- const bufferStyle = bar.bufferBar
1206
- ? {
1207
- color: patternFill("buffer", base) ?? "transparent",
1208
- borderColor: base,
1209
- borderWidth: STROKE_WIDTH,
1210
- borderRadius,
1211
- }
1212
- : null;
1213
-
1214
- // Per-bar glow shadow — the shadowColor is sampled from the series gradient
1215
- // at each bar's horizontal position, so a multi-stop series glows in its own
1216
- // colors across the plot instead of one flat tint (a single shadowColor was
1217
- // the bug). A canvas shape carries only one shadow, so the sample is per bar,
1218
- // not within a bar; the wide, soft shadowBlur reads as the Recharts blur's
1219
- // colored halo with no hard rim.
1220
- // `glowing` haloes every bar in the series; enableMaxValueHighlight haloes only
1221
- // the winning column — the muted ones must stay flat or the "one bar stands
1222
- // out" reading collapses. Same shadow either way, so the two share a builder.
1223
- const glowAt = (i: number) => ({
1224
- shadowBlur: GLOW_BLUR,
1225
- shadowColor: withAlpha(
1226
- sampleGradient(slots, values.length > 1 ? i / (values.length - 1) : 0),
1227
- GLOW_OPACITY,
1228
- ),
1229
- });
1230
- const glowFor = bar.glowing
1231
- ? glowAt
1232
- : ctx.maxHighlightIndex != null
1233
- ? (i: number) => (i === ctx.maxHighlightIndex ? glowAt(i) : {})
1234
- : null;
1235
-
1236
- // Only wrap a datum in an object when it needs per-point overrides (stripped
1237
- // cap, buffer tip, or glow); otherwise keep the bare number so the series
1238
- // itemStyle applies untouched. Stripped and glow touch every datum; buffer only
1239
- // the last one.
1240
- const dataPoints =
1241
- isStripped ||
1242
- isExpandable ||
1243
- glowFor ||
1244
- ctx.maxHighlightIndex != null ||
1245
- (bufferStyle && lastIndex >= 0)
1246
- ? values.map((value, i) => {
1247
- const isBuffer = !!bufferStyle && i === lastIndex;
1248
- if (!isBuffer && !glowFor && !isStripped && !isExpandable && !isMuted(i)) return value;
1249
- return {
1250
- value,
1251
- ...(isExpandable ? { label: { show: i === expandHovered } } : {}),
1252
- itemStyle: {
1253
- // The stripped cap is per datum: its fixed pixel height becomes a
1254
- // fraction of THIS bar's own height, so the cap is a constant pixel
1255
- // height across bars. The buffer tip (bare hatched) still wins on
1256
- // the last datum.
1257
- ...(isStripped && !isBuffer
1258
- ? {
1259
- color: strippedDatumPaint(
1260
- slots,
1261
- isHorizontal,
1262
- strippedCapFraction(value, ctx.valuePxPerUnit),
1263
- ),
1264
- }
1265
- : {}),
1266
- ...(isExpandable && !isBuffer
1267
- ? { color: expandableDatumPaint(slots, expandOf(i)) }
1268
- : {}),
1269
- ...(isBuffer && bufferStyle ? bufferStyle : {}),
1270
- ...(glowFor ? glowFor(i) : {}),
1271
- // Last so it overrides the variant's own paint.
1272
- ...(isMuted(i) ? { color: mutedFill } : {}),
1273
- },
1274
- };
1275
- })
1276
- : values;
1277
-
1278
- return {
1279
- id: key,
1280
- name: typeof config[key]?.label === "string" ? config[key]?.label : key,
1281
- type: "bar",
1282
- data: dataPoints,
1283
- stack: isStacked ? "total" : undefined,
1284
- barGap,
1285
- barCategoryGap,
1286
- cursor: bar.isClickable ? "pointer" : "default",
1287
- // Selected series ride on top; when a selection is active the rest sink below.
1288
- z: isSelected ? 3 : hasSelection ? 1 : 2,
1289
- // The hovered bar names its value above itself (Recharts twin parity).
1290
- label: isExpandable
1291
- ? {
1292
- show: false,
1293
- position: "top",
1294
- color: resolved.tokens.foreground,
1295
- fontFamily: "var(--font-mono, monospace)",
1296
- fontSize: 11,
1297
- }
1298
- : undefined,
1299
- showBackground: isBlocks,
1300
- backgroundStyle: blockTrack ? { color: blockTrack, borderRadius } : undefined,
1301
- itemStyle: {
1302
- color: fill,
1303
- borderRadius,
1304
- opacity: dim,
1305
- // The glow lives on each datum's itemStyle (per-bar sampled shadowColor),
1306
- // not here — a single series-level shadowColor can't follow a gradient.
1307
- },
1308
- // Hover-highlight uses ECharts-native focus/blur: `self` keeps only the
1309
- // hovered bar lit and dims every other, matching the twin's per-bar dim.
1310
- // A click-selection OWNS the dim while it is active, so hover highlighting
1311
- // switches off entirely whenever a selection exists (this option rebuilds on
1312
- // every selection change, and the notMerge push clears any live blur) and
1313
- // resumes once the selection clears. Otherwise emphasis is disabled so
1314
- // hovering leaves the bar untouched.
1315
- emphasis:
1316
- bar.enableHoverHighlight && !hasSelection
1317
- ? { focus: "self" as const, blurScope: "coordinateSystem" as const }
1318
- : { disabled: true },
1319
- blur:
1320
- bar.enableHoverHighlight && !hasSelection
1321
- ? { itemStyle: { opacity: HOVER_BLUR } }
1322
- : undefined,
1323
- // The grow-in envelope. Only takes effect on the reveal push (top-level
1324
- // `animation: true`); every later push sends `animation: false`, so the
1325
- // per-datum stagger is dormant then.
1326
- animationDuration: BAR_GROW_DURATION,
1327
- animationEasing: "cubicOut",
1328
- animationDelay: (idx: number) => barStaggerDelay(barAnim, idx, data.length),
1329
- };
1330
- });
1331
-
1332
- // Stacked segments butt together into one solid column, so part them with a REAL
1333
- // gap: a transparent series stacked between each adjacent pair. A background
1334
- // -colored border can't do this — a border paints all four sides, outlining every
1335
- // segment (glaring the moment a bar glows) instead of only separating them.
1336
- //
1337
- // The spacer's value is in DATA units, so it is derived from the measured
1338
- // pixels-per-unit to keep the gap a constant pixel height whatever the scale.
1339
- // Before the first layout that measurement is null and the gap is simply skipped;
1340
- // the push re-applies once it exists, in the same frame (see the sync effect).
1341
- const gapUnits =
1342
- (isStacked || isPercent) && series.length > 1 && ctx.valuePxPerUnit
1343
- ? STACK_SEGMENT_GAP / ctx.valuePxPerUnit
1344
- : 0;
1345
- if (!gapUnits) return series;
1346
-
1347
- const spaced: BarSeriesOption[] = [];
1348
- series.forEach((entry, i) => {
1349
- spaced.push(entry);
1350
- if (i === series.length - 1) return;
1351
- spaced.push({
1352
- id: `__stackgap-${i}`,
1353
- type: "bar",
1354
- stack: isStacked ? "total" : undefined,
1355
- data: data.map(() => gapUnits),
1356
- itemStyle: { color: "transparent" },
1357
- silent: true,
1358
- tooltip: { show: false },
1359
- legendHoverLink: false,
1360
- emphasis: { disabled: true },
1361
- animation: false,
1362
- z: 1,
1363
- });
1364
- });
1365
- return spaced;
1366
- }
1367
-
1368
- // ─────────────────────────────────────────────────────────────────────────────
1369
- // Live imperative state — everything the ECharts event handlers, rAF loops, and
1370
- // theme/resize repushes read or write OUTSIDE the React render cycle, grouped in
1371
- // one ref-stable object. None of it is render output, which is exactly why it is
1372
- // not React state.
1373
- // ─────────────────────────────────────────────────────────────────────────────
1374
-
1375
- type LiveState = {
1376
- resolved: ResolvedColors | null; // colors read off the live DOM — feeds builds and rAF loops
1377
- hasRevealed: boolean; // the intro grow-in already played on this chart instance
1378
- revealEndsAt: number; // performance.now() the entrance settles — gates the stripped-cap correction
1379
- valuePxPerUnit: number | null; // measured value-axis pixels-per-unit — sizes the stripped cap
1380
- barWidthPx: number | null; // measured bar width — sizes the blocks variant's squares
1381
- // Openness per bar index for the expandable variant, plus which one the pointer
1382
- // is on. Per-index so a bar being left keeps easing shut while the next one
1383
- // opens — a single shared value made the outgoing bar snap.
1384
- expand: { key: string | null; hovered: number | null; progress: Map<number, number> };
1385
- expandRaf: number; // in-flight expand animation frame
1386
- animateExpand: (key: string | null, index: number | null) => void;
1387
- loadingRows: number[] | null; // skeleton data, lazily rolled and re-rolled per shimmer sweep
1388
- categories: string[]; // x labels of the last build, for the brush label pills
1389
- dataLength: number; // row count, for the datazoom index math
1390
- brushRange: BrushRange; // live zoom window — carried through every rebuild
1391
- brushGeom: BrushGeometry | null; // brush footer layout of the last build
1392
- brushOverlay: BrushOverlayElements | null; // zrender elements, owned by syncBrushOverlay
1393
- brushHover: { inside: boolean; left: boolean; right: boolean };
1394
- // Latest callbacks/flags for the imperative ECharts event handlers.
1395
- handlers: {
1396
- onBrushChange?: (range: { startIndex: number; endIndex: number }) => void;
1397
- clickableKeys: Set<string>;
1398
- brushFormatLabel?: (value: string, index: number) => string;
1399
- seriesKeys: string[];
1400
- hasStripped: boolean; // any visible stripped bar → run the post-layout cap correction
1401
- hasBlocks: boolean; // any blocks bar → re-push once the bar width is measurable
1402
- hasStackGap: boolean; // stacked with >1 series → the segment gap needs the axis scale
1403
- expandableKey: string | null; // the expandable series, if any — drives the column hover
1404
- barCategoryGap?: number; // consumer's category gap, needed to derive the bar width
1405
- isHorizontal: boolean; // layout, for measuring the value axis in the finished handler
1406
- };
1407
- // Update-style re-push for paths that bypass React entirely (theme flips,
1408
- // resizes) — set by the sync effect.
1409
- repush: () => void;
1410
- // Rebuilds ONLY the stripped bar series (fresh per-datum cap fractions) and
1411
- // merges them with a silent lazyUpdate — never notMerge, so it leaves the
1412
- // dataZoom drag anchor and the running entrance untouched. Set by the sync effect.
1413
- patchStrippedCaps: () => void;
1414
- };
1415
-
1416
- // ─────────────────────────────────────────────────────────────────────────────
1417
- // Component
1418
- // ─────────────────────────────────────────────────────────────────────────────
1419
-
1420
- /**
1421
- * Apache ECharts port of the EvilCharts bar chart, exposing a compound-as-config
1422
- * API so its JSX reads identically to the Recharts twin. The root owns the data,
1423
- * config, selection state, loading skeleton, intro grow-in, and optional zoom
1424
- * brush; every visual part — `<Bar>`, `<XAxis>`, `<YAxis>`, `<Grid>`,
1425
- * `<Tooltip>`, `<Legend>` — is composed as a declarative child that renders
1426
- * nothing. The root walks those children by reference and drives a single
1427
- * imperative ECharts instance. Fully self-contained: its only dependencies are
1428
- * `react`, `echarts`, and `motion`.
1429
- */
1430
- export function EChartsBarChart<TData extends Record<string, unknown>>({
1431
- data,
1432
- config,
1433
- renderer = DEFAULT_ECHARTS_RENDERER,
1434
- xDataKey,
1435
- className,
1436
- stackType = "default",
1437
- layout = "vertical",
1438
- barRadius = DEFAULT_BAR_RADIUS,
1439
- animation = true,
1440
- animationType = "left-to-right",
1441
- barGap,
1442
- barCategoryGap,
1443
- selectedDataKey: selectedDataKeyProp,
1444
- defaultSelectedDataKey = null,
1445
- onSelectionChange,
1446
- enableMaxValueHighlight = false,
1447
- isLoading = false,
1448
- loadingBars = LOADING_DEFAULT_BARS,
1449
- chartOptions,
1450
- children,
1451
- }: EChartsBarChartProps<TData>) {
1452
- const rawId = useId();
1453
- const chartId = `chart-${rawId.replace(/:/g, "")}`;
1454
-
1455
- const containerRef = useRef<HTMLDivElement>(null);
1456
- const mountRef = useRef<HTMLDivElement>(null);
1457
- const echartsRef = useRef<EChartsInstance | null>(null);
1458
-
1459
- // The single imperative surface (see LiveState). `resolved` lives here rather
1460
- // than in state: as state it forced an extra render pass and an effect whose
1461
- // only job was to trigger the option push. The object identity is stable for
1462
- // the component's lifetime.
1463
- const live = useRef<LiveState>({
1464
- resolved: null,
1465
- hasRevealed: false,
1466
- revealEndsAt: 0,
1467
- valuePxPerUnit: null,
1468
- barWidthPx: null,
1469
- expand: { key: null, hovered: null, progress: new Map<number, number>() },
1470
- expandRaf: 0,
1471
- animateExpand: () => {},
1472
- loadingRows: null,
1473
- categories: [],
1474
- dataLength: 0,
1475
- brushRange: { start: 0, end: 100 },
1476
- brushGeom: null,
1477
- brushOverlay: null,
1478
- brushHover: { inside: false, left: false, right: false },
1479
- handlers: {
1480
- onBrushChange: undefined, // set per-render from the <Brush> child's onChange
1481
- clickableKeys: new Set<string>(),
1482
- brushFormatLabel: undefined, // set per-render from the <Brush> child's formatLabel
1483
- seriesKeys: [],
1484
- hasStripped: false,
1485
- hasBlocks: false,
1486
- hasStackGap: false,
1487
- expandableKey: null,
1488
- isHorizontal: false,
1489
- },
1490
- repush: () => {},
1491
- patchStrippedCaps: () => {},
1492
- }).current;
1493
-
1494
- // Skeleton rows roll lazily on first use — an impure useRef initializer would
1495
- // re-roll Math.random() on every render.
1496
- const loadingData = useCallback(
1497
- () => (live.loadingRows ??= getLoadingBarData(loadingBars)),
1498
- [live, loadingBars],
1499
- );
1500
- const shouldReduceMotion = useReducedMotion();
1501
-
1502
- const [internalSelectedKey, setSelectedDataKey] = useState<string | null>(defaultSelectedDataKey);
1503
- const selectedDataKey = selectedDataKeyProp !== undefined ? selectedDataKeyProp : internalSelectedKey;
1504
-
1505
- // ── Declarative config, collected from children by reference ─────────────────
1506
- const collected = useMemo(() => collectConfig(children), [children]);
1507
- const {
1508
- bars,
1509
- xAxis: xAxisSlot,
1510
- yAxis: yAxisSlot,
1511
- showGrid,
1512
- tooltip: tooltipSlot,
1513
- legend: legendSlot,
1514
- brush: brushSlot,
1515
- } = collected;
1516
- // Brush is a <Brush> child now (not props): presence turns it on, its props
1517
- // carry height/formatLabel/onChange.
1518
- const showBrush = brushSlot.present;
1519
- const brushHeight = brushSlot.height ?? 56;
1520
-
1521
- const isHorizontal = layout === "horizontal";
1522
- const isPercent = stackType === "percent";
1523
- const isStacked = stackType === "stacked" || isPercent;
1524
-
1525
- // Category axis is x when vertical, y when horizontal; value axis the other.
1526
- const categorySlot = isHorizontal ? yAxisSlot : xAxisSlot;
1527
- const valueSlot = isHorizontal ? xAxisSlot : yAxisSlot;
1528
-
1529
- const seriesKeys = useMemo(() => bars.map((bar) => bar.dataKey), [bars]);
1530
-
1531
- // category key: category axis dataKey → root xDataKey → first data column no <Bar> claims.
1532
- const categoryKey = useMemo(() => {
1533
- if (categorySlot.dataKey) return categorySlot.dataKey;
1534
- if (xDataKey) return xDataKey as string;
1535
- const firstRow = data[0];
1536
- if (firstRow) {
1537
- const claimed = new Set(seriesKeys);
1538
- const found = Object.keys(firstRow).find((key) => !claimed.has(key));
1539
- if (found) return found;
1540
- }
1541
- return "";
1542
- }, [categorySlot.dataKey, xDataKey, data, seriesKeys]);
1543
-
1544
- // The intro grow-in follows the first bar's setting, falling back to the root default.
1545
- const effectiveAnimation = bars[0]?.animationType ?? animationType;
1546
-
1547
- // The tallest COLUMN, comparing totals across every series so a stack or group
1548
- // wins together rather than one bar inside it. Null when the flag is off.
1549
- const maxHighlightIndex = useMemo(() => {
1550
- if (!enableMaxValueHighlight || !data.length || !seriesKeys.length) return null;
1551
- let best = 0;
1552
- let bestTotal = -Infinity;
1553
- data.forEach((row, i) => {
1554
- const total = seriesKeys.reduce((sum, key) => sum + (Number(row[key]) || 0), 0);
1555
- if (total > bestTotal) {
1556
- bestTotal = total;
1557
- best = i;
1558
- }
1559
- });
1560
- return best;
1561
- }, [enableMaxValueHighlight, data, seriesKeys]);
1562
-
1563
- const css = useMemo(() => buildChartCss(chartId, config), [chartId, config]);
1564
-
1565
- const hasSelection = selectedDataKey !== null;
1566
-
1567
- // Which series may be clicked to toggle selection (consulted by the click handler).
1568
- const clickableKeys = useMemo(
1569
- () => new Set(bars.filter((bar) => bar.isClickable).map((bar) => bar.dataKey)),
1570
- [bars],
1571
- );
1572
-
1573
- // Any visible stripped bar? (never while loading — the skeleton has no stripped
1574
- // series.) Gates the post-layout cap correction in the `finished` handler.
1575
- const hasStrippedBars = !isLoading && bars.some((bar) => bar.variant === "stripped");
1576
-
1577
- // Refresh the handlers' snapshot of the latest callbacks/flags every render.
1578
- live.handlers = {
1579
- onBrushChange: brushSlot.onChange,
1580
- clickableKeys,
1581
- brushFormatLabel: brushSlot.formatLabel,
1582
- seriesKeys,
1583
- hasStripped: hasStrippedBars,
1584
- hasBlocks: bars.some((bar) => bar.variant === "blocks"),
1585
- hasStackGap: (stackType === "stacked" || stackType === "percent") && bars.length > 1,
1586
- expandableKey: bars.find((bar) => bar.variant === "expandable")?.dataKey ?? null,
1587
- barCategoryGap,
1588
- isHorizontal,
1589
- };
1590
- live.dataLength = data.length;
1591
-
1592
- const toggleSelection = useCallback(
1593
- (key: string) => {
1594
- const next = selectedDataKey === key ? null : key;
1595
- if (selectedDataKeyProp === undefined) setSelectedDataKey(next);
1596
- onSelectionChange?.(next);
1597
- },
1598
- [onSelectionChange, selectedDataKey, selectedDataKeyProp],
1599
- );
1600
-
1601
- // The brush is meaningful only when the category axis is on x (vertical layout).
1602
- const brushEnabled = showBrush && !isHorizontal;
1603
-
1604
- // Reposition the brush overlays from the live refs — safe to call from drag
1605
- // events, hover tracking, and pushes alike, since it never touches setOption.
1606
- const syncBrushOverlayNow = useCallback(() => {
1607
- const chart = echartsRef.current;
1608
- if (!chart) return;
1609
-
1610
- const geom = live.brushGeom;
1611
- const tokens = live.resolved?.tokens;
1612
- if (!geom || !tokens) {
1613
- syncBrushOverlay(chart, live, null);
1614
- return;
1615
- }
1616
-
1617
- const range = live.brushRange;
1618
- const categories = live.categories;
1619
- const format = live.handlers.brushFormatLabel;
1620
- const lastIndex = Math.max(categories.length - 1, 0);
1621
- const startIndex = Math.round((range.start / 100) * lastIndex);
1622
- const endIndex = Math.round((range.end / 100) * lastIndex);
1623
- const labels =
1624
- format && categories.length
1625
- ? {
1626
- start: format(categories[startIndex] ?? "", startIndex),
1627
- end: format(categories[endIndex] ?? "", endIndex),
1628
- }
1629
- : null;
1630
-
1631
- syncBrushOverlay(chart, live, {
1632
- range,
1633
- geom,
1634
- size: { width: chart.getWidth(), height: chart.getHeight() },
1635
- tokens,
1636
- labels,
1637
- showLabels: live.brushHover.inside,
1638
- hover: live.brushHover,
1639
- });
1640
- }, [live]);
1641
-
1642
- // ── Option builder ─────────────────────────────────────────────────────────
1643
- const buildOption = useCallback((): EChartsOption => {
1644
- const resolved = live.resolved;
1645
- if (!resolved) return {};
1646
-
1647
- const categories = data.map((row) => String(row[categoryKey]));
1648
- live.categories = categories;
1649
-
1650
- const ctx: OptionBuildContext = {
1651
- data,
1652
- config,
1653
- bars,
1654
- seriesKeys,
1655
- animationType,
1656
- barRadius,
1657
- isHorizontal,
1658
- isStacked,
1659
- isPercent,
1660
- selectedDataKey,
1661
- hasSelection,
1662
- showGrid,
1663
- categorySlot,
1664
- valueSlot,
1665
- tooltipSlot,
1666
- legendSlot,
1667
- isLoading,
1668
- loadingData,
1669
- showBrush: brushEnabled,
1670
- brushHeight,
1671
- barGap,
1672
- barCategoryGap,
1673
- resolved,
1674
- categories,
1675
- brushRange: live.brushRange,
1676
- valuePxPerUnit: live.valuePxPerUnit,
1677
- barWidthPx: live.barWidthPx,
1678
- expand: live.expand,
1679
- maxHighlightIndex,
1680
- };
1681
-
1682
- const { grid, brushBottom } = buildChartLayout(ctx);
1683
- live.brushGeom = brushEnabled ? { bottom: brushBottom, height: brushHeight } : null;
1684
-
1685
- const { xAxis, yAxis } = buildMainAxes(ctx);
1686
-
1687
- if (isLoading) return buildLoadingOption(ctx, { grid, xAxis, yAxis });
1688
-
1689
- const brush = brushEnabled ? buildBrushOption(ctx, brushBottom) : null;
1690
-
1691
- return {
1692
- animation: false,
1693
- grid: brush ? [grid, brush.miniGrid] : grid,
1694
- xAxis: brush ? [xAxis, brush.miniXAxis] : xAxis,
1695
- yAxis: brush ? [yAxis, brush.miniYAxis] : yAxis,
1696
- tooltip: buildTooltipOption(ctx),
1697
- dataZoom: brush?.dataZoom,
1698
- series: [...buildBarSeries(ctx), ...(brush?.miniSeries ?? [])],
1699
- };
1700
- }, [
1701
- live,
1702
- data,
1703
- config,
1704
- bars,
1705
- seriesKeys,
1706
- categoryKey,
1707
- animationType,
1708
- barRadius,
1709
- isHorizontal,
1710
- isStacked,
1711
- isPercent,
1712
- selectedDataKey,
1713
- hasSelection,
1714
- showGrid,
1715
- categorySlot,
1716
- valueSlot,
1717
- tooltipSlot,
1718
- legendSlot,
1719
- isLoading,
1720
- loadingData,
1721
- brushEnabled,
1722
- brushHeight,
1723
- barGap,
1724
- barCategoryGap,
1725
- maxHighlightIndex,
1726
- ]);
1727
-
1728
- // ── Init + resize + theme observer (per renderer instance) ──────────────────
1729
- useEffect(() => {
1730
- const mount = mountRef.current;
1731
- const container = containerRef.current;
1732
- if (!mount || !container) return;
1733
-
1734
- const chart = echarts.init(mount, null, { renderer });
1735
- echartsRef.current = chart;
1736
-
1737
- const resizeObserver = new ResizeObserver(() => {
1738
- // Observers always fire once right after observe(). Repushing on that
1739
- // no-op fire would land one frame into the intro and stomp the grow-in —
1740
- // only react when the renderer size actually changed.
1741
- if (mount.clientWidth === chart.getWidth() && mount.clientHeight === chart.getHeight()) {
1742
- return;
1743
- }
1744
- chart.resize();
1745
- live.repush();
1746
- });
1747
- resizeObserver.observe(mount);
1748
-
1749
- // Light/dark flips change no React state — re-resolve and push directly.
1750
- const themeObserver = new MutationObserver(() => {
1751
- live.repush();
1752
- });
1753
- themeObserver.observe(document.documentElement, {
1754
- attributes: true,
1755
- attributeFilter: ["class"],
1756
- });
1757
-
1758
- // Expandable hover, driven by the pointer's COLUMN rather than the bar element.
1759
- // An expandable bar is a hairline at rest, so element hover would only catch a
1760
- // couple of pixels — and hovering the empty space above a bar (where the axis
1761
- // tooltip still responds) would highlight it without expanding it. Converting
1762
- // the pointer's x back to a category index makes the whole column the target,
1763
- // matching what the tooltip already does. Registered ONCE here rather than in
1764
- // the sync effect, which re-runs on every prop/theme change and would stack
1765
- // duplicate listeners; it calls through live.animateExpand, always the current one.
1766
- chart.getZr().on("mousemove", (event: { offsetX: number; offsetY: number }) => {
1767
- const { expandableKey } = live.handlers;
1768
- if (!expandableKey) return;
1769
- const point = [event.offsetX, event.offsetY];
1770
- if (!chart.containPixel({ gridIndex: 0 }, point)) {
1771
- live.animateExpand(expandableKey, null);
1772
- return;
1773
- }
1774
- // A grid finder returns [xValue, yValue]; on a category axis the x value IS
1775
- // the index. An xAxisIndex finder returns null for a 2D point.
1776
- const converted = chart.convertFromPixel({ gridIndex: 0 }, point);
1777
- const index = Array.isArray(converted) ? converted[0] : converted;
1778
- live.animateExpand(expandableKey, typeof index === "number" ? Math.round(index) : null);
1779
- });
1780
- chart.getZr().on("globalout", () => {
1781
- const { expandableKey } = live.handlers;
1782
- if (expandableKey) live.animateExpand(expandableKey, null);
1783
- });
1784
-
1785
- chart.on("click", (params) => {
1786
- const { clickableKeys: clickable, seriesKeys: keys } = live.handlers;
1787
- const p = params as { seriesId?: string; seriesIndex?: number };
1788
- // Bar clicks carry seriesId; keep the seriesIndex fallback for safety. Main
1789
- // series come first in the series array, so the index maps directly.
1790
- const id =
1791
- p.seriesId ?? (typeof p.seriesIndex === "number" ? keys[p.seriesIndex] : undefined);
1792
- if (typeof id === "string" && clickable.has(id)) toggleSelection(id);
1793
- });
1794
-
1795
- chart.on("datazoom", () => {
1796
- const option = chart.getOption() as { dataZoom?: { start?: number; end?: number }[] };
1797
- const zoom = option.dataZoom?.[0];
1798
- if (!zoom) return;
1799
-
1800
- // Ride the selection — pure zrender updates, so the drag stays 1:1.
1801
- live.brushRange = { start: zoom.start ?? 0, end: zoom.end ?? 100 };
1802
- syncBrushOverlayNow();
1803
-
1804
- const { onBrushChange: onChange } = live.handlers;
1805
- if (!onChange) return;
1806
- const len = live.dataLength;
1807
- const startIndex = Math.round(((zoom.start ?? 0) / 100) * (len - 1));
1808
- const endIndex = Math.round(((zoom.end ?? 100) / 100) * (len - 1));
1809
- onChange({ startIndex, endIndex });
1810
- });
1811
-
1812
- // Every push measures the axis scale and corrects stripped caps before it
1813
- // paints, so this only catches rescales that BYPASS push — a dataZoom drag
1814
- // narrowing the window until the value axis re-ranges. The correction is a
1815
- // SILENT series-only merge (patchStrippedCaps), so it can't reset a dataZoom
1816
- // drag. Held off until the entrance finishes (revealEndsAt) so it never lands
1817
- // mid-grow; guarded by an epsilon so a stable measurement doesn't loop.
1818
- chart.on("finished", () => {
1819
- const { hasStripped, isHorizontal: horiz } = live.handlers;
1820
- if (!hasStripped || performance.now() < live.revealEndsAt) return;
1821
- const measured = measureValuePxPerUnit(chart, horiz);
1822
- if (measured == null) return;
1823
- if (live.valuePxPerUnit != null && Math.abs(measured - live.valuePxPerUnit) < 0.5) return;
1824
- live.valuePxPerUnit = measured;
1825
- live.patchStrippedCaps();
1826
- });
1827
-
1828
- // Hover tracking for the overlay: labels show while the pointer is over the
1829
- // brush, and each pill brightens when the pointer is near its edge.
1830
- const zr = chart.getZr();
1831
- const applyHover = (next: { inside: boolean; left: boolean; right: boolean }) => {
1832
- const prev = live.brushHover;
1833
- if (prev.inside === next.inside && prev.left === next.left && prev.right === next.right) {
1834
- return;
1835
- }
1836
- live.brushHover = next;
1837
- syncBrushOverlayNow();
1838
- };
1839
- const onZrMove = (event: { offsetX?: number; offsetY?: number }) => {
1840
- const geom = live.brushGeom;
1841
- if (!geom) return;
1842
- const x = event.offsetX ?? -1;
1843
- const y = event.offsetY ?? -1;
1844
- const top = chart.getHeight() - geom.bottom - geom.height;
1845
- const inside = y >= top - 4 && y <= top + geom.height + 4;
1846
- const trackLeft = 8;
1847
- const trackWidth = Math.max(chart.getWidth() - 16, 1);
1848
- const { start, end } = live.brushRange;
1849
- const selectionLeft = trackLeft + (trackWidth * start) / 100;
1850
- const selectionRight = trackLeft + (trackWidth * end) / 100;
1851
- applyHover({
1852
- inside,
1853
- left: inside && Math.abs(x - selectionLeft) <= 8,
1854
- right: inside && Math.abs(x - selectionRight) <= 8,
1855
- });
1856
- };
1857
- const onZrOut = () => applyHover({ inside: false, left: false, right: false });
1858
- zr.on("mousemove", onZrMove);
1859
- zr.on("globalout", onZrOut);
1860
-
1861
- return () => {
1862
- zr.off("mousemove", onZrMove);
1863
- zr.off("globalout", onZrOut);
1864
- resizeObserver.disconnect();
1865
- themeObserver.disconnect();
1866
- if (live.expandRaf) {
1867
- cancelAnimationFrame(live.expandRaf);
1868
- live.expandRaf = 0;
1869
- }
1870
- chart.dispose();
1871
- echartsRef.current = null;
1872
- // The overlay elements died with the zrender instance.
1873
- live.brushOverlay = null;
1874
- live.brushHover = { inside: false, left: false, right: false };
1875
- // Expand progress and layout measurements belong to the disposed painter.
1876
- // Recompute them for the replacement renderer instead of briefly painting
1877
- // its first frame with geometry captured from the old surface.
1878
- live.expand = { key: null, hovered: null, progress: new Map<number, number>() };
1879
- live.animateExpand = () => {};
1880
- live.valuePxPerUnit = null;
1881
- live.barWidthPx = null;
1882
- // The reveal guard belongs to the chart instance it guarded. Without this
1883
- // reset, StrictMode's dev-only mount→unmount→remount plays the entrance on
1884
- // the throwaway instance and the surviving one renders without it.
1885
- live.hasRevealed = false;
1886
- };
1887
- // eslint-disable-next-line react-hooks/exhaustive-deps
1888
- }, [renderer]);
1889
-
1890
- // ── Sync ECharts with props/theme/selection — resolve, build, push ────────────
1891
- useEffect(() => {
1892
- const chart = echartsRef.current;
1893
- const container = containerRef.current;
1894
- if (!chart || !container) return;
1895
-
1896
- // Colors come from the <style> committed just before this effect ran — read
1897
- // them here, right before the push, rather than round-tripping through state.
1898
- live.resolved = resolveColors(container, config, seriesKeys);
1899
-
1900
- const push = (withEntrance: boolean) => {
1901
- // Refresh the value-axis pixel scale before building, so stripped caps get the
1902
- // right per-bar fraction on this same push (after a resize the coordinate
1903
- // system is already updated here). Null before the very first push, and stale
1904
- // when this push rescales the axis (loading skeleton → real data) — the
1905
- // post-apply re-measure below corrects both before anything paints.
1906
- const measured = measureValuePxPerUnit(chart, isHorizontal);
1907
- if (measured != null) live.valuePxPerUnit = measured;
1908
-
1909
- const apply = () => {
1910
- const option = buildOption();
1911
- const merged = chartOptions ? { ...option, ...chartOptions } : option;
1912
- Object.assign(merged, {
1913
- animation: withEntrance,
1914
- animationDuration: BAR_GROW_DURATION,
1915
- animationDurationUpdate: 0,
1916
- });
1917
- // chartOptions is an untyped escape hatch — the spread erases the option's
1918
- // shape, so re-assert it. The only cast in the file.
1919
- chart.setOption(merged as EChartsOption, { notMerge: true });
1920
- };
1921
-
1922
- apply();
1923
-
1924
- // Some things can only be sized once a coordinate system has been laid out, so
1925
- // the first build uses fallbacks: the `blocks` variant's square segments (bar
1926
- // width), the stacked-segment gap, and the stripped variant's constant-pixel
1927
- // cap (both value-axis pixels-per-unit). Measure now and, if anything moved,
1928
- // rebuild IMMEDIATELY — still inside this task, before the browser paints, so
1929
- // the corrected chart is the only thing ever shown. Doing this from the async
1930
- // `finished` handler instead made the bars visibly re-align a frame later —
1931
- // exactly the stripped-cap flicker this replaces.
1932
- let needsRebuild = false;
1933
- if (live.handlers.hasBlocks) {
1934
- const width = measureBarWidthPx(chart, isHorizontal, barCategoryGap);
1935
- if (width != null && (live.barWidthPx == null || Math.abs(width - live.barWidthPx) > 0.5)) {
1936
- live.barWidthPx = width;
1937
- needsRebuild = true;
1938
- }
1939
- }
1940
- if (live.handlers.hasStackGap || live.handlers.hasStripped) {
1941
- const scale = measureValuePxPerUnit(chart, isHorizontal);
1942
- if (scale != null && (live.valuePxPerUnit == null || live.valuePxPerUnit !== scale)) {
1943
- live.valuePxPerUnit = scale;
1944
- needsRebuild = true;
1945
- }
1946
- }
1947
- if (needsRebuild) apply();
1948
- // Mark when the entrance settles, so the stripped-cap correction holds off
1949
- // until the grow finishes (0 = nothing animating, correct immediately).
1950
- const maxStagger = data.length > 1 ? (data.length - 1) * BAR_STAGGER : 0;
1951
- live.revealEndsAt = withEntrance ? performance.now() + BAR_GROW_DURATION + maxStagger : 0;
1952
- // Overlays live outside the option — reposition them after every push.
1953
- syncBrushOverlayNow();
1954
- };
1955
-
1956
- // A stripped-cap correction that never disturbs the entrance or a brush drag:
1957
- // rebuild only the stripped series with fresh per-datum cap fractions (the just
1958
- // -measured live.valuePxPerUnit) and merge them silently.
1959
- // Drives the `expandable` hover: eases live.expand.progress toward its target
1960
- // and re-merges ONLY the expandable series each frame, so the strip grows out
1961
- // of the bar's middle. A series-scoped silent merge (same shape as
1962
- // patchStrippedCaps) — never a full notMerge push, which would fight the
1963
- // hover state it is animating.
1964
- live.animateExpand = (key: string | null, index: number | null) => {
1965
- const expandKeys = new Set(
1966
- bars.filter((bar) => bar.variant === "expandable").map((bar) => bar.dataKey),
1967
- );
1968
- if (!expandKeys.size) return;
1969
-
1970
- const next = index != null && key != null ? index : null;
1971
- if (live.expand.hovered === next && (key == null || live.expand.key === key)) return;
1972
- if (key != null) live.expand.key = key;
1973
- live.expand.hovered = next;
1974
- // Seed the newly hovered bar so it has something to ease from.
1975
- if (next != null && !live.expand.progress.has(next)) {
1976
- live.expand.progress.set(next, EXPAND_COLLAPSED);
1977
- }
1978
- if (live.expandRaf) return; // a loop is already running; it picks up the new target
1979
-
1980
- const patchOnce = () => {
1981
- const option = buildOption();
1982
- const series = Array.isArray(option.series)
1983
- ? option.series
1984
- : option.series
1985
- ? [option.series]
1986
- : [];
1987
- const patch = series.filter(
1988
- (s): s is BarSeriesOption => typeof s?.id === "string" && expandKeys.has(s.id),
1989
- );
1990
- if (patch.length) chart.setOption({ series: patch }, { silent: true, lazyUpdate: true });
1991
- };
1992
-
1993
- let last = performance.now();
1994
- const step = () => {
1995
- const now = performance.now();
1996
- const dt = Math.min(64, now - last);
1997
- last = now;
1998
- // Exponential approach — every bar eases toward its own target, so the one
1999
- // being left keeps animating shut while the next one opens.
2000
- const k = 1 - Math.exp(-dt / EXPAND_TAU);
2001
- let moving = false;
2002
- for (const [i, value] of live.expand.progress) {
2003
- const target = i === live.expand.hovered ? 1 : EXPAND_COLLAPSED;
2004
- const eased = value + (target - value) * k;
2005
- if (Math.abs(target - eased) < 0.004) {
2006
- if (target === EXPAND_COLLAPSED) live.expand.progress.delete(i);
2007
- else live.expand.progress.set(i, target);
2008
- } else {
2009
- live.expand.progress.set(i, eased);
2010
- moving = true;
2011
- }
2012
- }
2013
- patchOnce();
2014
- live.expandRaf = moving ? requestAnimationFrame(step) : 0;
2015
- };
2016
- live.expandRaf = requestAnimationFrame(step);
2017
- };
2018
-
2019
- live.patchStrippedCaps = () => {
2020
- const option = buildOption();
2021
- const series = Array.isArray(option.series)
2022
- ? option.series
2023
- : option.series
2024
- ? [option.series]
2025
- : [];
2026
- const strippedKeys = new Set(
2027
- bars.filter((bar) => bar.variant === "stripped").map((bar) => bar.dataKey),
2028
- );
2029
- const patch = series.filter(
2030
- (s): s is BarSeriesOption => typeof s?.id === "string" && strippedKeys.has(s.id),
2031
- );
2032
- if (patch.length) chart.setOption({ series: patch }, { silent: true, lazyUpdate: true });
2033
- };
2034
-
2035
- // Intro grow-in — ECharts' native bar entrance (bars rise from the baseline),
2036
- // staggered per-datum by animationType, enabled only for the first real
2037
- // render: every later push (selection, theme, zoom) applies instantly, since
2038
- // notMerge would otherwise replay the entrance on each. A loading cycle
2039
- // re-arms it: the Recharts twin remounts its <Bar>s while loading and replays
2040
- // the intro, so data → loading → data grows in again here too.
2041
- if (isLoading) live.hasRevealed = false;
2042
- const shouldReveal = !live.hasRevealed && !isLoading;
2043
- if (shouldReveal) live.hasRevealed = true;
2044
- const revealEnabled =
2045
- animation && shouldReveal && effectiveAnimation !== "none" && !shouldReduceMotion;
2046
- push(revealEnabled);
2047
-
2048
- // Theme flips and resizes re-enter here without touching React: re-read the
2049
- // tokens (the .dark class changed, or textures need renderer-sized rebakes)
2050
- // and push an update-style option.
2051
- live.repush = () => {
2052
- live.resolved = resolveColors(container, config, seriesKeys);
2053
- push(false);
2054
- };
2055
- }, [
2056
- renderer,
2057
- live,
2058
- buildOption,
2059
- chartOptions,
2060
- isLoading,
2061
- animation,
2062
- effectiveAnimation,
2063
- shouldReduceMotion,
2064
- config,
2065
- seriesKeys,
2066
- data.length,
2067
- bars,
2068
- isHorizontal,
2069
- barCategoryGap,
2070
- syncBrushOverlayNow,
2071
- ]);
2072
-
2073
- // ── Default tooltip — show the tooltip at `defaultIndex` with no hover ────────
2074
- // Recharts' `defaultIndex` keeps a tooltip open on load; ECharts has no static
2075
- // equivalent, so dispatch `showTip` once the layout has settled.
2076
- useEffect(() => {
2077
- const chart = echartsRef.current;
2078
- const index = tooltipSlot.defaultIndex;
2079
- if (!chart || isLoading || !tooltipSlot.present || index == null) return;
2080
- const timer = setTimeout(() => {
2081
- chart.dispatchAction({ type: "showTip", seriesIndex: 0, dataIndex: index });
2082
- }, 300);
2083
- return () => clearTimeout(timer);
2084
- }, [
2085
- renderer,
2086
- tooltipSlot.present,
2087
- tooltipSlot.defaultIndex,
2088
- isLoading,
2089
- data.length,
2090
- seriesKeys.length,
2091
- ]);
2092
-
2093
- // ── Loading shimmer — rAF sweeps a bright band across the gray bars ──────────
2094
- useEffect(() => {
2095
- const chart = echartsRef.current;
2096
- if (!chart || !isLoading) return;
2097
-
2098
- let raf = 0;
2099
- let lastPhase = 0;
2100
- const start = performance.now();
2101
- const tick = (now: number) => {
2102
- const phase = ((((now - start) / LOADING_ANIMATION_DURATION) % 1) + 1) % 1;
2103
- // Wrapped past 1 → the band is off-screen; swap in fresh random heights.
2104
- if (phase < lastPhase) live.loadingRows = getLoadingBarData(loadingBars);
2105
- lastPhase = phase;
2106
-
2107
- // Read tokens per frame, so a theme flip mid-loading retints the shimmer.
2108
- const foreground = live.resolved?.tokens.foreground ?? GRAY;
2109
- const w = chart.getWidth();
2110
- const h = chart.getHeight();
2111
- if (!w || !h) {
2112
- raf = requestAnimationFrame(tick);
2113
- return;
2114
- }
2115
- // Sweep the clip window from fully off-screen to fully off-screen, leaned
2116
- // 45°. The gradient runs on ABSOLUTE pixel coordinates (0,0)→(w,w) shared
2117
- // by every bar — the whole skeleton lives in one coordinate frame, so each
2118
- // bar brightens as the diagonal band passes diagonally over it, the same
2119
- // sweep language as the area chart's loading shimmer. `maxT` is the farthest
2120
- // plot corner projected onto the 45° axis, keeping the sweep tight instead
2121
- // of dawdling off-plot at the end of each loop.
2122
- const maxT = (w + h) / (2 * w);
2123
- const center = phase * (maxT + 2 * LOADING_SHIMMER_BAND) - LOADING_SHIMMER_BAND;
2124
- const fill = new echarts.graphic.LinearGradient(
2125
- 0,
2126
- 0,
2127
- w,
2128
- w,
2129
- shimmerWindowStops(center, foreground, LOADING_SHIMMER_MAX_OPACITY),
2130
- true,
2131
- );
2132
- chart.setOption(
2133
- { series: [{ id: "__loading", data: loadingData(), itemStyle: { color: fill } }] },
2134
- { silent: true, lazyUpdate: true },
2135
- );
2136
- raf = requestAnimationFrame(tick);
2137
- };
2138
- raf = requestAnimationFrame(tick);
2139
- return () => cancelAnimationFrame(raf);
2140
- }, [renderer, live, isLoading, loadingBars, loadingData]);
2141
-
2142
- // ── Legend overlay position ──────────────────────────────────────────────────
2143
- // Insets match the Recharts legend's breathing room inside the plot frame.
2144
- const legendStyle: CSSProperties = {
2145
- position: "absolute",
2146
- left: 16,
2147
- right: 16,
2148
- pointerEvents: "auto",
2149
- ...(legendSlot.verticalAlign === "top"
2150
- ? { top: 12 }
2151
- : legendSlot.verticalAlign === "bottom"
2152
- ? { bottom: brushEnabled ? brushHeight + 16 : 12 }
2153
- : { top: "50%", transform: "translateY(-50%)" }),
2154
- };
2155
-
2156
- return (
2157
- <div
2158
- ref={containerRef}
2159
- data-chart={chartId}
2160
- className={`relative flex flex-col text-xs ${className ?? ""}`}
2161
- >
2162
- <style dangerouslySetInnerHTML={{ __html: css }} />
2163
-
2164
- <div className="relative min-h-0 w-full flex-1">
2165
- <div ref={mountRef} className="h-full min-h-0 w-full" />
2166
- </div>
2167
-
2168
- {legendSlot.present && !isLoading && (
2169
- <LegendOverlay
2170
- seriesKeys={seriesKeys}
2171
- config={config}
2172
- variant={legendSlot.variant}
2173
- align={legendSlot.align}
2174
- verticalAlign={legendSlot.verticalAlign}
2175
- selectedKey={selectedDataKey}
2176
- hoveredKey={null}
2177
- isClickable={legendSlot.isClickable}
2178
- onToggle={toggleSelection}
2179
- style={legendStyle}
2180
- />
2181
- )}
2182
-
2183
- {isLoading && (
2184
- <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center">
2185
- <motion.div
2186
- initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.92 }}
2187
- animate={{ opacity: 1, scale: 1 }}
2188
- transition={{ duration: 0.25, ease: "easeOut" }}
2189
- className="text-primary bg-background flex items-center justify-center gap-2 rounded-md border px-2 py-0.5 text-sm"
2190
- >
2191
- <div className="border-border border-t-primary h-3 w-3 animate-spin rounded-full border" />
2192
- <span>Loading</span>
2193
- </motion.div>
2194
- </div>
2195
- )}
2196
- </div>
2197
- );
2198
- }
2199
-
2200
- // ─────────────────────────────────────────────────────────────────────────────
2201
- // Loading skeleton helpers
2202
- // ─────────────────────────────────────────────────────────────────────────────
2203
-
2204
- // Skeleton bar heights as a smooth random walk in a comfortable band — reads
2205
- // like a resting chart instead of raw noise spikes.
2206
- function getLoadingBarData(bars: number): number[] {
2207
- const rows: number[] = [];
2208
- let value = 40 + Math.random() * 25;
2209
- for (let i = 0; i < bars; i++) {
2210
- value = Math.min(85, Math.max(20, value + (Math.random() - 0.5) * 30));
2211
- rows.push(Math.round(value));
2212
- }
2213
- return rows;
2214
- }
2215
-
2216
- // Gradient stops forming a hard clip window around `center`: full `peak` alpha
2217
- // inside, zero outside, with a small feather so the edge isn't aliased.
2218
- // `center` may run outside [0, 1] so the window fully enters and exits the frame.
2219
- function shimmerWindowStops(center: number, color: string, peak: number) {
2220
- const half = LOADING_SHIMMER_BAND;
2221
- const feather = LOADING_SHIMMER_FEATHER;
2222
-
2223
- const alphaAt = (x: number) => {
2224
- const dist = Math.abs(x - center);
2225
- if (dist <= half - feather) return peak;
2226
- if (dist >= half) return 0;
2227
- // Sine-eased falloff — a linear ramp still reads as a hard cut.
2228
- return peak * Math.sin(((1 - (dist - (half - feather)) / feather) * Math.PI) / 2);
2229
- };
2230
-
2231
- const offsets = [
2232
- 0,
2233
- center - half,
2234
- center - half + feather,
2235
- center,
2236
- center + half - feather,
2237
- center + half,
2238
- 1,
2239
- ]
2240
- .filter((x) => x >= 0 && x <= 1)
2241
- .sort((a, b) => a - b);
2242
-
2243
- const stops: { offset: number; color: string }[] = [];
2244
- for (const offset of offsets) {
2245
- if (stops.length === 0 || offset - stops[stops.length - 1].offset > 1e-4) {
2246
- stops.push({ offset, color: withAlpha(color, alphaAt(offset)) });
2247
- }
2248
- }
2249
- return stops;
2250
- }
2251
-
2252
- // Compound API: every part hangs off the root as a static member, so a consumer
2253
- // writes <EChartsBarChart.Bar/>, <EChartsBarChart.Tooltip/>, … from a single
2254
- // import — no colliding named marker exports when several charts share one file.
2255
- EChartsBarChart.Bar = Bar;
2256
- EChartsBarChart.XAxis = XAxis;
2257
- EChartsBarChart.YAxis = YAxis;
2258
- EChartsBarChart.Grid = Grid;
2259
- EChartsBarChart.Tooltip = Tooltip;
2260
- EChartsBarChart.Legend = Legend;
2261
- EChartsBarChart.Brush = Brush;