@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,425 @@
1
+ import {
2
+ channelLabel,
3
+ isPlaceable,
4
+ numericValue,
5
+ readChannel,
6
+ } from "#/core/channel.ts";
7
+ import { markId, paint } from "#/core/marks/shared.ts";
8
+ import {
9
+ boxSummary,
10
+ kernelDensity,
11
+ samples,
12
+ silvermanBandwidth,
13
+ } from "#/core/marks/stats/statistics.ts";
14
+ import { areaPath, type ChartXY, polygonPath } from "#/core/paths.ts";
15
+ import { categoryKey } from "#/core/scales/discrete.ts";
16
+ import type {
17
+ ChartAccessor,
18
+ ChartMark,
19
+ ChartPoint,
20
+ ChartValue,
21
+ SceneNode,
22
+ } from "#/core/types.ts";
23
+
24
+ /**
25
+ * Distributions per category: the box plot, the violin and the ridgeline.
26
+ * Each groups the values by category first; a category's point (what the
27
+ * keyboard visits) sits on its median.
28
+ */
29
+
30
+ interface DistributionOptions<TDatum> {
31
+ readonly id?: string;
32
+ readonly label?: string;
33
+ readonly fill?: string;
34
+ readonly opacity?: number;
35
+ readonly tip?: boolean;
36
+ readonly category: ChartAccessor<TDatum, ChartValue>;
37
+ readonly value: ChartAccessor<TDatum, ChartValue>;
38
+ }
39
+
40
+ interface Group {
41
+ readonly category: ChartValue;
42
+ readonly values: number[];
43
+ }
44
+
45
+ function groupValues<TDatum>(
46
+ data: ReadonlyArray<TDatum>,
47
+ options: DistributionOptions<TDatum>,
48
+ ): ReadonlyArray<Group> {
49
+ const categories = readChannel(data, options.category);
50
+ const values = readChannel(data, options.value).map(numericValue);
51
+ const groups = new Map<string, Group>();
52
+ for (const [index, category] of categories.entries()) {
53
+ const value = values[index];
54
+ if (!isPlaceable(category) || value === undefined) {
55
+ continue;
56
+ }
57
+ const key = categoryKey(category);
58
+ const group = groups.get(key) ?? { category, values: [] };
59
+ group.values.push(value);
60
+ groups.set(key, group);
61
+ }
62
+ return [...groups.values()];
63
+ }
64
+
65
+ function rangeOf(groups: ReadonlyArray<Group>): ReadonlyArray<number> {
66
+ return groups.flatMap((group) =>
67
+ group.values.length === 0
68
+ ? []
69
+ : [Math.min(...group.values), Math.max(...group.values)],
70
+ );
71
+ }
72
+
73
+ export interface BoxOptions<TDatum> extends DistributionOptions<TDatum> {
74
+ /** Share of the band the box takes. */
75
+ readonly width?: number;
76
+ }
77
+
78
+ const OUTLIER_RADIUS = 2.5;
79
+
80
+ /**
81
+ * Box and whiskers per category: the box from the first to the third
82
+ * quartile, the median across it, whiskers to the furthest values within
83
+ * 1.5 IQR, and the outliers past them as dots.
84
+ */
85
+ export function boxY<TDatum>(
86
+ data: ReadonlyArray<TDatum>,
87
+ options: BoxOptions<TDatum>,
88
+ ): ChartMark {
89
+ const groups = groupValues(data, options);
90
+ const summaries = groups.map((group) => ({
91
+ group,
92
+ summary: boxSummary(group.values),
93
+ }));
94
+ return {
95
+ id: options.id,
96
+ coordinate: "cartesian",
97
+ focusOrder: "x",
98
+ channels: {
99
+ x: {
100
+ values: groups.map((group) => group.category),
101
+ discrete: "band",
102
+ label: channelLabel(options.category),
103
+ },
104
+ y: {
105
+ values: rangeOf(groups),
106
+ label: channelLabel(options.value) ?? options.label,
107
+ },
108
+ },
109
+ render(context) {
110
+ const id = markId(options.id, "boxY", context);
111
+ const { x: xScale, y: yScale } = context.scales;
112
+ if (xScale === undefined || yScale === undefined) {
113
+ return { nodes: [] };
114
+ }
115
+ const color = options.fill ?? context.paletteColor(context.markIndex);
116
+ const width = xScale.bandwidth * (options.width ?? 0.6);
117
+ const nodes: SceneNode[] = [];
118
+ const points: ChartPoint[] = [];
119
+ for (const { group, summary } of summaries) {
120
+ if (summary === undefined) {
121
+ continue;
122
+ }
123
+ const key = categoryKey(group.category);
124
+ const center = xScale.center(group.category);
125
+ const left = center - width / 2;
126
+ const stroke = {
127
+ stroke: color,
128
+ strokeWidth: 1.5,
129
+ opacity: options.opacity,
130
+ };
131
+ const top = yScale.map(summary.q3);
132
+ const bottom = yScale.map(summary.q1);
133
+ nodes.push(
134
+ {
135
+ kind: "line",
136
+ key: `${id}:whisker:${key}`,
137
+ role: "mark",
138
+ x1: center,
139
+ y1: yScale.map(summary.high),
140
+ x2: center,
141
+ y2: yScale.map(summary.low),
142
+ paint: paint(stroke),
143
+ },
144
+ {
145
+ kind: "rect",
146
+ key: `${id}:box:${key}`,
147
+ role: "mark",
148
+ x: left,
149
+ y: Math.min(top, bottom),
150
+ width,
151
+ height: Math.abs(bottom - top),
152
+ corners: [2, 2, 2, 2],
153
+ paint: paint({ ...stroke, fill: color, fillOpacity: 0.25 }),
154
+ },
155
+ {
156
+ kind: "line",
157
+ key: `${id}:median:${key}`,
158
+ role: "mark",
159
+ x1: left,
160
+ y1: yScale.map(summary.median),
161
+ x2: left + width,
162
+ y2: yScale.map(summary.median),
163
+ paint: paint({ ...stroke, strokeWidth: 2.5 }),
164
+ },
165
+ ...summary.outliers.map(
166
+ (value, index): SceneNode => ({
167
+ kind: "circle",
168
+ key: `${id}:outlier:${key}:${index}`,
169
+ role: "mark",
170
+ cx: center,
171
+ cy: yScale.map(value),
172
+ r: OUTLIER_RADIUS,
173
+ paint: paint({ fill: "none", ...stroke }),
174
+ }),
175
+ ),
176
+ );
177
+ if (options.tip !== false) {
178
+ points.push({
179
+ key: `${id}:${key}`,
180
+ markId: id,
181
+ index: groups.indexOf(group),
182
+ datum: summary,
183
+ x: center,
184
+ y: yScale.map(summary.median),
185
+ xValue: group.category,
186
+ yValue: summary.median,
187
+ seriesLabel: options.label,
188
+ color,
189
+ title: context.formatX(group.category),
190
+ value: `${context.formatY(summary.median)} (${context.formatY(summary.q1)} – ${context.formatY(summary.q3)})`,
191
+ hit: {
192
+ kind: "rect",
193
+ rect: {
194
+ x: left,
195
+ y: Math.min(top, bottom),
196
+ width,
197
+ height: Math.abs(bottom - top),
198
+ },
199
+ },
200
+ });
201
+ }
202
+ }
203
+ return { nodes, points };
204
+ },
205
+ };
206
+ }
207
+
208
+ const DENSITY_SAMPLES = 48;
209
+
210
+ /**
211
+ * The density of each category's values, mirrored around its centre: the
212
+ * shape a box plot hides (two peaks, a long tail).
213
+ */
214
+ export function violinY<TDatum>(
215
+ data: ReadonlyArray<TDatum>,
216
+ options: DistributionOptions<TDatum>,
217
+ ): ChartMark {
218
+ const groups = groupValues(data, options);
219
+ const shapes = groups.map((group) => {
220
+ const low = Math.min(...group.values);
221
+ const high = Math.max(...group.values);
222
+ const pad = silvermanBandwidth(group.values);
223
+ const at = samples(low - pad, high + pad, DENSITY_SAMPLES);
224
+ return {
225
+ group,
226
+ at,
227
+ density: kernelDensity(group.values, at),
228
+ summary: boxSummary(group.values),
229
+ };
230
+ });
231
+ const peak = Math.max(0, ...shapes.flatMap((shape) => shape.density));
232
+ return {
233
+ id: options.id,
234
+ coordinate: "cartesian",
235
+ focusOrder: "x",
236
+ channels: {
237
+ x: {
238
+ values: groups.map((group) => group.category),
239
+ discrete: "band",
240
+ label: channelLabel(options.category),
241
+ },
242
+ y: {
243
+ values: shapes.flatMap((shape) => [
244
+ shape.at[0],
245
+ shape.at[shape.at.length - 1],
246
+ ]),
247
+ label: channelLabel(options.value) ?? options.label,
248
+ },
249
+ },
250
+ render(context) {
251
+ const id = markId(options.id, "violinY", context);
252
+ const { x: xScale, y: yScale } = context.scales;
253
+ if (xScale === undefined || yScale === undefined) {
254
+ return { nodes: [] };
255
+ }
256
+ const color = options.fill ?? context.paletteColor(context.markIndex);
257
+ const half = xScale.bandwidth / 2;
258
+ const nodes: SceneNode[] = [];
259
+ const points: ChartPoint[] = [];
260
+ for (const [index, shape] of shapes.entries()) {
261
+ const center = xScale.center(shape.group.category);
262
+ const width = (value: number) =>
263
+ peak === 0 ? 0 : (value / peak) * half;
264
+ const right: ChartXY[] = shape.at.map((value, sample) => ({
265
+ x: center + width(shape.density[sample]),
266
+ y: yScale.map(value),
267
+ }));
268
+ const left: ChartXY[] = shape.at
269
+ .map((value, sample) => ({
270
+ x: center - width(shape.density[sample]),
271
+ y: yScale.map(value),
272
+ }))
273
+ .reverse();
274
+ const key = categoryKey(shape.group.category);
275
+ nodes.push({
276
+ kind: "path",
277
+ key: `${id}:${key}`,
278
+ role: "mark",
279
+ d: polygonPath([...right, ...left]),
280
+ paint: paint({
281
+ fill: color,
282
+ fillOpacity: 0.3,
283
+ stroke: color,
284
+ strokeWidth: 1.5,
285
+ opacity: options.opacity,
286
+ }),
287
+ });
288
+ const summary = shape.summary;
289
+ if (summary === undefined) {
290
+ continue;
291
+ }
292
+ nodes.push({
293
+ kind: "line",
294
+ key: `${id}:median:${key}`,
295
+ role: "mark",
296
+ x1: center - half * 0.25,
297
+ y1: yScale.map(summary.median),
298
+ x2: center + half * 0.25,
299
+ y2: yScale.map(summary.median),
300
+ paint: { stroke: color, strokeWidth: 2.5 },
301
+ });
302
+ if (options.tip !== false) {
303
+ points.push({
304
+ key: `${id}:${key}`,
305
+ markId: id,
306
+ index,
307
+ datum: summary,
308
+ x: center,
309
+ y: yScale.map(summary.median),
310
+ xValue: shape.group.category,
311
+ yValue: summary.median,
312
+ seriesLabel: options.label,
313
+ color,
314
+ title: context.formatX(shape.group.category),
315
+ value: `${context.formatY(summary.median)} (${context.formatY(summary.q1)} – ${context.formatY(summary.q3)})`,
316
+ });
317
+ }
318
+ }
319
+ return { nodes, points };
320
+ },
321
+ };
322
+ }
323
+
324
+ const DEFAULT_OVERLAP = 1.6;
325
+
326
+ export interface RidgelineOptions<TDatum> extends DistributionOptions<TDatum> {
327
+ /** How far each ridge may rise into the row above: 1 stays in its row. */
328
+ readonly overlap?: number;
329
+ }
330
+
331
+ /**
332
+ * One density per row, stacked down the chart and allowed to overlap the row
333
+ * above: many distributions compared along one shared x axis.
334
+ */
335
+ export function ridgeline<TDatum>(
336
+ data: ReadonlyArray<TDatum>,
337
+ options: RidgelineOptions<TDatum>,
338
+ ): ChartMark {
339
+ const overlap = options.overlap ?? DEFAULT_OVERLAP;
340
+ const groups = groupValues(data, options);
341
+ const all = groups.flatMap((group) => group.values);
342
+ const low = Math.min(...all);
343
+ const high = Math.max(...all);
344
+ const pad = silvermanBandwidth(all);
345
+ const at = samples(low - pad, high + pad, DENSITY_SAMPLES * 2);
346
+ const shapes = groups.map((group) => ({
347
+ group,
348
+ density: kernelDensity(group.values, at, silvermanBandwidth(group.values)),
349
+ }));
350
+ const peak = Math.max(0, ...shapes.flatMap((shape) => shape.density));
351
+ return {
352
+ id: options.id,
353
+ coordinate: "cartesian",
354
+ focusOrder: "y",
355
+ channels: {
356
+ x: {
357
+ values: all.length === 0 ? [] : [at[0], at[at.length - 1]],
358
+ label: channelLabel(options.value) ?? options.label,
359
+ },
360
+ y: {
361
+ values: groups.map((group) => group.category),
362
+ discrete: "band",
363
+ padding: 0,
364
+ // Room above the first row for its ridge to rise into.
365
+ paddingOuter: Math.max(0, overlap - 1),
366
+ label: channelLabel(options.category),
367
+ },
368
+ },
369
+ render(context) {
370
+ const id = markId(options.id, "ridgeline", context);
371
+ const { x: xScale, y: yScale } = context.scales;
372
+ if (xScale === undefined || yScale === undefined) {
373
+ return { nodes: [] };
374
+ }
375
+ const rise = yScale.bandwidth * overlap;
376
+ const nodes: SceneNode[] = [];
377
+ const points: ChartPoint[] = [];
378
+ for (const [index, shape] of shapes.entries()) {
379
+ const color = options.fill ?? context.paletteColor(index);
380
+ const baseline = yScale.map(shape.group.category) + yScale.bandwidth;
381
+ const top = at.map((value, sample) => ({
382
+ x: xScale.map(value),
383
+ y:
384
+ baseline - (peak === 0 ? 0 : (shape.density[sample] / peak) * rise),
385
+ }));
386
+ const bottom = at.map((value) => ({
387
+ x: xScale.map(value),
388
+ y: baseline,
389
+ }));
390
+ const key = categoryKey(shape.group.category);
391
+ nodes.push({
392
+ kind: "path",
393
+ key: `${id}:${key}`,
394
+ role: "mark",
395
+ d: areaPath(top, bottom, "monotone"),
396
+ paint: paint({
397
+ fill: color,
398
+ fillOpacity: 0.75,
399
+ stroke: context.theme.background,
400
+ strokeWidth: 1,
401
+ opacity: options.opacity,
402
+ }),
403
+ });
404
+ const summary = boxSummary(shape.group.values);
405
+ if (options.tip !== false && summary !== undefined) {
406
+ points.push({
407
+ key: `${id}:${key}`,
408
+ markId: id,
409
+ index,
410
+ datum: summary,
411
+ x: xScale.map(summary.median),
412
+ y: baseline - yScale.bandwidth / 2,
413
+ xValue: summary.median,
414
+ yValue: shape.group.category,
415
+ seriesLabel: options.label,
416
+ color,
417
+ title: context.formatY(shape.group.category),
418
+ value: `${context.formatX(summary.median)} (${context.formatX(summary.q1)} – ${context.formatX(summary.q3)})`,
419
+ });
420
+ }
421
+ }
422
+ return { nodes, points };
423
+ },
424
+ };
425
+ }
@@ -0,0 +1,136 @@
1
+ import { channelLabel, isPlaceable, readChannel } from "#/core/channel.ts";
2
+ import {
3
+ type ChartMarkOptions,
4
+ colorChannel,
5
+ markId,
6
+ paint,
7
+ seriesResolver,
8
+ valueOn,
9
+ } from "#/core/marks/shared.ts";
10
+ import type {
11
+ ChartAccessor,
12
+ ChartMark,
13
+ ChartPoint,
14
+ ChartValue,
15
+ SceneNode,
16
+ } from "#/core/types.ts";
17
+
18
+ export interface DodgeOptions<TDatum> extends ChartMarkOptions<TDatum> {
19
+ readonly x: ChartAccessor<TDatum, ChartValue>;
20
+ readonly r?: number;
21
+ readonly fill?: string;
22
+ /** Grow from the middle of the plot, or up from its bottom. */
23
+ readonly anchor?: "middle" | "bottom";
24
+ }
25
+
26
+ const DEFAULT_RADIUS = 4;
27
+ const SPACING = 1;
28
+
29
+ /**
30
+ * A beeswarm: one dot per datum along x, nudged up or down just enough not
31
+ * to overlap, so a crowd of values shows its shape and every value stays
32
+ * visible and focusable.
33
+ */
34
+ export function dodgeY<TDatum>(
35
+ data: ReadonlyArray<TDatum>,
36
+ options: DodgeOptions<TDatum>,
37
+ ): ChartMark {
38
+ const xs = readChannel(data, options.x);
39
+ const colors = colorChannel(data, options);
40
+ const r = options.r ?? DEFAULT_RADIUS;
41
+ return {
42
+ id: options.id,
43
+ coordinate: "cartesian",
44
+ focusOrder: "point",
45
+ channels: {
46
+ x: {
47
+ values: xs.filter(isPlaceable),
48
+ label: channelLabel(options.x) ?? options.label,
49
+ inset: r,
50
+ },
51
+ color: colors.channel,
52
+ },
53
+ render(context) {
54
+ const id = markId(options.id, "dodgeY", context);
55
+ const xScale = context.scales.x;
56
+ if (xScale === undefined) {
57
+ return { nodes: [] };
58
+ }
59
+ const { plot } = context;
60
+ const seriesOf = seriesResolver(
61
+ colors.values,
62
+ { ...options, fixedColor: options.fill },
63
+ id,
64
+ context,
65
+ );
66
+ const order = xs
67
+ .map((raw, index) => ({ index, value: valueOn(xScale, raw) }))
68
+ .filter(
69
+ (entry): entry is { index: number; value: ChartValue } =>
70
+ entry.value !== undefined,
71
+ )
72
+ .map((entry) => ({ ...entry, x: xScale.center(entry.value) }))
73
+ .sort((left, right) => left.x - right.x);
74
+ const placed: Array<{ x: number; y: number }> = [];
75
+ const diameter = r * 2 + SPACING;
76
+ const bottom = options.anchor === "bottom";
77
+ const baseline = bottom
78
+ ? plot.y + plot.height - r
79
+ : plot.y + plot.height / 2;
80
+ const nodes: SceneNode[] = [];
81
+ const points: ChartPoint[] = [];
82
+ for (const entry of order) {
83
+ const near = placed.filter(
84
+ (other) => Math.abs(other.x - entry.x) < diameter,
85
+ );
86
+ let offset = 0;
87
+ // Try 0, +1, −1, +2, −2 half-diameters (only upwards from a bottom anchor).
88
+ for (let step = 0; ; step += 1) {
89
+ const magnitude = Math.ceil(step / (bottom ? 1 : 2)) * (diameter / 2);
90
+ offset = bottom || step % 2 === 1 ? -magnitude : magnitude;
91
+ const y = baseline + offset;
92
+ if (
93
+ near.every(
94
+ (other) => Math.hypot(other.x - entry.x, other.y - y) >= diameter,
95
+ )
96
+ )
97
+ break;
98
+ }
99
+ const y = baseline + offset;
100
+ placed.push({ x: entry.x, y });
101
+ const series = seriesOf(entry.index);
102
+ nodes.push({
103
+ kind: "circle",
104
+ key: `${id}:${entry.index}`,
105
+ series: series.key,
106
+ role: "mark",
107
+ cx: entry.x,
108
+ cy: y,
109
+ r,
110
+ paint: paint({ fill: series.color, opacity: options.opacity }),
111
+ });
112
+ if (options.tip !== false) {
113
+ points.push({
114
+ key: `${id}:${entry.index}`,
115
+ markId: id,
116
+ index: entry.index,
117
+ datum: data[entry.index],
118
+ x: entry.x,
119
+ y,
120
+ xValue: entry.value,
121
+ series: series.key,
122
+ seriesLabel: series.label,
123
+ color: series.color,
124
+ title: series.label ?? context.formatX(entry.value),
125
+ value: context.formatX(entry.value),
126
+ hit: {
127
+ kind: "rect",
128
+ rect: { x: entry.x - r, y: y - r, width: r * 2, height: r * 2 },
129
+ },
130
+ });
131
+ }
132
+ }
133
+ return { nodes, points };
134
+ },
135
+ };
136
+ }
@@ -0,0 +1,152 @@
1
+ import { channelLabel, isPlaceable, readChannel } from "#/core/channel.ts";
2
+ import { markId, valueOn } from "#/core/marks/shared.ts";
3
+ import { polygonPath } from "#/core/paths.ts";
4
+ import type {
5
+ ChartAccessor,
6
+ ChartMark,
7
+ ChartPoint,
8
+ ChartValue,
9
+ SceneNode,
10
+ } from "#/core/types.ts";
11
+
12
+ export interface HexbinOptions<TDatum> {
13
+ readonly id?: string;
14
+ readonly x: ChartAccessor<TDatum, ChartValue>;
15
+ readonly y: ChartAccessor<TDatum, ChartValue>;
16
+ /** Pixel radius of a hexagon. */
17
+ readonly radius?: number;
18
+ /** Colour of the fullest bin; emptier bins fade towards the background. */
19
+ readonly fill?: string;
20
+ readonly label?: string;
21
+ }
22
+
23
+ const DEFAULT_RADIUS = 10;
24
+ const SQRT3 = Math.sqrt(3);
25
+
26
+ /**
27
+ * The centre of the hexagon a pixel falls in, on a pointy-top grid whose odd
28
+ * rows are offset by half a hexagon. Near a slanted edge the nearest row is
29
+ * not always the right one, so the closer of the two candidate centres wins.
30
+ */
31
+ export function hexCenter(
32
+ x: number,
33
+ y: number,
34
+ radius: number,
35
+ ): { x: number; y: number } {
36
+ const dx = radius * SQRT3;
37
+ const dy = radius * 1.5;
38
+ const row = Math.round(y / dy);
39
+ const neighbourRow = y / dy > row ? row + 1 : row - 1;
40
+ const centres = [row, neighbourRow].map((candidate) => {
41
+ const shift = (candidate & 1) / 2;
42
+ return { x: (Math.round(x / dx - shift) + shift) * dx, y: candidate * dy };
43
+ });
44
+ return Math.hypot(centres[0].x - x, centres[0].y - y) <=
45
+ Math.hypot(centres[1].x - x, centres[1].y - y)
46
+ ? centres[0]
47
+ : centres[1];
48
+ }
49
+
50
+ function hexagon(cx: number, cy: number, radius: number) {
51
+ return Array.from({ length: 6 }, (_unused, index) => {
52
+ const angle = (Math.PI / 3) * index + Math.PI / 6;
53
+ return {
54
+ x: cx + radius * Math.cos(angle),
55
+ y: cy + radius * Math.sin(angle),
56
+ };
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Points counted into hexagons: a scatter too dense to read as dots. The
62
+ * bins are found in pixels, so they stay hexagonal whatever the scales.
63
+ */
64
+ export function hexbin<TDatum>(
65
+ data: ReadonlyArray<TDatum>,
66
+ options: HexbinOptions<TDatum>,
67
+ ): ChartMark {
68
+ const xs = readChannel(data, options.x);
69
+ const ys = readChannel(data, options.y);
70
+ const reach = options.radius ?? DEFAULT_RADIUS;
71
+ return {
72
+ id: options.id,
73
+ coordinate: "cartesian",
74
+ focusOrder: "point",
75
+ channels: {
76
+ x: {
77
+ values: xs.filter(isPlaceable),
78
+ label: channelLabel(options.x),
79
+ inset: reach,
80
+ },
81
+ y: {
82
+ values: ys.filter(isPlaceable),
83
+ label: channelLabel(options.y),
84
+ inset: reach,
85
+ },
86
+ },
87
+ render(context) {
88
+ const id = markId(options.id, "hexbin", context);
89
+ const { x: xScale, y: yScale } = context.scales;
90
+ if (xScale === undefined || yScale === undefined) {
91
+ return { nodes: [] };
92
+ }
93
+ const radius = options.radius ?? DEFAULT_RADIUS;
94
+ const bins = new Map<
95
+ string,
96
+ { x: number; y: number; members: number[] }
97
+ >();
98
+ for (const [index, raw] of xs.entries()) {
99
+ const x = valueOn(xScale, raw);
100
+ const y = valueOn(yScale, ys[index]);
101
+ if (x === undefined || y === undefined) continue;
102
+ const center = hexCenter(xScale.center(x), yScale.center(y), radius);
103
+ const key = `${Math.round(center.x)}:${Math.round(center.y)}`;
104
+ const bin = bins.get(key) ?? { ...center, members: [] };
105
+ bin.members.push(index);
106
+ bins.set(key, bin);
107
+ }
108
+ const fullest = Math.max(
109
+ 1,
110
+ ...[...bins.values()].map((bin) => bin.members.length),
111
+ );
112
+ const color = options.fill ?? context.paletteColor(context.markIndex);
113
+ const nodes: SceneNode[] = [];
114
+ const points: ChartPoint[] = [];
115
+ for (const [key, bin] of bins) {
116
+ const share = bin.members.length / fullest;
117
+ const fill = `color-mix(in oklab, ${color} ${Math.round(15 + 85 * share)}%, ${context.theme.background})`;
118
+ nodes.push({
119
+ kind: "path",
120
+ key: `${id}:${key}`,
121
+ role: "mark",
122
+ d: polygonPath(hexagon(bin.x, bin.y, radius - 0.5)),
123
+ paint: { fill },
124
+ });
125
+ points.push({
126
+ key: `${id}:${key}`,
127
+ markId: id,
128
+ index: bin.members[0],
129
+ datum: bin.members.map((index) => data[index]),
130
+ x: bin.x,
131
+ y: bin.y,
132
+ xValue: xScale.invert(bin.x),
133
+ yValue: yScale.invert(bin.y),
134
+ seriesLabel: options.label,
135
+ color: fill,
136
+ title: `${context.formatX(xScale.invert(bin.x))} · ${context.formatY(yScale.invert(bin.y))}`,
137
+ value: String(bin.members.length),
138
+ hit: {
139
+ kind: "rect",
140
+ rect: {
141
+ x: bin.x - radius,
142
+ y: bin.y - radius,
143
+ width: radius * 2,
144
+ height: radius * 2,
145
+ },
146
+ },
147
+ });
148
+ }
149
+ return { nodes, points };
150
+ },
151
+ };
152
+ }