@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,108 @@
1
+ import { isPlaceable, numericValue, readChannel } from "#/core/channel.ts";
2
+ import { rectY } from "#/core/marks/rect.ts";
3
+ import type { ChartMarkOptions } from "#/core/marks/shared.ts";
4
+ import { tickStep } from "#/core/ticks.ts";
5
+ import type { ChartAccessor, ChartMark, ChartValue } from "#/core/types.ts";
6
+
7
+ export interface HistogramOptions<TDatum> extends ChartMarkOptions<TDatum> {
8
+ readonly x: ChartAccessor<TDatum, ChartValue>;
9
+ /** About how many bins: the edges land on round numbers, so the count is approximate. */
10
+ readonly bins?: number;
11
+ readonly fill?: string;
12
+ /** Each bin's share of the total instead of its count. */
13
+ readonly normalize?: boolean;
14
+ }
15
+
16
+ export interface Bin {
17
+ readonly x1: number;
18
+ readonly x2: number;
19
+ readonly count: number;
20
+ readonly series?: ChartValue;
21
+ }
22
+
23
+ /** Round-edged bins over the values' extent, every value in exactly one. */
24
+ export function binValues(
25
+ values: ReadonlyArray<number>,
26
+ target: number,
27
+ ): ReadonlyArray<{ x1: number; x2: number }> {
28
+ const finite = values.filter(Number.isFinite);
29
+ if (finite.length === 0) {
30
+ return [];
31
+ }
32
+ const low = Math.min(...finite);
33
+ const high = Math.max(...finite);
34
+ const step = tickStep(low, high, target) || 1;
35
+ const first = Math.floor(low / step) * step;
36
+ const count = Math.max(
37
+ 1,
38
+ Math.ceil((high - first) / step + (high === first ? 1 : 0)),
39
+ );
40
+ return Array.from({ length: count }, (_unused, index) => ({
41
+ x1: first + index * step,
42
+ x2: first + (index + 1) * step,
43
+ }));
44
+ }
45
+
46
+ const DEFAULT_BINS = 20;
47
+
48
+ /**
49
+ * Counts per bin, as touching rectangles over a continuous x. With a color
50
+ * channel the series stack inside each bin.
51
+ */
52
+ export function histogram<TDatum>(
53
+ data: ReadonlyArray<TDatum>,
54
+ options: HistogramOptions<TDatum>,
55
+ ): ChartMark {
56
+ const xs = readChannel(data, options.x).map(numericValue);
57
+ const colors = readChannel(data, options.color);
58
+ const edges = binValues(
59
+ xs.filter((value): value is number => value !== undefined),
60
+ options.bins ?? DEFAULT_BINS,
61
+ );
62
+ const seriesValues =
63
+ options.color === undefined
64
+ ? [undefined]
65
+ : [
66
+ ...new Map(
67
+ colors.filter(isPlaceable).map((value) => [String(value), value]),
68
+ ).values(),
69
+ ];
70
+ const total = xs.filter((value) => value !== undefined).length || 1;
71
+ const bins: Bin[] = [];
72
+ for (const series of seriesValues) {
73
+ for (const [binIndex, edge] of edges.entries()) {
74
+ const last = binIndex === edges.length - 1;
75
+ let count = 0;
76
+ for (const [index, x] of xs.entries()) {
77
+ const inSeries =
78
+ series === undefined || String(colors[index]) === String(series);
79
+ if (
80
+ inSeries &&
81
+ x !== undefined &&
82
+ x >= edge.x1 &&
83
+ (x < edge.x2 || (last && x <= edge.x2))
84
+ ) {
85
+ count += 1;
86
+ }
87
+ }
88
+ bins.push({
89
+ ...edge,
90
+ count: options.normalize ? count / total : count,
91
+ series,
92
+ });
93
+ }
94
+ }
95
+ return rectY(bins, {
96
+ id: options.id,
97
+ label: options.label,
98
+ tip: options.tip,
99
+ opacity: options.opacity,
100
+ fill: options.fill,
101
+ x1: (bin) => bin.x1,
102
+ x2: (bin) => bin.x2,
103
+ y: (bin) => bin.count,
104
+ ...(options.color === undefined
105
+ ? {}
106
+ : { color: (bin: Bin) => bin.series as ChartValue }),
107
+ });
108
+ }
@@ -0,0 +1,108 @@
1
+ import { isPlaceable, numericValue, readChannel } from "#/core/channel.ts";
2
+ import { markId, paint } from "#/core/marks/shared.ts";
3
+ import { linearFit } from "#/core/marks/stats/statistics.ts";
4
+ import { areaPath, linePath } from "#/core/paths.ts";
5
+ import { toNumber } from "#/core/scales/continuous.ts";
6
+ import type {
7
+ ChartAccessor,
8
+ ChartMark,
9
+ ChartValue,
10
+ SceneNode,
11
+ } from "#/core/types.ts";
12
+
13
+ export interface RegressionOptions<TDatum> {
14
+ readonly id?: string;
15
+ readonly x: ChartAccessor<TDatum, ChartValue>;
16
+ readonly y: ChartAccessor<TDatum, ChartValue>;
17
+ readonly stroke?: string;
18
+ /** Shade the 95 % confidence band of the fitted mean. */
19
+ readonly band?: boolean;
20
+ readonly label?: string;
21
+ }
22
+
23
+ const BAND_SAMPLES = 24;
24
+
25
+ /** The least-squares line through x and y, and how sure it is. Drawn over a scatter. */
26
+ export function regressionY<TDatum>(
27
+ data: ReadonlyArray<TDatum>,
28
+ options: RegressionOptions<TDatum>,
29
+ ): ChartMark {
30
+ const pairs = readChannel(data, options.x)
31
+ .map((x, index) => ({
32
+ x,
33
+ y: numericValue(readChannel(data, options.y)[index]),
34
+ }))
35
+ .filter(
36
+ (pair): pair is { x: ChartValue; y: number } =>
37
+ isPlaceable(pair.x) && pair.y !== undefined,
38
+ );
39
+ const xs = pairs.map((pair) => toNumber(pair.x));
40
+ const fit = linearFit(
41
+ xs,
42
+ pairs.map((pair) => pair.y),
43
+ );
44
+ const dates = pairs.some((pair) => pair.x instanceof Date);
45
+ return {
46
+ id: options.id,
47
+ coordinate: "cartesian",
48
+ channels: {
49
+ x: { values: pairs.map((pair) => pair.x) },
50
+ y: { values: pairs.map((pair) => pair.y) },
51
+ },
52
+ render(context) {
53
+ const id = markId(options.id, "regressionY", context);
54
+ const { x: xScale, y: yScale } = context.scales;
55
+ if (
56
+ fit === undefined ||
57
+ xScale === undefined ||
58
+ yScale === undefined ||
59
+ xs.length === 0
60
+ ) {
61
+ return { nodes: [] };
62
+ }
63
+ const low = Math.min(...xs);
64
+ const high = Math.max(...xs);
65
+ const at = Array.from(
66
+ { length: BAND_SAMPLES },
67
+ (_unused, index) => low + ((high - low) * index) / (BAND_SAMPLES - 1),
68
+ );
69
+ const place = (x: number) => xScale.map(dates ? new Date(x) : x);
70
+ const stroke = options.stroke ?? context.theme.foreground;
71
+ const nodes: SceneNode[] = [];
72
+ if (options.band !== false) {
73
+ nodes.push({
74
+ kind: "path",
75
+ key: `${id}:band`,
76
+ role: "mark",
77
+ d: areaPath(
78
+ at.map((x) => ({
79
+ x: place(x),
80
+ y: yScale.map(fit.intercept + fit.slope * x + fit.margin(x)),
81
+ })),
82
+ at.map((x) => ({
83
+ x: place(x),
84
+ y: yScale.map(fit.intercept + fit.slope * x - fit.margin(x)),
85
+ })),
86
+ ),
87
+ paint: { fill: stroke, fillOpacity: 0.12 },
88
+ });
89
+ }
90
+ nodes.push({
91
+ kind: "path",
92
+ key: `${id}:line`,
93
+ role: "mark",
94
+ d: linePath([
95
+ { x: place(low), y: yScale.map(fit.intercept + fit.slope * low) },
96
+ { x: place(high), y: yScale.map(fit.intercept + fit.slope * high) },
97
+ ]),
98
+ paint: paint({ fill: "none", stroke, strokeWidth: 2 }),
99
+ });
100
+ return {
101
+ nodes,
102
+ legend: options.label
103
+ ? [{ key: id, label: options.label, color: stroke, shape: "line" }]
104
+ : [],
105
+ };
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The few statistics the statistical marks need, written out: quantiles,
3
+ * kernel density, least squares. Each is a handful of lines, which is less to
4
+ * carry than a dependency.
5
+ */
6
+
7
+ /** Linear-interpolated quantile of sorted values (the R-7 / Excel definition). */
8
+ export function quantile(sorted: ReadonlyArray<number>, p: number): number {
9
+ if (sorted.length === 0) {
10
+ return Number.NaN;
11
+ }
12
+ const position = (sorted.length - 1) * p;
13
+ const below = Math.floor(position);
14
+ const above = Math.ceil(position);
15
+ return sorted[below] + (sorted[above] - sorted[below]) * (position - below);
16
+ }
17
+
18
+ export interface BoxSummary {
19
+ readonly min: number;
20
+ readonly q1: number;
21
+ readonly median: number;
22
+ readonly q3: number;
23
+ readonly max: number;
24
+ /** Whisker ends: the furthest values within 1.5 IQR of the box. */
25
+ readonly low: number;
26
+ readonly high: number;
27
+ readonly outliers: ReadonlyArray<number>;
28
+ readonly count: number;
29
+ }
30
+
31
+ const TUKEY_FENCE = 1.5;
32
+
33
+ export function boxSummary(
34
+ values: ReadonlyArray<number>,
35
+ ): BoxSummary | undefined {
36
+ const sorted = values
37
+ .filter(Number.isFinite)
38
+ .sort((left, right) => left - right);
39
+ if (sorted.length === 0) {
40
+ return undefined;
41
+ }
42
+ const q1 = quantile(sorted, 0.25);
43
+ const q3 = quantile(sorted, 0.75);
44
+ const fence = (q3 - q1) * TUKEY_FENCE;
45
+ const inside = sorted.filter(
46
+ (value) => value >= q1 - fence && value <= q3 + fence,
47
+ );
48
+ return {
49
+ min: sorted[0],
50
+ q1,
51
+ median: quantile(sorted, 0.5),
52
+ q3,
53
+ max: sorted[sorted.length - 1],
54
+ low: inside[0] ?? q1,
55
+ high: inside[inside.length - 1] ?? q3,
56
+ outliers: sorted.filter(
57
+ (value) => value < q1 - fence || value > q3 + fence,
58
+ ),
59
+ count: sorted.length,
60
+ };
61
+ }
62
+
63
+ /** Silverman's rule of thumb: a bandwidth that suits most unimodal data. */
64
+ export function silvermanBandwidth(values: ReadonlyArray<number>): number {
65
+ const count = values.length;
66
+ if (count < 2) {
67
+ return 1;
68
+ }
69
+ const mean = values.reduce((sum, value) => sum + value, 0) / count;
70
+ const deviation = Math.sqrt(
71
+ values.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (count - 1),
72
+ );
73
+ const sorted = [...values].sort((left, right) => left - right);
74
+ const spread = Math.min(
75
+ deviation,
76
+ (quantile(sorted, 0.75) - quantile(sorted, 0.25)) / 1.34,
77
+ );
78
+ return 0.9 * (spread || deviation || 1) * count ** -0.2;
79
+ }
80
+
81
+ const GAUSSIAN = 1 / Math.sqrt(2 * Math.PI);
82
+
83
+ /** Gaussian kernel density of `values`, sampled at `at`. */
84
+ export function kernelDensity(
85
+ values: ReadonlyArray<number>,
86
+ at: ReadonlyArray<number>,
87
+ bandwidth = silvermanBandwidth(values),
88
+ ): ReadonlyArray<number> {
89
+ const count = values.length;
90
+ return at.map((x) => {
91
+ let sum = 0;
92
+ for (const value of values) {
93
+ const u = (x - value) / bandwidth;
94
+ sum += GAUSSIAN * Math.exp(-0.5 * u * u);
95
+ }
96
+ return count === 0 ? 0 : sum / (count * bandwidth);
97
+ });
98
+ }
99
+
100
+ /** `count` evenly spaced samples over `[low, high]`. */
101
+ export function samples(low: number, high: number, count: number): number[] {
102
+ if (count < 2 || low === high) {
103
+ return [low];
104
+ }
105
+ return Array.from(
106
+ { length: count },
107
+ (_unused, index) => low + ((high - low) * index) / (count - 1),
108
+ );
109
+ }
110
+
111
+ export interface LinearFit {
112
+ readonly slope: number;
113
+ readonly intercept: number;
114
+ /** Half-width of the 95 % confidence band of the mean at `x`. */
115
+ readonly margin: (x: number) => number;
116
+ }
117
+
118
+ /** 1.96: a normal approximation of Student's t, fine past a dozen points. */
119
+ const Z_95 = 1.96;
120
+
121
+ export function linearFit(
122
+ xs: ReadonlyArray<number>,
123
+ ys: ReadonlyArray<number>,
124
+ ): LinearFit | undefined {
125
+ const count = xs.length;
126
+ if (count < 2) {
127
+ return undefined;
128
+ }
129
+ const meanX = xs.reduce((sum, x) => sum + x, 0) / count;
130
+ const meanY = ys.reduce((sum, y) => sum + y, 0) / count;
131
+ let sxx = 0;
132
+ let sxy = 0;
133
+ for (let index = 0; index < count; index += 1) {
134
+ sxx += (xs[index] - meanX) ** 2;
135
+ sxy += (xs[index] - meanX) * (ys[index] - meanY);
136
+ }
137
+ if (sxx === 0) {
138
+ return undefined;
139
+ }
140
+ const slope = sxy / sxx;
141
+ const intercept = meanY - slope * meanX;
142
+ let residuals = 0;
143
+ for (let index = 0; index < count; index += 1) {
144
+ residuals += (ys[index] - (intercept + slope * xs[index])) ** 2;
145
+ }
146
+ const standardError = count > 2 ? Math.sqrt(residuals / (count - 2)) : 0;
147
+ return {
148
+ slope,
149
+ intercept,
150
+ margin: (x) =>
151
+ Z_95 * standardError * Math.sqrt(1 / count + (x - meanX) ** 2 / sxx),
152
+ };
153
+ }
@@ -0,0 +1,141 @@
1
+ import {
2
+ channelLabel,
3
+ isPlaceable,
4
+ numericValue,
5
+ readChannel,
6
+ } from "#/core/channel.ts";
7
+ import {
8
+ type ChartMarkOptions,
9
+ colorChannel,
10
+ markId,
11
+ paint,
12
+ seriesResolver,
13
+ } from "#/core/marks/shared.ts";
14
+ import { categoryKey } from "#/core/scales/discrete.ts";
15
+ import type {
16
+ ChartAccessor,
17
+ ChartMark,
18
+ ChartPoint,
19
+ ChartValue,
20
+ SceneNode,
21
+ } from "#/core/types.ts";
22
+
23
+ export interface WaffleOptions<TDatum> extends ChartMarkOptions<TDatum> {
24
+ readonly x: ChartAccessor<TDatum, ChartValue>;
25
+ readonly y: ChartAccessor<TDatum, ChartValue>;
26
+ /** What one cell stands for. */
27
+ readonly unit?: number;
28
+ /** Cells across a band. */
29
+ readonly columns?: number;
30
+ readonly fill?: string;
31
+ }
32
+
33
+ const DEFAULT_COLUMNS = 10;
34
+ const CELL_GAP = 1.5;
35
+
36
+ /**
37
+ * Bars made of countable cells: "12 missions" reads as twelve squares. A
38
+ * row of `columns` cells is `columns × unit` on the y axis, so the waffle
39
+ * still reads against it; series continue one another inside a column.
40
+ */
41
+ export function waffleY<TDatum>(
42
+ data: ReadonlyArray<TDatum>,
43
+ options: WaffleOptions<TDatum>,
44
+ ): ChartMark {
45
+ const xs = readChannel(data, options.x);
46
+ const values = readChannel(data, options.y).map(numericValue);
47
+ const colors = colorChannel(data, options);
48
+ const unit = options.unit ?? 1;
49
+ const columns = options.columns ?? DEFAULT_COLUMNS;
50
+ const totals = new Map<string, number>();
51
+ const offsets = xs.map((x, index) => {
52
+ if (!isPlaceable(x)) return 0;
53
+ const key = categoryKey(x);
54
+ const before = totals.get(key) ?? 0;
55
+ totals.set(
56
+ key,
57
+ before + Math.max(0, Math.round((values[index] ?? 0) / unit)),
58
+ );
59
+ return before;
60
+ });
61
+ const tallest = Math.max(0, ...totals.values());
62
+ return {
63
+ id: options.id,
64
+ coordinate: "cartesian",
65
+ focusOrder: "x",
66
+ channels: {
67
+ x: {
68
+ values: xs.filter(isPlaceable),
69
+ discrete: "band",
70
+ label: channelLabel(options.x),
71
+ },
72
+ y: {
73
+ values: [0, Math.ceil(tallest / columns) * columns * unit],
74
+ includeZero: true,
75
+ label: channelLabel(options.y) ?? options.label,
76
+ },
77
+ color: colors.channel,
78
+ },
79
+ render(context) {
80
+ const id = markId(options.id, "waffleY", context);
81
+ const { x: xScale, y: yScale } = context.scales;
82
+ if (xScale === undefined || yScale === undefined) {
83
+ return { nodes: [] };
84
+ }
85
+ const seriesOf = seriesResolver(
86
+ colors.values,
87
+ { ...options, fixedColor: options.fill },
88
+ id,
89
+ context,
90
+ );
91
+ const cellWidth = xScale.bandwidth / columns;
92
+ const rowHeight = Math.abs(yScale.map(columns * unit) - yScale.map(0));
93
+ const nodes: SceneNode[] = [];
94
+ const points: ChartPoint[] = [];
95
+ for (const [index, x] of xs.entries()) {
96
+ const value = values[index];
97
+ if (!isPlaceable(x) || value === undefined) continue;
98
+ const series = seriesOf(index);
99
+ const cells = Math.max(0, Math.round(value / unit));
100
+ let top = Number.POSITIVE_INFINITY;
101
+ for (let cell = 0; cell < cells; cell += 1) {
102
+ const position = offsets[index] + cell;
103
+ const column = position % columns;
104
+ const row = Math.floor(position / columns);
105
+ const y = yScale.map((row + 1) * columns * unit);
106
+ top = Math.min(top, y);
107
+ nodes.push({
108
+ kind: "rect",
109
+ key: `${id}:${index}:${cell}`,
110
+ series: series.key,
111
+ role: "mark",
112
+ x: xScale.map(x) + column * cellWidth + CELL_GAP / 2,
113
+ y: y + CELL_GAP / 2,
114
+ width: Math.max(0, cellWidth - CELL_GAP),
115
+ height: Math.max(0, rowHeight - CELL_GAP),
116
+ corners: [1.5, 1.5, 1.5, 1.5],
117
+ paint: paint({ fill: series.color, opacity: options.opacity }),
118
+ });
119
+ }
120
+ if (options.tip !== false) {
121
+ points.push({
122
+ key: `${id}:${index}`,
123
+ markId: id,
124
+ index,
125
+ datum: data[index],
126
+ x: xScale.center(x),
127
+ y: Number.isFinite(top) ? top : yScale.map(0),
128
+ xValue: x,
129
+ yValue: value,
130
+ series: series.key,
131
+ seriesLabel: series.label,
132
+ color: series.color,
133
+ title: context.formatX(x),
134
+ value: context.formatY(value),
135
+ });
136
+ }
137
+ }
138
+ return { nodes, points };
139
+ },
140
+ };
141
+ }
@@ -0,0 +1,97 @@
1
+ import { channelLabel, isPlaceable, readChannel } from "#/core/channel.ts";
2
+ import { markId, paint, valueOn } from "#/core/marks/shared.ts";
3
+ import type {
4
+ ChartAccessor,
5
+ ChartMark,
6
+ ChartTextPaint,
7
+ ChartValue,
8
+ SceneNode,
9
+ } from "#/core/types.ts";
10
+
11
+ export interface TextOptions<TDatum> {
12
+ readonly id?: string;
13
+ readonly x?: ChartAccessor<TDatum, ChartValue>;
14
+ readonly y?: ChartAccessor<TDatum, ChartValue>;
15
+ readonly text: ChartAccessor<TDatum, unknown>;
16
+ /** Pixel offsets from the anchor. */
17
+ readonly dx?: number;
18
+ readonly dy?: number;
19
+ readonly fill?: string;
20
+ readonly fontSize?: number;
21
+ readonly fontWeight?: number;
22
+ readonly textAnchor?: ChartTextPaint["textAnchor"];
23
+ readonly baseline?: ChartTextPaint["baseline"];
24
+ readonly opacity?: number;
25
+ }
26
+
27
+ /** A label per datum: values on bars, names on points, annotations. */
28
+ export function text<TDatum>(
29
+ data: ReadonlyArray<TDatum>,
30
+ options: TextOptions<TDatum>,
31
+ ): ChartMark {
32
+ const xs = readChannel(data, options.x);
33
+ const ys = readChannel(data, options.y);
34
+ const texts = readChannel(data, options.text);
35
+ return {
36
+ id: options.id,
37
+ coordinate: "cartesian",
38
+ channels: {
39
+ ...(options.x === undefined
40
+ ? {}
41
+ : {
42
+ x: {
43
+ values: xs.filter(isPlaceable),
44
+ label: channelLabel(options.x),
45
+ },
46
+ }),
47
+ ...(options.y === undefined
48
+ ? {}
49
+ : {
50
+ y: {
51
+ values: ys.filter(isPlaceable),
52
+ label: channelLabel(options.y),
53
+ },
54
+ }),
55
+ },
56
+ render(context) {
57
+ const id = markId(options.id, "text", context);
58
+ const { scales, plot, theme } = context;
59
+ const nodes: SceneNode[] = [];
60
+ for (const [index, content] of texts.entries()) {
61
+ const xValue = scales.x && valueOn(scales.x, xs[index]);
62
+ const yValue = scales.y && valueOn(scales.y, ys[index]);
63
+ const x =
64
+ scales.x && xValue !== undefined
65
+ ? scales.x.center(xValue)
66
+ : plot.x + plot.width / 2;
67
+ const y =
68
+ scales.y && yValue !== undefined
69
+ ? scales.y.center(yValue)
70
+ : plot.y + plot.height / 2;
71
+ if (content === undefined || content === null || content === "") {
72
+ continue;
73
+ }
74
+ nodes.push({
75
+ kind: "text",
76
+ key: `${id}:${index}`,
77
+ role: "label",
78
+ x: x + (options.dx ?? 0),
79
+ y: y + (options.dy ?? 0),
80
+ text:
81
+ typeof content === "number"
82
+ ? context.formatY(content)
83
+ : String(content),
84
+ paint: paint({
85
+ fill: options.fill ?? theme.foreground,
86
+ opacity: options.opacity,
87
+ fontSize: options.fontSize ?? theme.fontSize,
88
+ fontWeight: options.fontWeight,
89
+ textAnchor: options.textAnchor ?? "middle",
90
+ baseline: options.baseline ?? "middle",
91
+ }),
92
+ });
93
+ }
94
+ return { nodes };
95
+ },
96
+ };
97
+ }