@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,157 @@
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
+ valueOn,
14
+ } from "#/core/marks/shared.ts";
15
+ import type {
16
+ ChartAccessor,
17
+ ChartMark,
18
+ ChartPoint,
19
+ ChartValue,
20
+ SceneNode,
21
+ } from "#/core/types.ts";
22
+
23
+ export interface DotOptions<TDatum> extends ChartMarkOptions<TDatum> {
24
+ readonly x?: ChartAccessor<TDatum, ChartValue>;
25
+ readonly y?: ChartAccessor<TDatum, ChartValue>;
26
+ /** A radius in pixels, or a field whose values set the area of each dot. */
27
+ readonly r?: number | ChartAccessor<TDatum, ChartValue>;
28
+ /** The largest radius a sized dot reaches. */
29
+ readonly maxRadius?: number;
30
+ readonly fill?: string;
31
+ readonly fillOpacity?: number;
32
+ readonly stroke?: string;
33
+ readonly strokeWidth?: number;
34
+ }
35
+
36
+ const DEFAULT_RADIUS = 4;
37
+ const DEFAULT_MAX_RADIUS = 16;
38
+
39
+ /** A dot per datum: a scatter plot, or a bubble chart when `r` is a field. */
40
+ export function dot<TDatum>(
41
+ data: ReadonlyArray<TDatum>,
42
+ options: DotOptions<TDatum> = {},
43
+ ): ChartMark {
44
+ const xs = readChannel(
45
+ data,
46
+ options.x ?? ((_datum: TDatum, index: number) => index),
47
+ );
48
+ const ys = readChannel(
49
+ data,
50
+ options.y ?? ((datum: TDatum) => datum as unknown as ChartValue),
51
+ );
52
+ const colors = colorChannel(data, options);
53
+ const sized = options.r !== undefined && typeof options.r !== "number";
54
+ const sizes = sized
55
+ ? readChannel(data, options.r as ChartAccessor<TDatum, ChartValue>).map(
56
+ numericValue,
57
+ )
58
+ : [];
59
+ const largest = Math.max(0, ...sizes.map((size) => Math.abs(size ?? 0)));
60
+ const reach =
61
+ typeof options.r === "number"
62
+ ? options.r
63
+ : sized
64
+ ? (options.maxRadius ?? DEFAULT_MAX_RADIUS)
65
+ : DEFAULT_RADIUS;
66
+
67
+ return {
68
+ id: options.id,
69
+ coordinate: "cartesian",
70
+ focusOrder: "point",
71
+ channels: {
72
+ x: {
73
+ values: xs.filter(isPlaceable),
74
+ label: channelLabel(options.x),
75
+ inset: reach,
76
+ },
77
+ y: {
78
+ values: ys.filter(isPlaceable),
79
+ label: channelLabel(options.y) ?? options.label,
80
+ inset: reach,
81
+ },
82
+ color: colors.channel,
83
+ },
84
+ render(context) {
85
+ const id = markId(options.id, "dot", context);
86
+ const { x: xScale, y: yScale } = context.scales;
87
+ if (xScale === undefined || yScale === undefined) {
88
+ return { nodes: [] };
89
+ }
90
+ const seriesOf = seriesResolver(
91
+ colors.values,
92
+ { ...options, fixedColor: options.fill },
93
+ id,
94
+ context,
95
+ );
96
+ const maxRadius = options.maxRadius ?? DEFAULT_MAX_RADIUS;
97
+ // Area, not radius, carries the value: a dot twice the value looks twice as big.
98
+ function radiusOf(index: number): number {
99
+ if (typeof options.r === "number") return options.r;
100
+ if (!sized) return DEFAULT_RADIUS;
101
+ const size = Math.abs(sizes[index] ?? 0);
102
+ return largest === 0 ? 0 : Math.sqrt(size / largest) * maxRadius;
103
+ }
104
+ const nodes: SceneNode[] = [];
105
+ const points: ChartPoint[] = [];
106
+ for (const [index, datum] of data.entries()) {
107
+ const xValue = valueOn(xScale, xs[index]);
108
+ const yValue = valueOn(yScale, ys[index]);
109
+ if (xValue === undefined || yValue === undefined) {
110
+ continue;
111
+ }
112
+ const series = seriesOf(index);
113
+ const cx = xScale.center(xValue);
114
+ const cy = yScale.center(yValue);
115
+ const r = radiusOf(index);
116
+ nodes.push({
117
+ kind: "circle",
118
+ key: `${id}:${index}`,
119
+ series: series.key,
120
+ role: "mark",
121
+ cx,
122
+ cy,
123
+ r,
124
+ paint: paint({
125
+ fill: series.color,
126
+ fillOpacity: options.fillOpacity,
127
+ stroke: options.stroke,
128
+ strokeWidth: options.strokeWidth,
129
+ opacity: options.opacity,
130
+ }),
131
+ });
132
+ if (options.tip !== false) {
133
+ points.push({
134
+ key: `${id}:${index}`,
135
+ markId: id,
136
+ index,
137
+ datum,
138
+ x: cx,
139
+ y: cy,
140
+ xValue,
141
+ yValue,
142
+ series: series.key,
143
+ seriesLabel: series.label,
144
+ color: series.color,
145
+ title: context.formatX(xValue),
146
+ value: context.formatY(yValue),
147
+ hit: {
148
+ kind: "rect",
149
+ rect: { x: cx - r, y: cy - r, width: r * 2, height: r * 2 },
150
+ },
151
+ });
152
+ }
153
+ }
154
+ return { nodes, points };
155
+ },
156
+ };
157
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Funnel geometry, worked out away from React so it can be asserted on
3
+ * directly. One slice per step: a horizontal band whose left edge is as tall
4
+ * as the step's value and whose right edge slopes down to the next step's
5
+ * value, so the drop between two steps is visible as the flank itself.
6
+ */
7
+
8
+ export interface FunnelSlice {
9
+ /** Index of the datum, i.e. of the category slot. */
10
+ readonly index: number;
11
+ readonly value: number;
12
+ /** Left edge of the band, in plot coordinates. */
13
+ readonly x: number;
14
+ readonly width: number;
15
+ /** Height of the left flank, proportional to the step's value. */
16
+ readonly leftHeight: number;
17
+ /** Height of the right flank — the next step's value, or its own on the last step. */
18
+ readonly rightHeight: number;
19
+ /** The trapezoid, vertically centred on the band. `""` when there is nothing to draw. */
20
+ readonly path: string;
21
+ /** Conversion into the next step. `null` on the last step or when this one is empty. */
22
+ readonly ratioToNext: number | null;
23
+ }
24
+
25
+ export interface FunnelLayoutOptions {
26
+ readonly values: ReadonlyArray<number>;
27
+ readonly innerWidth: number;
28
+ readonly innerHeight: number;
29
+ /** Pixels of blank between two bands. */
30
+ readonly gap?: number;
31
+ /** Vertical centre line of the funnel. Defaults to half the inner height. */
32
+ readonly centerY?: number;
33
+ /** Height the largest value maps to. Defaults to the inner height. */
34
+ readonly maxHeight?: number;
35
+ /** Left edge of the first band. */
36
+ readonly originX?: number;
37
+ }
38
+
39
+ const DEFAULT_GAP = 4;
40
+ const PATH_PRECISION = 2;
41
+
42
+ function round(value: number): number {
43
+ const factor = 10 ** PATH_PRECISION;
44
+ return Math.round(value * factor) / factor;
45
+ }
46
+
47
+ function point(x: number, y: number): string {
48
+ return `${round(x)},${round(y)}`;
49
+ }
50
+
51
+ function trapezoidPath(
52
+ x: number,
53
+ width: number,
54
+ centerY: number,
55
+ leftHeight: number,
56
+ rightHeight: number,
57
+ ): string {
58
+ if (width <= 0 || (leftHeight <= 0 && rightHeight <= 0)) {
59
+ return "";
60
+ }
61
+ const right = x + width;
62
+ return [
63
+ `M${point(x, centerY - leftHeight / 2)}`,
64
+ `L${point(right, centerY - rightHeight / 2)}`,
65
+ `L${point(right, centerY + rightHeight / 2)}`,
66
+ `L${point(x, centerY + leftHeight / 2)}`,
67
+ "Z",
68
+ ].join("");
69
+ }
70
+
71
+ export function funnelSlices(
72
+ options: FunnelLayoutOptions,
73
+ ): ReadonlyArray<FunnelSlice> {
74
+ const {
75
+ values,
76
+ innerWidth,
77
+ innerHeight,
78
+ gap = DEFAULT_GAP,
79
+ centerY = innerHeight / 2,
80
+ maxHeight = innerHeight,
81
+ originX = 0,
82
+ } = options;
83
+
84
+ const count = values.length;
85
+ if (count === 0 || innerWidth <= 0 || maxHeight <= 0) {
86
+ return [];
87
+ }
88
+
89
+ const clamped = values.map((value) => Math.max(0, value));
90
+ const peak = Math.max(...clamped);
91
+ const heightFor = (value: number): number =>
92
+ peak <= 0 ? 0 : (value / peak) * maxHeight;
93
+
94
+ const bandWidth = Math.max(0, (innerWidth - gap * (count - 1)) / count);
95
+
96
+ return clamped.map((value, index) => {
97
+ const next = clamped[index + 1];
98
+ const leftHeight = heightFor(value);
99
+ // The last band has no next step to taper into, so it stays a rectangle.
100
+ const rightHeight = next === undefined ? leftHeight : heightFor(next);
101
+ const x = originX + index * (bandWidth + gap);
102
+ return {
103
+ index,
104
+ value: values[index],
105
+ x,
106
+ width: bandWidth,
107
+ leftHeight,
108
+ rightHeight,
109
+ path: trapezoidPath(x, bandWidth, centerY, leftHeight, rightHeight),
110
+ ratioToNext: next === undefined || value <= 0 ? null : next / value,
111
+ };
112
+ });
113
+ }
@@ -0,0 +1,172 @@
1
+ import {
2
+ channelLabel,
3
+ isPlaceable,
4
+ numericValue,
5
+ readChannel,
6
+ } from "#/core/channel.ts";
7
+ import { formatPercentage } from "#/core/format.ts";
8
+ import { funnelSlices } from "#/core/marks/funnel-layout.ts";
9
+ import {
10
+ type ChartMarkOptions,
11
+ markId,
12
+ paint,
13
+ seriesResolver,
14
+ } from "#/core/marks/shared.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
+ export interface FunnelOptions<TDatum> extends ChartMarkOptions<TDatum> {
25
+ /** The step: "Visites", "Inscriptions", "Missions". */
26
+ readonly category: ChartAccessor<TDatum, ChartValue>;
27
+ readonly value: ChartAccessor<TDatum, ChartValue>;
28
+ /** Pixels of blank between two steps. */
29
+ readonly gap?: number;
30
+ readonly fill?: string;
31
+ /** `false` drops the step name and value written above each step. */
32
+ readonly labels?: boolean;
33
+ }
34
+
35
+ /** Room kept above the shape for each step's name and value. */
36
+ const LABEL_ZONE = 36;
37
+
38
+ /**
39
+ * Steps left to right, each as tall as its value, each sloping down to the
40
+ * next: the drop between two stages is the shape itself. The tooltip gives
41
+ * the conversion from the step before.
42
+ */
43
+ export function funnel<TDatum>(
44
+ data: ReadonlyArray<TDatum>,
45
+ options: FunnelOptions<TDatum>,
46
+ ): ChartMark {
47
+ const categories = readChannel(data, options.category);
48
+ const values = readChannel(data, options.value).map(
49
+ (value) => numericValue(value) ?? 0,
50
+ );
51
+ const colorAccessor = options.color ?? options.category;
52
+ const colors = readChannel(data, colorAccessor);
53
+ return {
54
+ id: options.id,
55
+ coordinate: "frame",
56
+ focusOrder: "point",
57
+ colorLegend: options.labels === false,
58
+ titles: {
59
+ category: channelLabel(options.category),
60
+ value: channelLabel(options.value) ?? options.label,
61
+ },
62
+ channels: {
63
+ color: {
64
+ values: colors.filter(isPlaceable),
65
+ label: channelLabel(colorAccessor),
66
+ },
67
+ },
68
+ render(context) {
69
+ const id = markId(options.id, "funnel", context);
70
+ const { plot, theme } = context;
71
+ const labelled = options.labels !== false;
72
+ const top = plot.y + (labelled ? LABEL_ZONE : 0);
73
+ const height = Math.max(0, plot.height - (labelled ? LABEL_ZONE : 0));
74
+ const slices = funnelSlices({
75
+ values,
76
+ innerWidth: plot.width,
77
+ innerHeight: height,
78
+ centerY: top + height / 2,
79
+ originX: plot.x,
80
+ gap: options.gap,
81
+ });
82
+ const seriesOf = seriesResolver(
83
+ colors,
84
+ { ...options, color: colorAccessor, fixedColor: options.fill },
85
+ id,
86
+ context,
87
+ );
88
+ const nodes: SceneNode[] = [];
89
+ const points: ChartPoint[] = [];
90
+ for (const slice of slices) {
91
+ const category = categories[slice.index];
92
+ if (!isPlaceable(category) || slice.path === "") {
93
+ continue;
94
+ }
95
+ const series = seriesOf(slice.index);
96
+ const key = categoryKey(category);
97
+ nodes.push({
98
+ kind: "path",
99
+ key: `${id}:${key}`,
100
+ series: series.key,
101
+ role: "mark",
102
+ d: slice.path,
103
+ paint: paint({ fill: series.color, opacity: options.opacity }),
104
+ });
105
+ if (labelled) {
106
+ nodes.push(
107
+ {
108
+ kind: "text",
109
+ key: `${id}:name:${key}`,
110
+ role: "label",
111
+ x: slice.x,
112
+ y: plot.y,
113
+ text: series.label ?? context.formatX(category),
114
+ paint: {
115
+ fill: theme.muted,
116
+ fontSize: theme.fontSize,
117
+ baseline: "top",
118
+ },
119
+ },
120
+ {
121
+ kind: "text",
122
+ key: `${id}:value:${key}`,
123
+ role: "label",
124
+ x: slice.x,
125
+ y: plot.y + theme.fontSize + 4,
126
+ text: context.formatY(slice.value),
127
+ paint: {
128
+ fill: theme.foreground,
129
+ fontSize: theme.fontSize + 2,
130
+ fontWeight: 600,
131
+ baseline: "top",
132
+ },
133
+ },
134
+ );
135
+ }
136
+ if (options.tip === false) {
137
+ continue;
138
+ }
139
+ const previous = slice.index > 0 ? values[slice.index - 1] : undefined;
140
+ const conversion =
141
+ previous !== undefined && previous > 0
142
+ ? ` (${formatPercentage(slice.value / previous, context.locale)})`
143
+ : "";
144
+ const centerY = top + height / 2;
145
+ points.push({
146
+ key: `${id}:${key}`,
147
+ markId: id,
148
+ index: slice.index,
149
+ datum: data[slice.index],
150
+ x: slice.x + slice.width / 2,
151
+ y: centerY,
152
+ xValue: category,
153
+ yValue: slice.value,
154
+ series: series.key,
155
+ color: series.color,
156
+ title: series.label ?? context.formatX(category),
157
+ value: `${context.formatY(slice.value)}${conversion}`,
158
+ hit: {
159
+ kind: "rect",
160
+ rect: {
161
+ x: slice.x,
162
+ y: centerY - slice.leftHeight / 2,
163
+ width: slice.width,
164
+ height: slice.leftHeight,
165
+ },
166
+ },
167
+ });
168
+ }
169
+ return { nodes, points };
170
+ },
171
+ };
172
+ }
@@ -0,0 +1,206 @@
1
+ import { channelLabel, isPlaceable, readChannel } from "#/core/channel.ts";
2
+ import {
3
+ type ChartMarkOptions,
4
+ colorChannel,
5
+ groupBySeries,
6
+ markId,
7
+ paint,
8
+ seriesResolver,
9
+ valueOn,
10
+ } from "#/core/marks/shared.ts";
11
+ import { type ChartXY, linePath } from "#/core/paths.ts";
12
+ import type {
13
+ ChartAccessor,
14
+ ChartCurve,
15
+ ChartMark,
16
+ ChartPoint,
17
+ ChartValue,
18
+ SceneNode,
19
+ } from "#/core/types.ts";
20
+
21
+ export interface LineOptions<TDatum> extends ChartMarkOptions<TDatum> {
22
+ readonly x?: ChartAccessor<TDatum, ChartValue>;
23
+ readonly y?: ChartAccessor<TDatum, ChartValue>;
24
+ readonly stroke?: string;
25
+ readonly strokeWidth?: number;
26
+ readonly strokeDasharray?: string;
27
+ readonly curve?: ChartCurve;
28
+ /** A dot on every point, not only on the focused one. */
29
+ readonly dots?: boolean;
30
+ }
31
+
32
+ const DEFAULT_STROKE_WIDTH = 2;
33
+ const DOT_RADIUS = 3;
34
+
35
+ /** With no accessor, a line of plain numbers plots each against its index. */
36
+ function positionAccessor<TDatum>(
37
+ accessor: ChartAccessor<TDatum, ChartValue> | undefined,
38
+ fallback: "index" | "identity",
39
+ ): ChartAccessor<TDatum, ChartValue> {
40
+ if (accessor !== undefined) {
41
+ return accessor;
42
+ }
43
+ return fallback === "index"
44
+ ? (_datum, index) => index
45
+ : (datum) => datum as unknown as ChartValue;
46
+ }
47
+
48
+ function lineMark<TDatum>(
49
+ kind: "lineY" | "lineX",
50
+ data: ReadonlyArray<TDatum>,
51
+ options: LineOptions<TDatum>,
52
+ ): ChartMark {
53
+ // lineY: the value runs up the y axis; lineX: along x, positions down y.
54
+ const vertical = kind === "lineY";
55
+ const positionKey = vertical ? options.x : options.y;
56
+ const valueKey = vertical ? options.y : options.x;
57
+ const positions = readChannel(data, positionAccessor(positionKey, "index"));
58
+ const values = readChannel(data, positionAccessor(valueKey, "identity"));
59
+ const colors = colorChannel(data, options);
60
+ const positionChannel = {
61
+ values: positions.filter(isPlaceable),
62
+ label: channelLabel(positionKey),
63
+ };
64
+ const valueChannel = {
65
+ values: values.filter(isPlaceable),
66
+ label: channelLabel(valueKey) ?? options.label,
67
+ };
68
+
69
+ return {
70
+ id: options.id,
71
+ coordinate: "cartesian",
72
+ focusOrder: vertical ? "x" : "y",
73
+ channels: {
74
+ x: vertical ? positionChannel : valueChannel,
75
+ y: vertical ? valueChannel : positionChannel,
76
+ color: colors.channel,
77
+ },
78
+ render(context) {
79
+ const id = markId(options.id, kind, context);
80
+ const positionScale = vertical ? context.scales.x : context.scales.y;
81
+ const valueScale = vertical ? context.scales.y : context.scales.x;
82
+ if (positionScale === undefined || valueScale === undefined) {
83
+ return { nodes: [] };
84
+ }
85
+ const seriesOf = seriesResolver(
86
+ colors.values,
87
+ { ...options, fixedColor: options.stroke },
88
+ id,
89
+ context,
90
+ );
91
+ const lines: SceneNode[] = [];
92
+ const dots: SceneNode[] = [];
93
+ const points: ChartPoint[] = [];
94
+
95
+ for (const { series, indices } of groupBySeries(
96
+ data.length,
97
+ seriesOf,
98
+ ).values()) {
99
+ // A gap in the data is a gap in the line, not a dive to zero.
100
+ const runs: ChartXY[][] = [[]];
101
+ for (const index of indices) {
102
+ const position = positions[index];
103
+ const value = valueOn(valueScale, values[index]);
104
+ if (!isPlaceable(position) || value === undefined) {
105
+ runs.push([]);
106
+ continue;
107
+ }
108
+ const along = positionScale.center(position);
109
+ const across = valueScale.center(value);
110
+ const xy = vertical
111
+ ? { x: along, y: across }
112
+ : { x: across, y: along };
113
+ runs[runs.length - 1].push(xy);
114
+ if (options.tip !== false) {
115
+ points.push({
116
+ key: `${id}:${index}`,
117
+ markId: id,
118
+ index,
119
+ datum: data[index],
120
+ x: xy.x,
121
+ y: xy.y,
122
+ xValue: vertical ? position : value,
123
+ yValue: vertical ? value : position,
124
+ series: series.key,
125
+ seriesLabel: series.label,
126
+ color: series.color,
127
+ title: vertical
128
+ ? context.formatX(position)
129
+ : context.formatY(position),
130
+ value: vertical ? context.formatY(value) : context.formatX(value),
131
+ });
132
+ }
133
+ if (options.dots) {
134
+ dots.push({
135
+ kind: "circle",
136
+ key: `${id}:dot:${index}`,
137
+ series: series.key,
138
+ role: "mark",
139
+ cx: xy.x,
140
+ cy: xy.y,
141
+ r: DOT_RADIUS,
142
+ paint: paint({
143
+ fill: series.color,
144
+ stroke: context.theme.background,
145
+ strokeWidth: 1.5,
146
+ opacity: options.opacity,
147
+ }),
148
+ });
149
+ }
150
+ }
151
+ const d = runs
152
+ .map((run) => linePath(run, options.curve))
153
+ .filter((part) => part !== "")
154
+ .join("");
155
+ lines.push({
156
+ kind: "path",
157
+ key: `${id}:line:${series.key}`,
158
+ series: series.key,
159
+ role: "mark",
160
+ d,
161
+ paint: paint({
162
+ stroke: series.color,
163
+ strokeWidth: options.strokeWidth ?? DEFAULT_STROKE_WIDTH,
164
+ strokeDasharray: options.strokeDasharray,
165
+ strokeLinecap: "round",
166
+ strokeLinejoin: "round",
167
+ fill: "none",
168
+ opacity: options.opacity,
169
+ }),
170
+ });
171
+ }
172
+
173
+ const legend =
174
+ options.color === undefined && options.label !== undefined
175
+ ? [
176
+ {
177
+ key: id,
178
+ label: options.label,
179
+ color:
180
+ options.stroke ?? context.paletteColor(context.markIndex),
181
+ shape: options.strokeDasharray
182
+ ? ("dashed" as const)
183
+ : ("line" as const),
184
+ },
185
+ ]
186
+ : [];
187
+ return { nodes: [...lines, ...dots], points, legend };
188
+ },
189
+ };
190
+ }
191
+
192
+ /** A line whose value runs up the y axis, over x positions: a time series. */
193
+ export function lineY<TDatum>(
194
+ data: ReadonlyArray<TDatum>,
195
+ options: LineOptions<TDatum> = {},
196
+ ): ChartMark {
197
+ return lineMark("lineY", data, options);
198
+ }
199
+
200
+ /** A line whose value runs along x, over y positions. */
201
+ export function lineX<TDatum>(
202
+ data: ReadonlyArray<TDatum>,
203
+ options: LineOptions<TDatum> = {},
204
+ ): ChartMark {
205
+ return lineMark("lineX", data, options);
206
+ }