@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,568 @@
1
+ import {
2
+ channelLabel,
3
+ isPlaceable,
4
+ numericValue,
5
+ readChannel,
6
+ } from "#/core/channel.ts";
7
+ import { formatPercentage } from "#/core/format.ts";
8
+ import {
9
+ type ChartMarkOptions,
10
+ markId,
11
+ paint,
12
+ seriesResolver,
13
+ } from "#/core/marks/shared.ts";
14
+ import {
15
+ arcPath,
16
+ type ChartXY,
17
+ polarToCartesian,
18
+ polygonPath,
19
+ } from "#/core/paths.ts";
20
+ import { categoryKey } from "#/core/scales/discrete.ts";
21
+ import { niceDomain } from "#/core/ticks.ts";
22
+ import type {
23
+ ChartAccessor,
24
+ ChartMark,
25
+ ChartMarkContext,
26
+ ChartPoint,
27
+ ChartRect,
28
+ ChartValue,
29
+ SceneNode,
30
+ } from "#/core/types.ts";
31
+
32
+ /**
33
+ * Round marks. They own the whole plot rather than sitting on x and y
34
+ * scales; their slices are coloured by category, which is what the legend
35
+ * lists. Angles are degrees clockwise from twelve o'clock.
36
+ */
37
+
38
+ const FULL_TURN = 360;
39
+ const EDGE = 4;
40
+
41
+ interface Frame {
42
+ readonly cx: number;
43
+ readonly cy: number;
44
+ readonly radius: number;
45
+ }
46
+
47
+ /**
48
+ * The circle a round mark is drawn in: centred, as large as the plot allows
49
+ * once `insetX` (left and right) and `insetY` (top and bottom) are kept clear
50
+ * for labels drawn outside it.
51
+ */
52
+ function frameOf(plot: ChartRect, insetX = 0, insetY = insetX): Frame {
53
+ return {
54
+ cx: plot.x + plot.width / 2,
55
+ cy: plot.y + plot.height / 2,
56
+ radius: Math.max(
57
+ 0,
58
+ Math.min(plot.width / 2 - insetX, plot.height / 2 - insetY) - EDGE,
59
+ ),
60
+ };
61
+ }
62
+
63
+ interface SliceAngle {
64
+ readonly startAngle: number;
65
+ readonly endAngle: number;
66
+ readonly fraction: number;
67
+ }
68
+
69
+ /**
70
+ * Splits a turn between values, proportionally. Negative values count as
71
+ * zero: a pie of signed numbers has no honest reading.
72
+ */
73
+ export function sliceAngles(
74
+ values: ReadonlyArray<number>,
75
+ padAngle = 0,
76
+ startAngle = 0,
77
+ endAngle = FULL_TURN,
78
+ ): ReadonlyArray<SliceAngle> {
79
+ const positives = values.map((value) =>
80
+ Number.isFinite(value) && value > 0 ? value : 0,
81
+ );
82
+ const total = positives.reduce((sum, value) => sum + value, 0);
83
+ const pads = positives.filter((value) => value > 0).length > 1 ? padAngle : 0;
84
+ const available = Math.max(
85
+ 0,
86
+ endAngle - startAngle - pads * positives.length,
87
+ );
88
+ let cursor = startAngle;
89
+ return positives.map((value) => {
90
+ const fraction = total === 0 ? 0 : value / total;
91
+ const sweep = available * fraction;
92
+ const slice = {
93
+ startAngle: cursor + pads / 2,
94
+ endAngle: cursor + pads / 2 + sweep,
95
+ fraction,
96
+ };
97
+ cursor += sweep + pads;
98
+ return slice;
99
+ });
100
+ }
101
+
102
+ export interface ArcOptions<TDatum> extends ChartMarkOptions<TDatum> {
103
+ /** The slice's name: what the legend, tooltip and table call it. */
104
+ readonly category: ChartAccessor<TDatum, ChartValue>;
105
+ readonly value: ChartAccessor<TDatum, ChartValue>;
106
+ /** Share of the radius left empty in the middle: 0 is a pie, 0.6 a donut. */
107
+ readonly innerRadius?: number;
108
+ /** Degrees of blank between two slices. */
109
+ readonly padAngle?: number;
110
+ readonly startAngle?: number;
111
+ readonly endAngle?: number;
112
+ readonly fill?: string;
113
+ readonly stroke?: string;
114
+ }
115
+
116
+ const DEFAULT_PAD_ANGLE = 1;
117
+
118
+ /** A pie, or a donut with `innerRadius`: each datum a slice of the whole. */
119
+ export function arc<TDatum>(
120
+ data: ReadonlyArray<TDatum>,
121
+ options: ArcOptions<TDatum>,
122
+ ): ChartMark {
123
+ const categories = readChannel(data, options.category);
124
+ const values = readChannel(data, options.value).map(numericValue);
125
+ const colorAccessor = options.color ?? options.category;
126
+ const colors = readChannel(data, colorAccessor);
127
+ return {
128
+ id: options.id,
129
+ coordinate: "frame",
130
+ titles: {
131
+ category: channelLabel(options.category),
132
+ value: channelLabel(options.value) ?? options.label,
133
+ },
134
+ channels: {
135
+ color: {
136
+ values: colors.filter(isPlaceable),
137
+ label: channelLabel(colorAccessor),
138
+ },
139
+ },
140
+ render(context) {
141
+ const id = markId(options.id, "arc", context);
142
+ const { cx, cy, radius } = frameOf(context.plot);
143
+ const inner = radius * (options.innerRadius ?? 0);
144
+ const angles = sliceAngles(
145
+ values.map((value) => value ?? 0),
146
+ options.padAngle ?? DEFAULT_PAD_ANGLE,
147
+ options.startAngle,
148
+ options.endAngle,
149
+ );
150
+ const seriesOf = seriesResolver(
151
+ colors,
152
+ { ...options, color: colorAccessor, fixedColor: options.fill },
153
+ id,
154
+ context,
155
+ );
156
+ const nodes: SceneNode[] = [];
157
+ const points: ChartPoint[] = [];
158
+ for (const [index, angle] of angles.entries()) {
159
+ const category = categories[index];
160
+ const value = values[index];
161
+ if (
162
+ !isPlaceable(category) ||
163
+ value === undefined ||
164
+ angle.fraction === 0
165
+ ) {
166
+ continue;
167
+ }
168
+ const series = seriesOf(index);
169
+ const shape = {
170
+ cx,
171
+ cy,
172
+ innerRadius: inner,
173
+ outerRadius: radius,
174
+ startAngle: angle.startAngle,
175
+ endAngle: angle.endAngle,
176
+ };
177
+ nodes.push({
178
+ kind: "path",
179
+ key: `${id}:${categoryKey(category)}`,
180
+ series: series.key,
181
+ role: "mark",
182
+ d: arcPath(shape),
183
+ paint: paint({
184
+ fill: series.color,
185
+ stroke: options.stroke ?? context.theme.background,
186
+ strokeWidth: options.stroke ? 1 : 0,
187
+ opacity: options.opacity,
188
+ }),
189
+ });
190
+ if (options.tip === false) {
191
+ continue;
192
+ }
193
+ const middle = (angle.startAngle + angle.endAngle) / 2;
194
+ const anchor = polarToCartesian(cx, cy, (inner + radius) / 2, middle);
195
+ points.push({
196
+ key: `${id}:${categoryKey(category)}`,
197
+ markId: id,
198
+ index,
199
+ datum: data[index],
200
+ x: anchor.x,
201
+ y: anchor.y,
202
+ xValue: category,
203
+ yValue: value,
204
+ series: series.key,
205
+ seriesLabel: options.label,
206
+ color: series.color,
207
+ title: series.label ?? context.formatX(category),
208
+ value: `${context.formatY(value)} (${formatPercentage(angle.fraction, context.locale)})`,
209
+ hit: { kind: "arc", ...shape },
210
+ });
211
+ }
212
+ return { nodes, points };
213
+ },
214
+ };
215
+ }
216
+
217
+ /** A ring of slices: `arc` with the middle left empty. */
218
+ export function donut<TDatum>(
219
+ data: ReadonlyArray<TDatum>,
220
+ options: ArcOptions<TDatum>,
221
+ ): ChartMark {
222
+ return arc(data, { innerRadius: 0.6, ...options });
223
+ }
224
+
225
+ export interface RadialBarOptions<TDatum> extends ChartMarkOptions<TDatum> {
226
+ readonly category: ChartAccessor<TDatum, ChartValue>;
227
+ readonly value: ChartAccessor<TDatum, ChartValue>;
228
+ /** The value a full turn stands for. Defaults to the largest value, rounded up. */
229
+ readonly max?: number;
230
+ /** Share of the radius left empty in the middle. */
231
+ readonly innerRadius?: number;
232
+ /** Share of each ring's thickness left empty between two rings. */
233
+ readonly gap?: number;
234
+ /** Draw each ring's empty remainder, so the bars read against a full turn. */
235
+ readonly track?: boolean;
236
+ readonly fill?: string;
237
+ }
238
+
239
+ /** Concentric rings, each filled as far round as its value: progress against a common maximum. */
240
+ export function radialBar<TDatum>(
241
+ data: ReadonlyArray<TDatum>,
242
+ options: RadialBarOptions<TDatum>,
243
+ ): ChartMark {
244
+ const categories = readChannel(data, options.category);
245
+ const values = readChannel(data, options.value).map(numericValue);
246
+ const colorAccessor = options.color ?? options.category;
247
+ const colors = readChannel(data, colorAccessor);
248
+ return {
249
+ id: options.id,
250
+ coordinate: "frame",
251
+ titles: {
252
+ category: channelLabel(options.category),
253
+ value: channelLabel(options.value) ?? options.label,
254
+ },
255
+ channels: {
256
+ color: {
257
+ values: colors.filter(isPlaceable),
258
+ label: channelLabel(colorAccessor),
259
+ },
260
+ },
261
+ render(context) {
262
+ const id = markId(options.id, "radialBar", context);
263
+ const { cx, cy, radius } = frameOf(context.plot);
264
+ const peak = Math.max(0, ...values.map((value) => value ?? 0));
265
+ const max = options.max ?? niceDomain(0, peak)[1];
266
+ const inner = radius * (options.innerRadius ?? 0.3);
267
+ const count = Math.max(1, data.length);
268
+ const ring = (radius - inner) / count;
269
+ const thickness = ring * (1 - (options.gap ?? 0.25));
270
+ const seriesOf = seriesResolver(
271
+ colors,
272
+ { ...options, color: colorAccessor, fixedColor: options.fill },
273
+ id,
274
+ context,
275
+ );
276
+ const nodes: SceneNode[] = [];
277
+ const points: ChartPoint[] = [];
278
+ for (const [index, category] of categories.entries()) {
279
+ const value = values[index];
280
+ if (!isPlaceable(category) || value === undefined) {
281
+ continue;
282
+ }
283
+ const outerRadius = radius - index * ring;
284
+ const shape = {
285
+ cx,
286
+ cy,
287
+ innerRadius: outerRadius - thickness,
288
+ outerRadius,
289
+ startAngle: 0,
290
+ endAngle: max <= 0 ? 0 : (Math.min(value, max) / max) * FULL_TURN,
291
+ };
292
+ const series = seriesOf(index);
293
+ if (options.track !== false) {
294
+ nodes.push({
295
+ kind: "path",
296
+ key: `${id}:track:${categoryKey(category)}`,
297
+ role: "track",
298
+ d: arcPath({ ...shape, endAngle: FULL_TURN }),
299
+ paint: { fill: context.theme.grid, opacity: 0.5 },
300
+ });
301
+ }
302
+ nodes.push({
303
+ kind: "path",
304
+ key: `${id}:${categoryKey(category)}`,
305
+ series: series.key,
306
+ role: "mark",
307
+ d: arcPath(shape),
308
+ paint: paint({ fill: series.color, opacity: options.opacity }),
309
+ });
310
+ if (options.tip === false) {
311
+ continue;
312
+ }
313
+ const anchor = polarToCartesian(
314
+ cx,
315
+ cy,
316
+ outerRadius - thickness / 2,
317
+ Math.max(shape.endAngle, 1),
318
+ );
319
+ points.push({
320
+ key: `${id}:${categoryKey(category)}`,
321
+ markId: id,
322
+ index,
323
+ datum: data[index],
324
+ x: anchor.x,
325
+ y: anchor.y,
326
+ xValue: category,
327
+ yValue: value,
328
+ series: series.key,
329
+ color: series.color,
330
+ title: series.label ?? context.formatX(category),
331
+ value: context.formatY(value),
332
+ hit: { kind: "arc", ...shape, endAngle: FULL_TURN },
333
+ });
334
+ }
335
+ return { nodes, points };
336
+ },
337
+ };
338
+ }
339
+
340
+ export interface RadarOptions<TDatum> extends ChartMarkOptions<TDatum> {
341
+ /** The spoke a value sits on: "Vitesse", "Endurance"… */
342
+ readonly axis: ChartAccessor<TDatum, ChartValue>;
343
+ readonly value: ChartAccessor<TDatum, ChartValue>;
344
+ /** The value the outer ring stands for. Defaults to the largest value, rounded up. */
345
+ readonly max?: number;
346
+ /** Grid rings between the centre and the outer ring. */
347
+ readonly rings?: number;
348
+ readonly fillOpacity?: number;
349
+ readonly fill?: string;
350
+ /** `false` leaves the grid and spoke labels to another radar on the same chart. */
351
+ readonly grid?: boolean;
352
+ }
353
+
354
+ const LABEL_GAP = 8;
355
+
356
+ function anchorFor(angle: number): {
357
+ readonly textAnchor: "start" | "middle" | "end";
358
+ readonly baseline: "top" | "middle" | "bottom";
359
+ } {
360
+ const normalized = ((angle % FULL_TURN) + FULL_TURN) % FULL_TURN;
361
+ const side = Math.sin((normalized * Math.PI) / 180);
362
+ const vertical = -Math.cos((normalized * Math.PI) / 180);
363
+ return {
364
+ textAnchor: Math.abs(side) < 0.2 ? "middle" : side > 0 ? "start" : "end",
365
+ baseline:
366
+ Math.abs(vertical) < 0.2 ? "middle" : vertical > 0 ? "top" : "bottom",
367
+ };
368
+ }
369
+
370
+ function radarGrid(
371
+ id: string,
372
+ context: ChartMarkContext,
373
+ frame: Frame,
374
+ axes: ReadonlyArray<ChartValue>,
375
+ rings: number,
376
+ ): SceneNode[] {
377
+ const { cx, cy, radius } = frame;
378
+ const { theme } = context;
379
+ const nodes: SceneNode[] = [];
380
+ for (let level = 1; level <= rings; level += 1) {
381
+ const ringRadius = (radius * level) / rings;
382
+ nodes.push({
383
+ kind: "path",
384
+ key: `${id}:ring:${level}`,
385
+ role: "grid",
386
+ d: polygonPath(
387
+ axes.map((_axis, index) =>
388
+ polarToCartesian(
389
+ cx,
390
+ cy,
391
+ ringRadius,
392
+ (FULL_TURN * index) / axes.length,
393
+ ),
394
+ ),
395
+ ),
396
+ paint: { stroke: theme.grid, strokeWidth: 1, fill: "none" },
397
+ });
398
+ }
399
+ for (const [index, axis] of axes.entries()) {
400
+ const angle = (FULL_TURN * index) / axes.length;
401
+ const end = polarToCartesian(cx, cy, radius, angle);
402
+ const label = polarToCartesian(cx, cy, radius + LABEL_GAP, angle);
403
+ nodes.push(
404
+ {
405
+ kind: "line",
406
+ key: `${id}:spoke:${categoryKey(axis)}`,
407
+ role: "grid",
408
+ x1: cx,
409
+ y1: cy,
410
+ x2: end.x,
411
+ y2: end.y,
412
+ paint: { stroke: theme.grid, strokeWidth: 1 },
413
+ },
414
+ {
415
+ kind: "text",
416
+ key: `${id}:axis:${categoryKey(axis)}`,
417
+ role: "axis",
418
+ x: label.x,
419
+ y: label.y,
420
+ text: context.formatX(axis),
421
+ paint: {
422
+ fill: theme.muted,
423
+ fontSize: theme.fontSize,
424
+ ...anchorFor(angle),
425
+ },
426
+ },
427
+ );
428
+ }
429
+ return nodes;
430
+ }
431
+
432
+ /** Values on spokes joined into a polygon, one per series: profiles compared at a glance. */
433
+ export function radar<TDatum>(
434
+ data: ReadonlyArray<TDatum>,
435
+ options: RadarOptions<TDatum>,
436
+ ): ChartMark {
437
+ const axisValues = readChannel(data, options.axis);
438
+ const values = readChannel(data, options.value).map(numericValue);
439
+ const colors = readChannel(data, options.color);
440
+ const axes = [
441
+ ...new Map(
442
+ axisValues.filter(isPlaceable).map((axis) => [categoryKey(axis), axis]),
443
+ ).values(),
444
+ ];
445
+ return {
446
+ id: options.id,
447
+ coordinate: "frame",
448
+ titles: {
449
+ category: channelLabel(options.axis),
450
+ value: channelLabel(options.value) ?? options.label,
451
+ },
452
+ channels:
453
+ options.color === undefined
454
+ ? {}
455
+ : {
456
+ color: {
457
+ values: colors.filter(isPlaceable),
458
+ label: channelLabel(options.color),
459
+ },
460
+ },
461
+ render(context) {
462
+ const id = markId(options.id, "radar", context);
463
+ const widest = Math.max(
464
+ 0,
465
+ ...axes.map((axis) =>
466
+ context.measureText(context.formatX(axis), context.theme.fontSize),
467
+ ),
468
+ );
469
+ const frame = frameOf(
470
+ context.plot,
471
+ widest + LABEL_GAP,
472
+ context.theme.fontSize + LABEL_GAP,
473
+ );
474
+ const peak = Math.max(0, ...values.map((value) => value ?? 0));
475
+ const max = options.max ?? niceDomain(0, peak)[1];
476
+ const seriesOf = seriesResolver(
477
+ colors,
478
+ { ...options, fixedColor: options.fill },
479
+ id,
480
+ context,
481
+ );
482
+ const nodes: SceneNode[] =
483
+ options.grid === false
484
+ ? []
485
+ : radarGrid(id, context, frame, axes, options.rings ?? 4);
486
+ const points: ChartPoint[] = [];
487
+ const polygons = new Map<
488
+ string,
489
+ { color: string; vertices: Array<ChartXY | undefined> }
490
+ >();
491
+ for (const [index, axis] of axisValues.entries()) {
492
+ const value = values[index];
493
+ const spoke = isPlaceable(axis)
494
+ ? axes.findIndex(
495
+ (candidate) => categoryKey(candidate) === categoryKey(axis),
496
+ )
497
+ : -1;
498
+ if (spoke === -1 || value === undefined || !isPlaceable(axis)) {
499
+ continue;
500
+ }
501
+ const series = seriesOf(index);
502
+ const angle = (FULL_TURN * spoke) / axes.length;
503
+ const vertex = polarToCartesian(
504
+ frame.cx,
505
+ frame.cy,
506
+ max <= 0 ? 0 : (Math.min(value, max) / max) * frame.radius,
507
+ angle,
508
+ );
509
+ const polygon = polygons.get(series.key) ?? {
510
+ color: series.color,
511
+ vertices: axes.map(() => undefined),
512
+ };
513
+ polygon.vertices[spoke] = vertex;
514
+ polygons.set(series.key, polygon);
515
+ if (options.tip !== false) {
516
+ points.push({
517
+ key: `${id}:${index}`,
518
+ markId: id,
519
+ index,
520
+ datum: data[index],
521
+ x: vertex.x,
522
+ y: vertex.y,
523
+ xValue: axis,
524
+ yValue: value,
525
+ series: series.key,
526
+ seriesLabel: series.label,
527
+ color: series.color,
528
+ title: context.formatX(axis),
529
+ value: context.formatY(value),
530
+ });
531
+ }
532
+ }
533
+ for (const [key, polygon] of polygons) {
534
+ nodes.push({
535
+ kind: "path",
536
+ key: `${id}:shape:${key}`,
537
+ series: key,
538
+ role: "mark",
539
+ d: polygonPath(
540
+ polygon.vertices.map(
541
+ (vertex) => vertex ?? { x: frame.cx, y: frame.cy },
542
+ ),
543
+ ),
544
+ paint: paint({
545
+ fill: polygon.color,
546
+ fillOpacity: options.fillOpacity ?? 0.2,
547
+ stroke: polygon.color,
548
+ strokeWidth: 2,
549
+ strokeLinejoin: "round",
550
+ opacity: options.opacity,
551
+ }),
552
+ });
553
+ }
554
+ const legend =
555
+ options.color === undefined && options.label !== undefined
556
+ ? [
557
+ {
558
+ key: id,
559
+ label: options.label,
560
+ color: options.fill ?? context.paletteColor(context.markIndex),
561
+ shape: "square" as const,
562
+ },
563
+ ]
564
+ : [];
565
+ return { nodes, points, legend };
566
+ },
567
+ };
568
+ }
@@ -0,0 +1,156 @@
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 { stackSegments } from "#/core/marks/stack.ts";
16
+ import { categoryKey } from "#/core/scales/discrete.ts";
17
+ import type {
18
+ ChartAccessor,
19
+ ChartMark,
20
+ ChartPoint,
21
+ ChartValue,
22
+ SceneNode,
23
+ } from "#/core/types.ts";
24
+
25
+ export interface RectOptions<TDatum> extends ChartMarkOptions<TDatum> {
26
+ /** Start and end of the rectangle along a continuous x: a bin, a period. */
27
+ readonly x1: ChartAccessor<TDatum, ChartValue>;
28
+ readonly x2: ChartAccessor<TDatum, ChartValue>;
29
+ /** Its height. Stacks by colour like bars. */
30
+ readonly y: ChartAccessor<TDatum, ChartValue>;
31
+ readonly fill?: string;
32
+ readonly fillOpacity?: number;
33
+ /** Pixels kept clear on each side, so neighbours read as separate. */
34
+ readonly inset?: number;
35
+ readonly stack?: boolean;
36
+ readonly radius?: number;
37
+ }
38
+
39
+ /**
40
+ * A rectangle between two x values, as tall as its value: the bars of a
41
+ * histogram, periods on a timeline. Unlike `barY` the x axis stays
42
+ * continuous, so the bins keep their true widths.
43
+ */
44
+ export function rectY<TDatum>(
45
+ data: ReadonlyArray<TDatum>,
46
+ options: RectOptions<TDatum>,
47
+ ): ChartMark {
48
+ const starts = readChannel(data, options.x1);
49
+ const ends = readChannel(data, options.x2);
50
+ const values = readChannel(data, options.y).map(numericValue);
51
+ const colors = colorChannel(data, options);
52
+ const stacked = options.stack ?? options.color !== undefined;
53
+ const segments = stacked
54
+ ? stackSegments(
55
+ starts.map((start) =>
56
+ isPlaceable(start) ? categoryKey(start) : undefined,
57
+ ),
58
+ values,
59
+ )
60
+ : [];
61
+ const edges = values.map((value, index) =>
62
+ value === undefined
63
+ ? undefined
64
+ : (segments[index] ?? { low: 0, high: value, outer: true }),
65
+ );
66
+ return {
67
+ id: options.id,
68
+ coordinate: "cartesian",
69
+ focusOrder: "x",
70
+ channels: {
71
+ x: {
72
+ values: [...starts, ...ends].filter(isPlaceable),
73
+ label: channelLabel(options.x1),
74
+ },
75
+ y: {
76
+ values: edges.flatMap((edge) => (edge ? [edge.low, edge.high] : [])),
77
+ includeZero: true,
78
+ label: channelLabel(options.y) ?? options.label,
79
+ },
80
+ color: colors.channel,
81
+ },
82
+ render(context) {
83
+ const id = markId(options.id, "rectY", context);
84
+ const { x: xScale, y: yScale } = context.scales;
85
+ if (xScale === undefined || yScale === undefined) {
86
+ return { nodes: [] };
87
+ }
88
+ const seriesOf = seriesResolver(
89
+ colors.values,
90
+ { ...options, fixedColor: options.fill },
91
+ id,
92
+ context,
93
+ );
94
+ const inset = options.inset ?? 0.5;
95
+ const radius = options.radius ?? 0;
96
+ const nodes: SceneNode[] = [];
97
+ const points: ChartPoint[] = [];
98
+ for (const [index, edge] of edges.entries()) {
99
+ const start = valueOn(xScale, starts[index]);
100
+ const end = valueOn(xScale, ends[index]);
101
+ const value = values[index];
102
+ if (
103
+ edge === undefined ||
104
+ start === undefined ||
105
+ end === undefined ||
106
+ value === undefined
107
+ ) {
108
+ continue;
109
+ }
110
+ const left = Math.min(xScale.map(start), xScale.map(end)) + inset;
111
+ const right = Math.max(xScale.map(start), xScale.map(end)) - inset;
112
+ const top = Math.min(yScale.map(edge.low), yScale.map(edge.high));
113
+ const rect = {
114
+ x: left,
115
+ y: top,
116
+ width: Math.max(0, right - left),
117
+ height: Math.abs(yScale.map(edge.high) - yScale.map(edge.low)),
118
+ };
119
+ const series = seriesOf(index);
120
+ nodes.push({
121
+ kind: "rect",
122
+ key: `${id}:${index}`,
123
+ series: series.key,
124
+ role: "mark",
125
+ ...rect,
126
+ corners:
127
+ radius > 0 && edge.outer ? [radius, radius, 0, 0] : undefined,
128
+ paint: paint({
129
+ fill: series.color,
130
+ fillOpacity: options.fillOpacity,
131
+ opacity: options.opacity,
132
+ }),
133
+ });
134
+ if (options.tip !== false) {
135
+ points.push({
136
+ key: `${id}:${index}`,
137
+ markId: id,
138
+ index,
139
+ datum: data[index],
140
+ x: rect.x + rect.width / 2,
141
+ y: top,
142
+ xValue: start,
143
+ yValue: value,
144
+ series: series.key,
145
+ seriesLabel: series.label,
146
+ color: series.color,
147
+ title: `${context.formatX(start)} – ${context.formatX(end)}`,
148
+ value: context.formatY(value),
149
+ hit: { kind: "rect", rect },
150
+ });
151
+ }
152
+ }
153
+ return { nodes, points };
154
+ },
155
+ };
156
+ }