@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,104 @@
1
+ import { isPlaceable, readChannel } from "#/core/channel.ts";
2
+ import { markId, paint, valueOn } from "#/core/marks/shared.ts";
3
+ import type {
4
+ ChartAccessor,
5
+ ChartMark,
6
+ ChartValue,
7
+ SceneNode,
8
+ } from "#/core/types.ts";
9
+
10
+ export interface RuleOptions<TDatum> {
11
+ readonly id?: string;
12
+ /** The value of each rule. Defaults to the datum itself, so `ruleY([0])` works. */
13
+ readonly value?: ChartAccessor<TDatum, ChartValue>;
14
+ readonly stroke?: string;
15
+ readonly strokeWidth?: number;
16
+ readonly strokeDasharray?: string;
17
+ readonly opacity?: number;
18
+ /** Text written at the end of the rule: "Objectif", "Moyenne". */
19
+ readonly label?: string;
20
+ }
21
+
22
+ const LABEL_GAP = 4;
23
+
24
+ function ruleMark<TDatum>(
25
+ kind: "ruleY" | "ruleX",
26
+ data: ReadonlyArray<TDatum>,
27
+ options: RuleOptions<TDatum>,
28
+ ): ChartMark {
29
+ const horizontal = kind === "ruleY";
30
+ const values = readChannel(
31
+ data,
32
+ options.value ?? ((datum: TDatum) => datum as unknown as ChartValue),
33
+ );
34
+ const channel = { values: values.filter(isPlaceable) };
35
+ return {
36
+ id: options.id,
37
+ coordinate: "cartesian",
38
+ channels: horizontal ? { y: channel } : { x: channel },
39
+ render(context) {
40
+ const id = markId(options.id, kind, context);
41
+ const scale = horizontal ? context.scales.y : context.scales.x;
42
+ if (scale === undefined) {
43
+ return { nodes: [] };
44
+ }
45
+ const { plot, theme } = context;
46
+ const stroke = options.stroke ?? theme.foreground;
47
+ const nodes: SceneNode[] = [];
48
+ for (const [index, raw] of values.entries()) {
49
+ const value = valueOn(scale, raw);
50
+ if (value === undefined) {
51
+ continue;
52
+ }
53
+ const at = scale.center(value);
54
+ nodes.push({
55
+ kind: "line",
56
+ key: `${id}:${index}`,
57
+ role: "rule",
58
+ ...(horizontal
59
+ ? { x1: plot.x, y1: at, x2: plot.x + plot.width, y2: at }
60
+ : { x1: at, y1: plot.y, x2: at, y2: plot.y + plot.height }),
61
+ paint: paint({
62
+ stroke,
63
+ strokeWidth: options.strokeWidth ?? 1,
64
+ strokeDasharray: options.strokeDasharray,
65
+ opacity: options.opacity,
66
+ }),
67
+ });
68
+ if (options.label !== undefined) {
69
+ nodes.push({
70
+ kind: "text",
71
+ key: `${id}:label:${index}`,
72
+ role: "label",
73
+ x: horizontal ? plot.x + plot.width : at + LABEL_GAP,
74
+ y: horizontal ? at - LABEL_GAP : plot.y,
75
+ text: options.label,
76
+ paint: {
77
+ fill: stroke,
78
+ fontSize: theme.fontSize,
79
+ textAnchor: horizontal ? "end" : "start",
80
+ baseline: horizontal ? "bottom" : "top",
81
+ },
82
+ });
83
+ }
84
+ }
85
+ return { nodes };
86
+ },
87
+ };
88
+ }
89
+
90
+ /** Horizontal rules at y values: a baseline, a target, an average. */
91
+ export function ruleY<TDatum>(
92
+ data: ReadonlyArray<TDatum>,
93
+ options: RuleOptions<TDatum> = {},
94
+ ): ChartMark {
95
+ return ruleMark("ruleY", data, options);
96
+ }
97
+
98
+ /** Vertical rules at x values: a date, a threshold. */
99
+ export function ruleX<TDatum>(
100
+ data: ReadonlyArray<TDatum>,
101
+ options: RuleOptions<TDatum> = {},
102
+ ): ChartMark {
103
+ return ruleMark("ruleX", data, options);
104
+ }
@@ -0,0 +1,134 @@
1
+ import {
2
+ channelLabel,
3
+ isPlaceable,
4
+ numericValue,
5
+ readChannel,
6
+ } from "#/core/channel.ts";
7
+ import { categoryKey } from "#/core/scales/discrete.ts";
8
+ import type {
9
+ ChartAccessor,
10
+ ChartChannel,
11
+ ChartMarkContext,
12
+ ChartPaint,
13
+ ChartPositionScale,
14
+ ChartValue,
15
+ } from "#/core/types.ts";
16
+
17
+ /** Options every mark takes. */
18
+ export interface ChartMarkOptions<TDatum> {
19
+ /** Stable identity, so focus and legend state survive a data update. */
20
+ readonly id?: string;
21
+ /** The series' name when the mark has no color channel: legend, tooltip, table. */
22
+ readonly label?: string;
23
+ /** A field or function whose distinct values split the mark into coloured series. */
24
+ readonly color?: ChartAccessor<TDatum, ChartValue>;
25
+ /** `false` keeps the mark out of focus, tooltips and the data table. */
26
+ readonly tip?: boolean;
27
+ readonly opacity?: number;
28
+ }
29
+
30
+ export function markId(
31
+ id: string | undefined,
32
+ kind: string,
33
+ context: ChartMarkContext,
34
+ ): string {
35
+ return id ?? `${kind}-${context.markIndex}`;
36
+ }
37
+
38
+ /** A datum's series: its color value when there is a color channel, the mark otherwise. */
39
+ export interface SeriesInfo {
40
+ readonly key: string;
41
+ readonly label: string | undefined;
42
+ readonly color: string;
43
+ }
44
+
45
+ export function seriesResolver<TDatum>(
46
+ colors: ReadonlyArray<ChartValue | undefined>,
47
+ options: ChartMarkOptions<TDatum> & { readonly fixedColor?: string },
48
+ id: string,
49
+ context: ChartMarkContext,
50
+ ): (index: number) => SeriesInfo {
51
+ const fallback =
52
+ options.fixedColor ?? context.paletteColor(context.markIndex);
53
+ const single: SeriesInfo = { key: id, label: options.label, color: fallback };
54
+ if (options.color === undefined) {
55
+ return () => single;
56
+ }
57
+ return (index) => {
58
+ const value = colors[index];
59
+ if (value === undefined) {
60
+ return single;
61
+ }
62
+ return {
63
+ key: categoryKey(value),
64
+ label: context.seriesLabel(value),
65
+ color: options.fixedColor ?? context.colorOf(value),
66
+ };
67
+ };
68
+ }
69
+
70
+ export function colorChannel<TDatum>(
71
+ data: ReadonlyArray<TDatum>,
72
+ options: ChartMarkOptions<TDatum>,
73
+ ): {
74
+ readonly values: ReadonlyArray<ChartValue | undefined>;
75
+ readonly channel: ChartChannel | undefined;
76
+ } {
77
+ const values = readChannel(data, options.color);
78
+ return {
79
+ values,
80
+ channel:
81
+ options.color === undefined
82
+ ? undefined
83
+ : {
84
+ values: values.filter(isPlaceable),
85
+ label: channelLabel(options.color),
86
+ },
87
+ };
88
+ }
89
+
90
+ /** Keeps only the defined paint keys, so a scene snapshot shows what was asked for. */
91
+ export function paint<TPaint extends ChartPaint>(values: TPaint): TPaint {
92
+ return Object.fromEntries(
93
+ Object.entries(values).filter(([, value]) => value !== undefined),
94
+ ) as TPaint;
95
+ }
96
+
97
+ /** Indices of `data` grouped by series, each group in data order. */
98
+ export function groupBySeries(
99
+ count: number,
100
+ seriesOf: (index: number) => SeriesInfo,
101
+ ): ReadonlyMap<
102
+ string,
103
+ { readonly series: SeriesInfo; readonly indices: number[] }
104
+ > {
105
+ const groups = new Map<string, { series: SeriesInfo; indices: number[] }>();
106
+ for (let index = 0; index < count; index += 1) {
107
+ const series = seriesOf(index);
108
+ const group = groups.get(series.key);
109
+ if (group === undefined) {
110
+ groups.set(series.key, { series, indices: [index] });
111
+ } else {
112
+ group.indices.push(index);
113
+ }
114
+ }
115
+ return groups;
116
+ }
117
+
118
+ /**
119
+ * A raw channel value as the scale can place it, or `undefined` for a gap. A
120
+ * continuous scale reads numeric strings as numbers; a discrete one takes any
121
+ * category as is.
122
+ */
123
+ export function valueOn(
124
+ scale: ChartPositionScale,
125
+ raw: unknown,
126
+ ): ChartValue | undefined {
127
+ if (scale.kind === "band" || scale.kind === "point") {
128
+ return isPlaceable(raw) ? raw : undefined;
129
+ }
130
+ if (raw instanceof Date) {
131
+ return Number.isNaN(raw.getTime()) ? undefined : raw;
132
+ }
133
+ return numericValue(raw);
134
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Stacking: each value sits on the running total of the values before it at
3
+ * the same position. Positives grow away from zero upwards and negatives
4
+ * downwards, separately, so a stack of mixed signs never overlaps itself.
5
+ */
6
+
7
+ export interface StackSegment {
8
+ readonly low: number;
9
+ readonly high: number;
10
+ /** The outermost segment of its side of the stack: the one whose far end gets rounded. */
11
+ readonly outer: boolean;
12
+ }
13
+
14
+ export function stackSegments(
15
+ positions: ReadonlyArray<string | undefined>,
16
+ values: ReadonlyArray<number | undefined>,
17
+ ): ReadonlyArray<StackSegment | undefined> {
18
+ const above = new Map<string, number>();
19
+ const below = new Map<string, number>();
20
+ const lastAbove = new Map<string, number>();
21
+ const lastBelow = new Map<string, number>();
22
+ const segments: Array<
23
+ { low: number; high: number; outer: boolean } | undefined
24
+ > = positions.map((position, index) => {
25
+ const value = values[index];
26
+ if (position === undefined || value === undefined) {
27
+ return undefined;
28
+ }
29
+ const totals = value >= 0 ? above : below;
30
+ const base = totals.get(position) ?? 0;
31
+ totals.set(position, base + value);
32
+ (value >= 0 ? lastAbove : lastBelow).set(position, index);
33
+ return { low: base, high: base + value, outer: false };
34
+ });
35
+ for (const index of [...lastAbove.values(), ...lastBelow.values()]) {
36
+ const segment = segments[index];
37
+ if (segment !== undefined) {
38
+ segment.outer = true;
39
+ }
40
+ }
41
+ return segments;
42
+ }
@@ -0,0 +1,205 @@
1
+ import {
2
+ channelLabel,
3
+ isPlaceable,
4
+ numericValue,
5
+ readChannel,
6
+ } from "#/core/channel.ts";
7
+ import { markId, paint, valueOn } from "#/core/marks/shared.ts";
8
+ import { areaPath, type ChartXY, linePath } from "#/core/paths.ts";
9
+ import type {
10
+ ChartAccessor,
11
+ ChartMark,
12
+ ChartPoint,
13
+ ChartValue,
14
+ SceneNode,
15
+ } from "#/core/types.ts";
16
+
17
+ export interface DifferenceOptions<TDatum> {
18
+ readonly id?: string;
19
+ readonly x: ChartAccessor<TDatum, ChartValue>;
20
+ /** The series read against the other: the fill says when it is above. */
21
+ readonly y1: ChartAccessor<TDatum, ChartValue>;
22
+ readonly y2: ChartAccessor<TDatum, ChartValue>;
23
+ /** Names of the two series, for the tooltip, the table and the legend. */
24
+ readonly labels?: readonly [string, string];
25
+ /** Fill where `y1` is above `y2`, and below. */
26
+ readonly positiveFill?: string;
27
+ readonly negativeFill?: string;
28
+ readonly fillOpacity?: number;
29
+ }
30
+
31
+ interface Sample {
32
+ readonly x: number;
33
+ readonly top: number;
34
+ readonly bottom: number;
35
+ }
36
+
37
+ /** Splits the band between two lines into runs of one sign, cut where they cross. */
38
+ function signRuns(
39
+ samples: ReadonlyArray<Sample>,
40
+ ): Array<{ positive: boolean; samples: Sample[] }> {
41
+ const runs: Array<{ positive: boolean; samples: Sample[] }> = [];
42
+ for (const [index, sample] of samples.entries()) {
43
+ // Pixels: a smaller y is higher, so y1 above y2 means top < bottom.
44
+ const positive = sample.top <= sample.bottom;
45
+ const current = runs[runs.length - 1];
46
+ if (current === undefined) {
47
+ runs.push({ positive, samples: [sample] });
48
+ continue;
49
+ }
50
+ if (current.positive === positive) {
51
+ current.samples.push(sample);
52
+ continue;
53
+ }
54
+ const previous = samples[index - 1];
55
+ const before = previous.top - previous.bottom;
56
+ const after = sample.top - sample.bottom;
57
+ const share = before / (before - after);
58
+ const crossX = previous.x + (sample.x - previous.x) * share;
59
+ const crossY = previous.top + (sample.top - previous.top) * share;
60
+ const cross = { x: crossX, top: crossY, bottom: crossY };
61
+ current.samples.push(cross);
62
+ runs.push({ positive, samples: [cross, sample] });
63
+ }
64
+ return runs;
65
+ }
66
+
67
+ /**
68
+ * Two series and the band between them, filled by which one is ahead:
69
+ * revenue against costs, this year against last.
70
+ */
71
+ export function differenceY<TDatum>(
72
+ data: ReadonlyArray<TDatum>,
73
+ options: DifferenceOptions<TDatum>,
74
+ ): ChartMark {
75
+ const xs = readChannel(data, options.x);
76
+ const first = readChannel(data, options.y1).map(numericValue);
77
+ const second = readChannel(data, options.y2).map(numericValue);
78
+ const [firstLabel, secondLabel] = options.labels ?? [
79
+ channelLabel(options.y1) ?? "y1",
80
+ channelLabel(options.y2) ?? "y2",
81
+ ];
82
+ return {
83
+ id: options.id,
84
+ coordinate: "cartesian",
85
+ focusOrder: "x",
86
+ channels: {
87
+ x: { values: xs.filter(isPlaceable), label: channelLabel(options.x) },
88
+ y: {
89
+ values: [...first, ...second].filter(
90
+ (value): value is number => value !== undefined,
91
+ ),
92
+ },
93
+ },
94
+ render(context) {
95
+ const id = markId(options.id, "differenceY", context);
96
+ const { x: xScale, y: yScale } = context.scales;
97
+ if (xScale === undefined || yScale === undefined) {
98
+ return { nodes: [] };
99
+ }
100
+ const positive = options.positiveFill ?? context.paletteColor(1);
101
+ const negative = options.negativeFill ?? context.paletteColor(0);
102
+ const samples: Sample[] = [];
103
+ const points: ChartPoint[] = [];
104
+ for (const [index, raw] of xs.entries()) {
105
+ const x = valueOn(xScale, raw);
106
+ const a = first[index];
107
+ const b = second[index];
108
+ if (x === undefined || a === undefined || b === undefined) continue;
109
+ const sample = {
110
+ x: xScale.center(x),
111
+ top: yScale.map(a),
112
+ bottom: yScale.map(b),
113
+ };
114
+ samples.push(sample);
115
+ for (const [series, value, pixel, label] of [
116
+ ["y1", a, sample.top, firstLabel],
117
+ ["y2", b, sample.bottom, secondLabel],
118
+ ] as const) {
119
+ points.push({
120
+ key: `${id}:${series}:${index}`,
121
+ markId: id,
122
+ index,
123
+ datum: data[index],
124
+ x: sample.x,
125
+ y: pixel,
126
+ xValue: x,
127
+ yValue: value,
128
+ series: `${id}:${series}`,
129
+ seriesLabel: label,
130
+ color:
131
+ series === "y1" ? context.theme.foreground : context.theme.muted,
132
+ title: context.formatX(x),
133
+ value: context.formatY(value),
134
+ });
135
+ }
136
+ }
137
+ const nodes: SceneNode[] = signRuns(samples).map((run, index) => ({
138
+ kind: "path",
139
+ key: `${id}:band:${index}`,
140
+ role: "mark",
141
+ d: areaPath(
142
+ run.samples.map(
143
+ (sample): ChartXY => ({ x: sample.x, y: sample.top }),
144
+ ),
145
+ run.samples.map(
146
+ (sample): ChartXY => ({ x: sample.x, y: sample.bottom }),
147
+ ),
148
+ ),
149
+ paint: paint({
150
+ fill: run.positive ? positive : negative,
151
+ fillOpacity: options.fillOpacity ?? 0.35,
152
+ }),
153
+ }));
154
+ nodes.push(
155
+ {
156
+ kind: "path",
157
+ key: `${id}:line:y1`,
158
+ series: `${id}:y1`,
159
+ role: "mark",
160
+ d: linePath(
161
+ samples.map((sample) => ({ x: sample.x, y: sample.top })),
162
+ ),
163
+ paint: {
164
+ fill: "none",
165
+ stroke: context.theme.foreground,
166
+ strokeWidth: 1.5,
167
+ },
168
+ },
169
+ {
170
+ kind: "path",
171
+ key: `${id}:line:y2`,
172
+ series: `${id}:y2`,
173
+ role: "mark",
174
+ d: linePath(
175
+ samples.map((sample) => ({ x: sample.x, y: sample.bottom })),
176
+ ),
177
+ paint: {
178
+ fill: "none",
179
+ stroke: context.theme.muted,
180
+ strokeWidth: 1.5,
181
+ strokeDasharray: "4 3",
182
+ },
183
+ },
184
+ );
185
+ return {
186
+ nodes,
187
+ points,
188
+ legend: [
189
+ {
190
+ key: `${id}:y1`,
191
+ label: firstLabel,
192
+ color: context.theme.foreground,
193
+ shape: "line",
194
+ },
195
+ {
196
+ key: `${id}:y2`,
197
+ label: secondLabel,
198
+ color: context.theme.muted,
199
+ shape: "dashed",
200
+ },
201
+ ],
202
+ };
203
+ },
204
+ };
205
+ }