@voila.dev/chart 4.0.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 (377) hide show
  1. package/README.md +85 -0
  2. package/dist/brush/brush-layer.d.ts +16 -0
  3. package/dist/brush/brush-layer.d.ts.map +1 -0
  4. package/dist/brush/brush-layer.js +137 -0
  5. package/dist/brush/brush-layer.js.map +7 -0
  6. package/dist/brush/index.d.ts +10 -0
  7. package/dist/brush/index.d.ts.map +1 -0
  8. package/dist/brush/index.js +8 -0
  9. package/dist/brush/index.js.map +7 -0
  10. package/dist/canvas/canvas-renderer.d.ts +11 -0
  11. package/dist/canvas/canvas-renderer.d.ts.map +1 -0
  12. package/dist/canvas/canvas-renderer.js +49 -0
  13. package/dist/canvas/canvas-renderer.js.map +7 -0
  14. package/dist/canvas/index.d.ts +2 -0
  15. package/dist/canvas/index.d.ts.map +1 -0
  16. package/dist/canvas/index.js +5 -0
  17. package/dist/canvas/index.js.map +7 -0
  18. package/dist/canvas/paint-scene.d.ts +4 -0
  19. package/dist/canvas/paint-scene.d.ts.map +1 -0
  20. package/dist/canvas/paint-scene.js +130 -0
  21. package/dist/canvas/paint-scene.js.map +7 -0
  22. package/dist/canvas/resolve-color.d.ts +18 -0
  23. package/dist/canvas/resolve-color.d.ts.map +1 -0
  24. package/dist/canvas/resolve-color.js +52 -0
  25. package/dist/canvas/resolve-color.js.map +7 -0
  26. package/dist/contour/density.d.ts +20 -0
  27. package/dist/contour/density.d.ts.map +1 -0
  28. package/dist/contour/density.js +59 -0
  29. package/dist/contour/density.js.map +7 -0
  30. package/dist/contour/index.d.ts +2 -0
  31. package/dist/contour/index.d.ts.map +1 -0
  32. package/dist/contour/index.js +5 -0
  33. package/dist/contour/index.js.map +7 -0
  34. package/dist/core/channel.d.ts +11 -0
  35. package/dist/core/channel.d.ts.map +1 -0
  36. package/dist/core/channel.js +44 -0
  37. package/dist/core/channel.js.map +7 -0
  38. package/dist/core/compile.d.ts +22 -0
  39. package/dist/core/compile.d.ts.map +1 -0
  40. package/dist/core/compile.js +295 -0
  41. package/dist/core/compile.js.map +7 -0
  42. package/dist/core/define-chart.d.ts +96 -0
  43. package/dist/core/define-chart.d.ts.map +1 -0
  44. package/dist/core/define-chart.js +7 -0
  45. package/dist/core/define-chart.js.map +7 -0
  46. package/dist/core/facet.d.ts +7 -0
  47. package/dist/core/facet.d.ts.map +1 -0
  48. package/dist/core/facet.js +134 -0
  49. package/dist/core/facet.js.map +7 -0
  50. package/dist/core/format.d.ts +37 -0
  51. package/dist/core/format.d.ts.map +1 -0
  52. package/dist/core/format.js +108 -0
  53. package/dist/core/format.js.map +7 -0
  54. package/dist/core/guides/axes.d.ts +25 -0
  55. package/dist/core/guides/axes.d.ts.map +1 -0
  56. package/dist/core/guides/axes.js +204 -0
  57. package/dist/core/guides/axes.js.map +7 -0
  58. package/dist/core/marks/area.d.ts +23 -0
  59. package/dist/core/marks/area.d.ts.map +1 -0
  60. package/dist/core/marks/area.js +187 -0
  61. package/dist/core/marks/area.js.map +7 -0
  62. package/dist/core/marks/bar.d.ts +26 -0
  63. package/dist/core/marks/bar.d.ts.map +1 -0
  64. package/dist/core/marks/bar.js +200 -0
  65. package/dist/core/marks/bar.js.map +7 -0
  66. package/dist/core/marks/cell.d.ts +16 -0
  67. package/dist/core/marks/cell.d.ts.map +1 -0
  68. package/dist/core/marks/cell.js +98 -0
  69. package/dist/core/marks/cell.js.map +7 -0
  70. package/dist/core/marks/dot.d.ts +17 -0
  71. package/dist/core/marks/dot.d.ts.map +1 -0
  72. package/dist/core/marks/dot.js +125 -0
  73. package/dist/core/marks/dot.js.map +7 -0
  74. package/dist/core/marks/funnel-layout.d.ts +37 -0
  75. package/dist/core/marks/funnel-layout.d.ts.map +1 -0
  76. package/dist/core/marks/funnel-layout.js +61 -0
  77. package/dist/core/marks/funnel-layout.js.map +7 -0
  78. package/dist/core/marks/funnel.d.ts +19 -0
  79. package/dist/core/marks/funnel.d.ts.map +1 -0
  80. package/dist/core/marks/funnel.js +143 -0
  81. package/dist/core/marks/funnel.js.map +7 -0
  82. package/dist/core/marks/line.d.ts +17 -0
  83. package/dist/core/marks/line.d.ts.map +1 -0
  84. package/dist/core/marks/line.js +150 -0
  85. package/dist/core/marks/line.js.map +7 -0
  86. package/dist/core/marks/polar.d.ts +61 -0
  87. package/dist/core/marks/polar.d.ts.map +1 -0
  88. package/dist/core/marks/polar.js +436 -0
  89. package/dist/core/marks/polar.js.map +7 -0
  90. package/dist/core/marks/rect.d.ts +22 -0
  91. package/dist/core/marks/rect.d.ts.map +1 -0
  92. package/dist/core/marks/rect.js +119 -0
  93. package/dist/core/marks/rect.js.map +7 -0
  94. package/dist/core/marks/rule.d.ts +17 -0
  95. package/dist/core/marks/rule.d.ts.map +1 -0
  96. package/dist/core/marks/rule.js +73 -0
  97. package/dist/core/marks/rule.js.map +7 -0
  98. package/dist/core/marks/shared.d.ts +41 -0
  99. package/dist/core/marks/shared.d.ts.map +1 -0
  100. package/dist/core/marks/shared.js +74 -0
  101. package/dist/core/marks/shared.js.map +7 -0
  102. package/dist/core/marks/stack.d.ts +13 -0
  103. package/dist/core/marks/stack.d.ts.map +1 -0
  104. package/dist/core/marks/stack.js +28 -0
  105. package/dist/core/marks/stack.js.map +7 -0
  106. package/dist/core/marks/stats/difference.d.ts +20 -0
  107. package/dist/core/marks/stats/difference.d.ts.map +1 -0
  108. package/dist/core/marks/stats/difference.js +168 -0
  109. package/dist/core/marks/stats/difference.js.map +7 -0
  110. package/dist/core/marks/stats/distribution.d.ts +41 -0
  111. package/dist/core/marks/stats/distribution.d.ts.map +1 -0
  112. package/dist/core/marks/stats/distribution.js +354 -0
  113. package/dist/core/marks/stats/distribution.js.map +7 -0
  114. package/dist/core/marks/stats/dodge.d.ts +16 -0
  115. package/dist/core/marks/stats/dodge.d.ts.map +1 -0
  116. package/dist/core/marks/stats/dodge.js +104 -0
  117. package/dist/core/marks/stats/dodge.js.map +7 -0
  118. package/dist/core/marks/stats/hexbin.d.ts +26 -0
  119. package/dist/core/marks/stats/hexbin.d.ts.map +1 -0
  120. package/dist/core/marks/stats/hexbin.js +113 -0
  121. package/dist/core/marks/stats/hexbin.js.map +7 -0
  122. package/dist/core/marks/stats/histogram.d.ts +27 -0
  123. package/dist/core/marks/stats/histogram.d.ts.map +1 -0
  124. package/dist/core/marks/stats/histogram.js +70 -0
  125. package/dist/core/marks/stats/histogram.js.map +7 -0
  126. package/dist/core/marks/stats/regression.d.ts +13 -0
  127. package/dist/core/marks/stats/regression.d.ts.map +1 -0
  128. package/dist/core/marks/stats/regression.js +80 -0
  129. package/dist/core/marks/stats/regression.js.map +7 -0
  130. package/dist/core/marks/stats/statistics.d.ts +34 -0
  131. package/dist/core/marks/stats/statistics.d.ts.map +1 -0
  132. package/dist/core/marks/stats/statistics.js +111 -0
  133. package/dist/core/marks/stats/statistics.js.map +7 -0
  134. package/dist/core/marks/stats/waffle.d.ts +18 -0
  135. package/dist/core/marks/stats/waffle.d.ts.map +1 -0
  136. package/dist/core/marks/stats/waffle.js +117 -0
  137. package/dist/core/marks/stats/waffle.js.map +7 -0
  138. package/dist/core/marks/text.d.ts +19 -0
  139. package/dist/core/marks/text.d.ts.map +1 -0
  140. package/dist/core/marks/text.js +60 -0
  141. package/dist/core/marks/text.js.map +7 -0
  142. package/dist/core/motion/tween.d.ts +6 -0
  143. package/dist/core/motion/tween.d.ts.map +1 -0
  144. package/dist/core/motion/tween.js +98 -0
  145. package/dist/core/motion/tween.js.map +7 -0
  146. package/dist/core/nearest.d.ts +25 -0
  147. package/dist/core/nearest.d.ts.map +1 -0
  148. package/dist/core/nearest.js +151 -0
  149. package/dist/core/nearest.js.map +7 -0
  150. package/dist/core/paths.d.ts +65 -0
  151. package/dist/core/paths.d.ts.map +1 -0
  152. package/dist/core/paths.js +192 -0
  153. package/dist/core/paths.js.map +7 -0
  154. package/dist/core/scales/color.d.ts +11 -0
  155. package/dist/core/scales/color.d.ts.map +1 -0
  156. package/dist/core/scales/color.js +35 -0
  157. package/dist/core/scales/color.js.map +7 -0
  158. package/dist/core/scales/continuous.d.ts +18 -0
  159. package/dist/core/scales/continuous.d.ts.map +1 -0
  160. package/dist/core/scales/continuous.js +82 -0
  161. package/dist/core/scales/continuous.js.map +7 -0
  162. package/dist/core/scales/discrete.d.ts +22 -0
  163. package/dist/core/scales/discrete.d.ts.map +1 -0
  164. package/dist/core/scales/discrete.js +103 -0
  165. package/dist/core/scales/discrete.js.map +7 -0
  166. package/dist/core/scales/resolve.d.ts +18 -0
  167. package/dist/core/scales/resolve.d.ts.map +1 -0
  168. package/dist/core/scales/resolve.js +112 -0
  169. package/dist/core/scales/resolve.js.map +7 -0
  170. package/dist/core/scales/time.d.ts +4 -0
  171. package/dist/core/scales/time.d.ts.map +1 -0
  172. package/dist/core/scales/time.js +140 -0
  173. package/dist/core/scales/time.js.map +7 -0
  174. package/dist/core/text.d.ts +3 -0
  175. package/dist/core/text.d.ts.map +1 -0
  176. package/dist/core/text.js +29 -0
  177. package/dist/core/text.js.map +7 -0
  178. package/dist/core/theme.d.ts +8 -0
  179. package/dist/core/theme.d.ts.map +1 -0
  180. package/dist/core/theme.js +21 -0
  181. package/dist/core/theme.js.map +7 -0
  182. package/dist/core/ticks.d.ts +27 -0
  183. package/dist/core/ticks.d.ts.map +1 -0
  184. package/dist/core/ticks.js +76 -0
  185. package/dist/core/ticks.js.map +7 -0
  186. package/dist/core/types.d.ts +305 -0
  187. package/dist/core/types.d.ts.map +1 -0
  188. package/dist/core/types.js +1 -0
  189. package/dist/core/types.js.map +7 -0
  190. package/dist/force/force.d.ts +30 -0
  191. package/dist/force/force.d.ts.map +1 -0
  192. package/dist/force/force.js +143 -0
  193. package/dist/force/force.js.map +7 -0
  194. package/dist/force/index.d.ts +2 -0
  195. package/dist/force/index.d.ts.map +1 -0
  196. package/dist/force/index.js +7 -0
  197. package/dist/force/index.js.map +7 -0
  198. package/dist/geo/geo.d.ts +59 -0
  199. package/dist/geo/geo.d.ts.map +1 -0
  200. package/dist/geo/geo.js +231 -0
  201. package/dist/geo/geo.js.map +7 -0
  202. package/dist/geo/index.d.ts +2 -0
  203. package/dist/geo/index.d.ts.map +1 -0
  204. package/dist/geo/index.js +13 -0
  205. package/dist/geo/index.js.map +7 -0
  206. package/dist/hierarchy/hierarchy.d.ts +24 -0
  207. package/dist/hierarchy/hierarchy.d.ts.map +1 -0
  208. package/dist/hierarchy/hierarchy.js +279 -0
  209. package/dist/hierarchy/hierarchy.js.map +7 -0
  210. package/dist/hierarchy/index.d.ts +2 -0
  211. package/dist/hierarchy/index.d.ts.map +1 -0
  212. package/dist/hierarchy/index.js +11 -0
  213. package/dist/hierarchy/index.js.map +7 -0
  214. package/dist/index.d.ts +29 -0
  215. package/dist/index.d.ts.map +1 -0
  216. package/dist/index.js +96 -0
  217. package/dist/index.js.map +7 -0
  218. package/dist/react/brush-contract.d.ts +23 -0
  219. package/dist/react/brush-contract.d.ts.map +1 -0
  220. package/dist/react/brush-contract.js +7 -0
  221. package/dist/react/brush-contract.js.map +7 -0
  222. package/dist/react/chart-data-table.d.ts +18 -0
  223. package/dist/react/chart-data-table.d.ts.map +1 -0
  224. package/dist/react/chart-data-table.js +55 -0
  225. package/dist/react/chart-data-table.js.map +7 -0
  226. package/dist/react/chart-legend.d.ts +17 -0
  227. package/dist/react/chart-legend.d.ts.map +1 -0
  228. package/dist/react/chart-legend.js +126 -0
  229. package/dist/react/chart-legend.js.map +7 -0
  230. package/dist/react/chart-tooltip.d.ts +18 -0
  231. package/dist/react/chart-tooltip.d.ts.map +1 -0
  232. package/dist/react/chart-tooltip.js +95 -0
  233. package/dist/react/chart-tooltip.js.map +7 -0
  234. package/dist/react/chart.d.ts +45 -0
  235. package/dist/react/chart.d.ts.map +1 -0
  236. package/dist/react/chart.js +383 -0
  237. package/dist/react/chart.js.map +7 -0
  238. package/dist/react/focus-overlay.d.ts +14 -0
  239. package/dist/react/focus-overlay.d.ts.map +1 -0
  240. package/dist/react/focus-overlay.js +147 -0
  241. package/dist/react/focus-overlay.js.map +7 -0
  242. package/dist/react/index.d.ts +7 -0
  243. package/dist/react/index.d.ts.map +1 -0
  244. package/dist/react/index.js +11 -0
  245. package/dist/react/index.js.map +7 -0
  246. package/dist/react/messages.d.ts +16 -0
  247. package/dist/react/messages.d.ts.map +1 -0
  248. package/dist/react/messages.js +33 -0
  249. package/dist/react/messages.js.map +7 -0
  250. package/dist/react/renderer.d.ts +14 -0
  251. package/dist/react/renderer.d.ts.map +1 -0
  252. package/dist/react/renderer.js +1 -0
  253. package/dist/react/renderer.js.map +7 -0
  254. package/dist/react/sr-only.d.ts +4 -0
  255. package/dist/react/sr-only.d.ts.map +1 -0
  256. package/dist/react/sr-only.js +15 -0
  257. package/dist/react/sr-only.js.map +7 -0
  258. package/dist/react/svg-renderer.d.ts +4 -0
  259. package/dist/react/svg-renderer.d.ts.map +1 -0
  260. package/dist/react/svg-renderer.js +20 -0
  261. package/dist/react/svg-renderer.js.map +7 -0
  262. package/dist/react/svg-scene.d.ts +8 -0
  263. package/dist/react/svg-scene.d.ts.map +1 -0
  264. package/dist/react/svg-scene.js +121 -0
  265. package/dist/react/svg-scene.js.map +7 -0
  266. package/dist/react/use-animated-scene.d.ts +10 -0
  267. package/dist/react/use-animated-scene.d.ts.map +1 -0
  268. package/dist/react/use-animated-scene.js +41 -0
  269. package/dist/react/use-animated-scene.js.map +7 -0
  270. package/dist/react/use-chart-focus.d.ts +29 -0
  271. package/dist/react/use-chart-focus.d.ts.map +1 -0
  272. package/dist/react/use-chart-focus.js +180 -0
  273. package/dist/react/use-chart-focus.js.map +7 -0
  274. package/dist/react/use-chart-width.d.ts +12 -0
  275. package/dist/react/use-chart-width.d.ts.map +1 -0
  276. package/dist/react/use-chart-width.js +23 -0
  277. package/dist/react/use-chart-width.js.map +7 -0
  278. package/dist/react/use-chart-zoom.d.ts +13 -0
  279. package/dist/react/use-chart-zoom.d.ts.map +1 -0
  280. package/dist/react/use-chart-zoom.js +52 -0
  281. package/dist/react/use-chart-zoom.js.map +7 -0
  282. package/dist/react/use-text-measurer.d.ts +8 -0
  283. package/dist/react/use-text-measurer.d.ts.map +1 -0
  284. package/dist/react/use-text-measurer.js +44 -0
  285. package/dist/react/use-text-measurer.js.map +7 -0
  286. package/dist/sankey/index.d.ts +2 -0
  287. package/dist/sankey/index.d.ts.map +1 -0
  288. package/dist/sankey/index.js +7 -0
  289. package/dist/sankey/index.js.map +7 -0
  290. package/dist/sankey/sankey.d.ts +29 -0
  291. package/dist/sankey/sankey.d.ts.map +1 -0
  292. package/dist/sankey/sankey.js +127 -0
  293. package/dist/sankey/sankey.js.map +7 -0
  294. package/dist/voronoi/index.d.ts +2 -0
  295. package/dist/voronoi/index.d.ts.map +1 -0
  296. package/dist/voronoi/index.js +5 -0
  297. package/dist/voronoi/index.js.map +7 -0
  298. package/dist/voronoi/voronoi.d.ts +16 -0
  299. package/dist/voronoi/voronoi.d.ts.map +1 -0
  300. package/dist/voronoi/voronoi.js +62 -0
  301. package/dist/voronoi/voronoi.js.map +7 -0
  302. package/package.json +74 -0
  303. package/src/brush/brush-layer.tsx +174 -0
  304. package/src/brush/index.ts +14 -0
  305. package/src/canvas/canvas-renderer.tsx +56 -0
  306. package/src/canvas/index.ts +1 -0
  307. package/src/canvas/paint-scene.ts +152 -0
  308. package/src/canvas/resolve-color.ts +65 -0
  309. package/src/contour/density.ts +90 -0
  310. package/src/contour/index.ts +1 -0
  311. package/src/core/channel.ts +53 -0
  312. package/src/core/compile.ts +436 -0
  313. package/src/core/define-chart.ts +128 -0
  314. package/src/core/facet.ts +189 -0
  315. package/src/core/format.ts +163 -0
  316. package/src/core/guides/axes.ts +283 -0
  317. package/src/core/marks/area.ts +238 -0
  318. package/src/core/marks/bar.ts +278 -0
  319. package/src/core/marks/cell.ts +123 -0
  320. package/src/core/marks/dot.ts +157 -0
  321. package/src/core/marks/funnel-layout.ts +113 -0
  322. package/src/core/marks/funnel.ts +172 -0
  323. package/src/core/marks/line.ts +206 -0
  324. package/src/core/marks/polar.ts +568 -0
  325. package/src/core/marks/rect.ts +156 -0
  326. package/src/core/marks/rule.ts +104 -0
  327. package/src/core/marks/shared.ts +134 -0
  328. package/src/core/marks/stack.ts +42 -0
  329. package/src/core/marks/stats/difference.ts +205 -0
  330. package/src/core/marks/stats/distribution.ts +425 -0
  331. package/src/core/marks/stats/dodge.ts +136 -0
  332. package/src/core/marks/stats/hexbin.ts +152 -0
  333. package/src/core/marks/stats/histogram.ts +108 -0
  334. package/src/core/marks/stats/regression.ts +108 -0
  335. package/src/core/marks/stats/statistics.ts +153 -0
  336. package/src/core/marks/stats/waffle.ts +141 -0
  337. package/src/core/marks/text.ts +97 -0
  338. package/src/core/motion/tween.ts +131 -0
  339. package/src/core/nearest.ts +223 -0
  340. package/src/core/paths.ts +323 -0
  341. package/src/core/scales/color.ts +51 -0
  342. package/src/core/scales/continuous.ts +111 -0
  343. package/src/core/scales/discrete.ts +156 -0
  344. package/src/core/scales/resolve.ts +158 -0
  345. package/src/core/scales/time.ts +180 -0
  346. package/src/core/text.ts +42 -0
  347. package/src/core/theme.ts +26 -0
  348. package/src/core/ticks.ts +118 -0
  349. package/src/core/types.ts +358 -0
  350. package/src/force/force.ts +200 -0
  351. package/src/force/index.ts +6 -0
  352. package/src/geo/geo.ts +346 -0
  353. package/src/geo/index.ts +10 -0
  354. package/src/hierarchy/hierarchy.ts +377 -0
  355. package/src/hierarchy/index.ts +6 -0
  356. package/src/index.ts +70 -0
  357. package/src/react/brush-contract.ts +35 -0
  358. package/src/react/chart-data-table.tsx +98 -0
  359. package/src/react/chart-legend.tsx +130 -0
  360. package/src/react/chart-tooltip.tsx +111 -0
  361. package/src/react/chart.tsx +489 -0
  362. package/src/react/focus-overlay.tsx +164 -0
  363. package/src/react/index.ts +9 -0
  364. package/src/react/messages.ts +49 -0
  365. package/src/react/renderer.ts +15 -0
  366. package/src/react/sr-only.ts +14 -0
  367. package/src/react/svg-renderer.tsx +18 -0
  368. package/src/react/svg-scene.tsx +146 -0
  369. package/src/react/use-animated-scene.ts +57 -0
  370. package/src/react/use-chart-focus.ts +245 -0
  371. package/src/react/use-chart-width.ts +27 -0
  372. package/src/react/use-chart-zoom.ts +67 -0
  373. package/src/react/use-text-measurer.ts +49 -0
  374. package/src/sankey/index.ts +6 -0
  375. package/src/sankey/sankey.ts +179 -0
  376. package/src/voronoi/index.ts +1 -0
  377. package/src/voronoi/voronoi.ts +84 -0
@@ -0,0 +1,111 @@
1
+ import type * as React from "react";
2
+ import type { ChartScene } from "#/core/types.ts";
3
+ import type { ActiveFocus } from "#/react/use-chart-focus.ts";
4
+
5
+ const OFFSET = 12;
6
+ const MAX_WIDTH = 280;
7
+
8
+ export interface ChartTooltipRenderProps {
9
+ readonly active: ActiveFocus;
10
+ readonly scene: ChartScene;
11
+ }
12
+
13
+ const SURFACE: React.CSSProperties = {
14
+ position: "absolute",
15
+ top: 0,
16
+ left: 0,
17
+ pointerEvents: "none",
18
+ zIndex: 1,
19
+ maxWidth: MAX_WIDTH,
20
+ padding: "6px 10px",
21
+ borderRadius: 8,
22
+ border:
23
+ "1px solid var(--border, color-mix(in oklab, currentColor 15%, transparent))",
24
+ background: "var(--popover, Canvas)",
25
+ color: "var(--popover-foreground, CanvasText)",
26
+ boxShadow: "0 4px 16px rgb(0 0 0 / 0.08)",
27
+ fontSize: 12,
28
+ lineHeight: 1.4,
29
+ whiteSpace: "nowrap",
30
+ };
31
+
32
+ /** The default content: the stop's title, then one row per series with its swatch. */
33
+ export function ChartTooltipContent({
34
+ active,
35
+ scene,
36
+ }: ChartTooltipRenderProps) {
37
+ const rows =
38
+ active.stop.points.length > 1 ? active.stop.points : [active.point];
39
+ return (
40
+ <>
41
+ <div style={{ fontWeight: 600, marginBottom: 2 }}>
42
+ {active.point.title}
43
+ </div>
44
+ {rows.map((point) => (
45
+ <div
46
+ key={point.key}
47
+ style={{
48
+ display: "flex",
49
+ alignItems: "center",
50
+ gap: 6,
51
+ fontWeight:
52
+ point.key === active.point.key && rows.length > 1 ? 600 : 400,
53
+ }}
54
+ >
55
+ <span
56
+ aria-hidden="true"
57
+ style={{
58
+ width: 8,
59
+ height: 8,
60
+ borderRadius: 2,
61
+ background: point.color,
62
+ flex: "none",
63
+ }}
64
+ />
65
+ <span style={{ color: "var(--muted-foreground, inherit)" }}>
66
+ {point.seriesLabel ?? scene.yLabel}
67
+ </span>
68
+ <span
69
+ style={{
70
+ marginLeft: "auto",
71
+ paddingLeft: 12,
72
+ fontVariantNumeric: "tabular-nums",
73
+ }}
74
+ >
75
+ {point.value}
76
+ </span>
77
+ </div>
78
+ ))}
79
+ </>
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Follows the focused stop, on the side with room. Hidden from assistive
85
+ * tech: the live region already says the same thing once, where this would
86
+ * say it on every pointer move.
87
+ */
88
+ export function ChartTooltip({
89
+ active,
90
+ scene,
91
+ render,
92
+ }: ChartTooltipRenderProps & {
93
+ render: (props: ChartTooltipRenderProps) => React.ReactNode;
94
+ }) {
95
+ const anchorX = scene.focusOrder === "x" ? active.stop.at : active.point.x;
96
+ const anchorY = Math.min(...active.stop.points.map((point) => point.y));
97
+ const flip = anchorX > scene.width / 2;
98
+ return (
99
+ <div
100
+ aria-hidden="true"
101
+ data-slot="chart-tooltip"
102
+ data-pinned={active.pinned ? "" : undefined}
103
+ style={{
104
+ ...SURFACE,
105
+ transform: `translate(${flip ? `calc(${anchorX - OFFSET}px - 100%)` : `${anchorX + OFFSET}px`}, ${Math.max(0, anchorY - OFFSET)}px)`,
106
+ }}
107
+ >
108
+ {render({ active, scene })}
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,489 @@
1
+ import * as React from "react";
2
+ import { compileChart } from "#/core/compile.ts";
3
+ import type { ChartDefinition } from "#/core/define-chart.ts";
4
+ import { findNearest, focusStops } from "#/core/nearest.ts";
5
+ import { toNumber } from "#/core/scales/continuous.ts";
6
+ import type { ChartPoint } from "#/core/types.ts";
7
+ import {
8
+ type ChartBrush,
9
+ type ChartBrushRange,
10
+ ordered,
11
+ } from "#/react/brush-contract.ts";
12
+ import { ChartDataTable } from "#/react/chart-data-table.tsx";
13
+ import { ChartColorRamp, ChartLegend } from "#/react/chart-legend.tsx";
14
+ import {
15
+ ChartTooltip,
16
+ ChartTooltipContent,
17
+ type ChartTooltipRenderProps,
18
+ } from "#/react/chart-tooltip.tsx";
19
+ import { FocusOverlay } from "#/react/focus-overlay.tsx";
20
+ import { messagesFor } from "#/react/messages.ts";
21
+ import type { ChartRenderer } from "#/react/renderer.ts";
22
+ import { SR_ONLY } from "#/react/sr-only.ts";
23
+ import { SvgRenderer } from "#/react/svg-renderer.tsx";
24
+ import { useAnimatedScene } from "#/react/use-animated-scene.ts";
25
+ import { useChartFocus } from "#/react/use-chart-focus.ts";
26
+ import { useChartWidth } from "#/react/use-chart-width.ts";
27
+ import { useChartZoom } from "#/react/use-chart-zoom.ts";
28
+ import { useTextMeasurer } from "#/react/use-text-measurer.ts";
29
+
30
+ const DEFAULT_HEIGHT = 300;
31
+ const DEFAULT_INITIAL_WIDTH = 640;
32
+ const DEFAULT_DURATION = 300;
33
+
34
+ /** Focus ring, forced colours and reduced motion: what inline styles cannot say. */
35
+ const CHART_CSS = `
36
+ [data-slot="chart-surface"]:focus-visible{outline:2px solid var(--ring,Highlight);outline-offset:2px;border-radius:4px}
37
+ @media (forced-colors:active){
38
+ [data-slot="chart-svg"] [data-role="axis"],[data-slot="chart-svg"] [data-role="axis-title"]{fill:CanvasText}
39
+ [data-slot="chart-svg"] [data-role="grid"]{stroke:GrayText}
40
+ [data-slot="chart-focus-ring"]{stroke:Highlight}
41
+ [data-slot="chart-tooltip"]{border-color:CanvasText}
42
+ }
43
+ @media (prefers-reduced-motion:no-preference){
44
+ [data-slot="chart-tooltip"]{transition:transform 80ms ease-out}
45
+ }
46
+ `;
47
+
48
+ // `title` would put a native tooltip on the whole chart; the props carry the
49
+ // accessible name as `ariaLabel` instead.
50
+ interface Props
51
+ extends Omit<React.ComponentProps<"div">, "children" | "onSelect" | "title"> {
52
+ readonly definition: ChartDefinition;
53
+ /** What the chart shows, in a sentence: the screen reader's name for it. Required. */
54
+ readonly ariaLabel: string;
55
+ /** The takeaway, when the label alone does not carry it. */
56
+ readonly ariaDescription?: string;
57
+ /** Pixels. Ignored when `aspectRatio` is set. */
58
+ readonly height?: number;
59
+ /** Height as a share of the width: `0.5` is twice as wide as tall. */
60
+ readonly aspectRatio?: number;
61
+ /** Width used on the server and for the first client render, before the container is measured. */
62
+ readonly initialWidth?: number;
63
+ readonly renderer?: ChartRenderer;
64
+ /** `false` turns the tooltip off; a function replaces its content. */
65
+ readonly tooltip?:
66
+ | boolean
67
+ | ((props: ChartTooltipRenderProps) => React.ReactNode);
68
+ /** Shown by default when there is more than one series. */
69
+ readonly legend?: boolean;
70
+ /** The visually hidden table of the values. On by default; turn it off only when the page shows the same table. */
71
+ readonly dataTable?: boolean;
72
+ /** Tween a data update, in milliseconds; `false` snaps. Reduced motion always snaps. */
73
+ readonly animate?: boolean | number;
74
+ /**
75
+ * Zoom a continuous x: the wheel while the chart has focus, + and −,
76
+ * Shift and the arrows to pan, a drag to pan when zoomed, 0 to reset.
77
+ */
78
+ readonly zoom?: boolean;
79
+ /** Select a range of x by dragging or from the keyboard: `brushX({ onBrush })` from `@voila.dev/chart/brush`. */
80
+ readonly brush?: ChartBrush;
81
+ readonly onFocusChange?: (point: ChartPoint | null) => void;
82
+ readonly onSelect?: (point: ChartPoint | null) => void;
83
+ }
84
+
85
+ /** A pointer that moved less than this between down and up clicked rather than dragged. */
86
+ const DRAG_THRESHOLD = 3;
87
+ const ZOOM_STEP = 0.8;
88
+ const PAN_SHARE = 0.1;
89
+
90
+ /**
91
+ * Draws a definition. Everything a reader needs beyond the picture comes with
92
+ * it: keyboard focus on every value, a live region that reads the focused
93
+ * value, a legend that toggles series, and the values as a hidden table.
94
+ */
95
+ export function Chart({
96
+ definition,
97
+ ariaLabel,
98
+ ariaDescription,
99
+ height = DEFAULT_HEIGHT,
100
+ aspectRatio,
101
+ initialWidth = DEFAULT_INITIAL_WIDTH,
102
+ renderer: Renderer = SvgRenderer,
103
+ tooltip = true,
104
+ legend,
105
+ dataTable = true,
106
+ animate = true,
107
+ zoom = false,
108
+ brush: brushBehavior,
109
+ onFocusChange,
110
+ onSelect,
111
+ style,
112
+ ...props
113
+ }: Props) {
114
+ const chartId = React.useId().replaceAll(":", "");
115
+ const { ref, width, node } = useChartWidth(initialWidth);
116
+ const measureText = useTextMeasurer(node);
117
+ const [hidden, setHidden] = React.useState<ReadonlySet<string>>(
118
+ () => new Set(),
119
+ );
120
+ const chartHeight =
121
+ aspectRatio === undefined ? height : Math.round(width * aspectRatio);
122
+
123
+ const baseScene = React.useMemo(
124
+ () =>
125
+ compileChart(definition, {
126
+ width,
127
+ height: chartHeight,
128
+ measureText,
129
+ hiddenSeries: hidden,
130
+ }),
131
+ [definition, width, chartHeight, measureText, hidden],
132
+ );
133
+ const baseX = baseScene.scales.x;
134
+ const fullX = React.useMemo<readonly [number, number] | undefined>(
135
+ () =>
136
+ baseX && baseX.kind !== "band" && baseX.kind !== "point"
137
+ ? [toNumber(baseX.domain[0]), toNumber(baseX.domain[1])]
138
+ : undefined,
139
+ [baseX],
140
+ );
141
+ const viewport = useChartZoom(zoom ? fullX : undefined, definition);
142
+ const scene = React.useMemo(
143
+ () =>
144
+ viewport.domain === null
145
+ ? baseScene
146
+ : compileChart(definition, {
147
+ width,
148
+ height: chartHeight,
149
+ measureText,
150
+ hiddenSeries: hidden,
151
+ xDomain: viewport.domain,
152
+ }),
153
+ [
154
+ baseScene,
155
+ viewport.domain,
156
+ definition,
157
+ width,
158
+ chartHeight,
159
+ measureText,
160
+ hidden,
161
+ ],
162
+ );
163
+ const [brush, setBrush] = React.useState<ChartBrushRange | null>(null);
164
+ const [zoomAnnouncement, setZoomAnnouncement] = React.useState("");
165
+ const drag = React.useRef<{
166
+ readonly startX: number;
167
+ readonly domain: readonly [number, number] | null;
168
+ moved: boolean;
169
+ } | null>(null);
170
+ const stops = React.useMemo(() => focusStops(scene), [scene]);
171
+ const animationTrigger = React.useMemo(
172
+ () => [definition, hidden],
173
+ [definition, hidden],
174
+ );
175
+ const shown = useAnimatedScene(
176
+ scene,
177
+ animationTrigger,
178
+ animate === false ? 0 : animate === true ? DEFAULT_DURATION : animate,
179
+ );
180
+ const messages = messagesFor(scene.locale);
181
+ const { active, handlers } = useChartFocus({
182
+ stops,
183
+ order: scene.focusOrder,
184
+ hitTest: (x, y) => findNearest(scene, stops, x, y),
185
+ onFocusChange,
186
+ onSelect,
187
+ });
188
+
189
+ const xScale = scene.scales.x;
190
+ const xContinuous =
191
+ xScale !== undefined && xScale.kind !== "band" && xScale.kind !== "point";
192
+
193
+ function updateBrush(range: ChartBrushRange | null) {
194
+ setBrush(range);
195
+ brushBehavior?.onBrush(range);
196
+ }
197
+
198
+ function localX(event: React.PointerEvent<HTMLElement>): number {
199
+ return event.clientX - event.currentTarget.getBoundingClientRect().left;
200
+ }
201
+
202
+ function onPointerDown(event: React.PointerEvent<HTMLDivElement>) {
203
+ if (!brushBehavior && !(zoom && viewport.domain !== null)) {
204
+ return;
205
+ }
206
+ event.currentTarget.setPointerCapture(event.pointerId);
207
+ // A drag selects a range, not a value: the hover tooltip gives way.
208
+ handlers.onPointerLeave();
209
+ drag.current = {
210
+ startX: localX(event),
211
+ domain: viewport.domain,
212
+ moved: false,
213
+ };
214
+ }
215
+
216
+ function onPointerMove(event: React.PointerEvent<HTMLDivElement>) {
217
+ const current = drag.current;
218
+ if (current === null || xScale === undefined) {
219
+ handlers.onPointerMove(event);
220
+ return;
221
+ }
222
+ const x = localX(event);
223
+ if (Math.abs(x - current.startX) > DRAG_THRESHOLD) {
224
+ current.moved = true;
225
+ }
226
+ if (!current.moved) return;
227
+ if (brushBehavior) {
228
+ updateBrush(
229
+ ordered(xScale, [xScale.invert(current.startX), xScale.invert(x)]),
230
+ );
231
+ } else if (current.domain !== null) {
232
+ const [from, to] = current.domain;
233
+ const perPixel = (to - from) / Math.max(1, scene.plot.width);
234
+ viewport.pan(
235
+ (current.startX - x) * perPixel -
236
+ ((viewport.domain?.[0] ?? from) - from),
237
+ );
238
+ }
239
+ }
240
+
241
+ function onPointerUp(event: React.PointerEvent<HTMLDivElement>) {
242
+ if (
243
+ drag.current !== null &&
244
+ event.currentTarget.hasPointerCapture(event.pointerId)
245
+ ) {
246
+ event.currentTarget.releasePointerCapture(event.pointerId);
247
+ }
248
+ }
249
+
250
+ function onClick(event: React.MouseEvent<HTMLDivElement>) {
251
+ const dragged = drag.current?.moved === true;
252
+ drag.current = null;
253
+ if (!dragged) {
254
+ handlers.onClick(event);
255
+ }
256
+ }
257
+
258
+ function announceViewport(next: readonly [number, number] | null) {
259
+ if (xScale === undefined) return;
260
+ const [from, to] = next ?? fullX ?? [0, 0];
261
+ const asValue = (value: number) =>
262
+ xScale.kind === "time" ? new Date(value) : value;
263
+ setZoomAnnouncement(
264
+ messages.showing(
265
+ xScale.format(asValue(from)),
266
+ xScale.format(asValue(to)),
267
+ ),
268
+ );
269
+ }
270
+
271
+ function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
272
+ if (zoom && xContinuous && fullX !== undefined) {
273
+ const focused = active?.point.xValue;
274
+ const [from, to] = viewport.domain ?? fullX;
275
+ const center =
276
+ focused === undefined ? (from + to) / 2 : toNumber(focused);
277
+ if (event.key === "+" || event.key === "=") {
278
+ event.preventDefault();
279
+ viewport.zoomAround(center, ZOOM_STEP);
280
+ return;
281
+ }
282
+ if (event.key === "-" || event.key === "_") {
283
+ event.preventDefault();
284
+ viewport.zoomAround(center, 1 / ZOOM_STEP);
285
+ return;
286
+ }
287
+ if (event.key === "0") {
288
+ event.preventDefault();
289
+ viewport.reset();
290
+ return;
291
+ }
292
+ if (
293
+ event.shiftKey &&
294
+ (event.key === "ArrowLeft" || event.key === "ArrowRight")
295
+ ) {
296
+ event.preventDefault();
297
+ viewport.pan(
298
+ (event.key === "ArrowRight" ? 1 : -1) * viewport.span() * PAN_SHARE,
299
+ );
300
+ return;
301
+ }
302
+ }
303
+ handlers.onKeyDown(event);
304
+ }
305
+
306
+ // Moving the focus says the new value; the last zoom's message gives way.
307
+ const activeKey = active?.point.key;
308
+ // biome-ignore lint/correctness/useExhaustiveDependencies: clears on a change of focused point only
309
+ React.useEffect(() => {
310
+ setZoomAnnouncement("");
311
+ }, [activeKey]);
312
+
313
+ const viewportKey = viewport.domain?.join(":") ?? "full";
314
+ const previousViewport = React.useRef(viewportKey);
315
+ // biome-ignore lint/correctness/useExhaustiveDependencies: announce a change of viewport, once
316
+ React.useEffect(() => {
317
+ if (previousViewport.current !== viewportKey) {
318
+ previousViewport.current = viewportKey;
319
+ announceViewport(viewport.domain);
320
+ }
321
+ }, [viewportKey]);
322
+
323
+ // The wheel zooms only while the chart has focus: scrolling past a chart
324
+ // must keep scrolling the page.
325
+ const zoomRef = React.useRef({
326
+ viewport,
327
+ xScale,
328
+ enabled: zoom && xContinuous,
329
+ });
330
+ zoomRef.current = { viewport, xScale, enabled: zoom && xContinuous };
331
+ React.useEffect(() => {
332
+ if (node === null) return;
333
+ function onWheel(event: WheelEvent) {
334
+ const { viewport: current, xScale: scale, enabled } = zoomRef.current;
335
+ if (
336
+ !enabled ||
337
+ scale === undefined ||
338
+ document.activeElement !== node ||
339
+ node === null
340
+ )
341
+ return;
342
+ event.preventDefault();
343
+ const x = event.clientX - node.getBoundingClientRect().left;
344
+ current.zoomAround(
345
+ toNumber(scale.invert(x)),
346
+ event.deltaY > 0 ? 1 / ZOOM_STEP : ZOOM_STEP,
347
+ );
348
+ }
349
+ node.addEventListener("wheel", onWheel, { passive: false });
350
+ return () => node.removeEventListener("wheel", onWheel);
351
+ }, [node]);
352
+
353
+ const announcement =
354
+ active?.source === "keyboard"
355
+ ? `${active.point.title} : ${(active.stop.points.length > 1
356
+ ? active.stop.points
357
+ : [active.point]
358
+ )
359
+ .map((point) => `${point.seriesLabel ?? scene.yLabel} ${point.value}`)
360
+ .join(", ")}`
361
+ : "";
362
+ const showLegend = legend ?? scene.legend.length > 1;
363
+ const descriptionId = `${chartId}-description`;
364
+ const renderTooltip =
365
+ typeof tooltip === "function" ? tooltip : ChartTooltipContent;
366
+
367
+ function toggle(key: string) {
368
+ setHidden((current) => {
369
+ const next = new Set(current);
370
+ if (!next.delete(key)) {
371
+ next.add(key);
372
+ }
373
+ return next;
374
+ });
375
+ }
376
+
377
+ return (
378
+ <div
379
+ data-slot="chart"
380
+ style={{ display: "grid", gap: 8, ...style }}
381
+ {...props}
382
+ >
383
+ <style href="voila-chart" precedence="default">
384
+ {CHART_CSS}
385
+ </style>
386
+ <div
387
+ data-slot="chart-frame"
388
+ style={{ position: "relative", height: chartHeight }}
389
+ >
390
+ <div
391
+ ref={ref}
392
+ role="img"
393
+ aria-roledescription={messages.roleDescription}
394
+ aria-label={ariaLabel}
395
+ aria-describedby={descriptionId}
396
+ // The chart is one focus stop; the arrow keys move inside it, the way
397
+ // a reader walks the values. role="img" keeps it a single named
398
+ // graphic, the live region speaks the focused value.
399
+ // biome-ignore lint/a11y/noNoninteractiveTabindex: keyboard exploration of the values
400
+ tabIndex={0}
401
+ data-slot="chart-surface"
402
+ style={{
403
+ position: "absolute",
404
+ inset: 0,
405
+ touchAction: brushBehavior || zoom ? "none" : "pan-y",
406
+ // Dragging across the plot brushes or pans; without this it
407
+ // also selected the tick labels and the tooltip as text.
408
+ ...(brushBehavior || zoom
409
+ ? { userSelect: "none", WebkitUserSelect: "none" }
410
+ : {}),
411
+ }}
412
+ {...handlers}
413
+ onKeyDown={onKeyDown}
414
+ onPointerDown={onPointerDown}
415
+ onPointerMove={onPointerMove}
416
+ onPointerUp={onPointerUp}
417
+ onClick={onClick}
418
+ >
419
+ <Renderer scene={shown} chartId={chartId} />
420
+ <FocusOverlay scene={scene} active={active} theme={scene.theme} />
421
+ {tooltip !== false && active !== null ? (
422
+ <ChartTooltip
423
+ active={active}
424
+ scene={scene}
425
+ render={renderTooltip}
426
+ />
427
+ ) : null}
428
+ </div>
429
+ {brushBehavior && xScale ? (
430
+ <brushBehavior.Layer
431
+ scale={xScale}
432
+ plot={scene.plot}
433
+ range={brush}
434
+ messages={messages}
435
+ onChange={updateBrush}
436
+ />
437
+ ) : null}
438
+ </div>
439
+ <div id={descriptionId} style={SR_ONLY}>
440
+ {ariaDescription ? `${ariaDescription} ` : ""}
441
+ {messages.keyboardHint}
442
+ {zoom && xContinuous ? messages.zoomHint : ""}
443
+ </div>
444
+ <div aria-live="polite" aria-atomic="true" style={SR_ONLY}>
445
+ {zoomAnnouncement || announcement}
446
+ </div>
447
+ {viewport.domain !== null ? (
448
+ <button
449
+ type="button"
450
+ data-slot="chart-reset-zoom"
451
+ onClick={() => viewport.reset()}
452
+ style={{
453
+ justifySelf: "end",
454
+ border:
455
+ "1px solid var(--border, color-mix(in oklab, currentColor 20%, transparent))",
456
+ borderRadius: 6,
457
+ background: "transparent",
458
+ color: "inherit",
459
+ font: "inherit",
460
+ fontSize: 12,
461
+ padding: "2px 8px",
462
+ cursor: "pointer",
463
+ }}
464
+ >
465
+ {messages.resetZoom}
466
+ </button>
467
+ ) : null}
468
+ {showLegend && scene.legend.length > 0 ? (
469
+ <ChartLegend
470
+ items={scene.legend}
471
+ hidden={hidden}
472
+ label={messages.legend}
473
+ onToggle={toggle}
474
+ />
475
+ ) : null}
476
+ {scene.colorRamp && legend !== false ? (
477
+ <ChartColorRamp ramp={scene.colorRamp} label={messages.legend} />
478
+ ) : null}
479
+ {dataTable ? (
480
+ <ChartDataTable
481
+ scene={scene}
482
+ stops={stops}
483
+ caption={messages.dataTable(ariaLabel)}
484
+ seriesHeading={messages.series}
485
+ />
486
+ ) : null}
487
+ </div>
488
+ );
489
+ }