@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,51 @@
1
+ import { toNumber } from "#/core/scales/continuous.ts";
2
+ import { categoryKey } from "#/core/scales/discrete.ts";
3
+ import type { ChartColorScale, ChartValue } from "#/core/types.ts";
4
+
5
+ /**
6
+ * Colours stay CSS strings all the way to the renderer (`var(--chart-2)`,
7
+ * `color-mix(…)`): SVG resolves them itself, and the Canvas renderer resolves
8
+ * them against the page, so a theme switch needs no recompute.
9
+ */
10
+
11
+ /** One palette colour per category, cycling when there are more categories than colours. */
12
+ export function ordinalColorScale(
13
+ domain: ReadonlyArray<ChartValue>,
14
+ palette: ReadonlyArray<string>,
15
+ ): ChartColorScale {
16
+ const indexByKey = new Map<string, number>();
17
+ for (const value of domain) {
18
+ const key = categoryKey(value);
19
+ if (!indexByKey.has(key)) {
20
+ indexByKey.set(key, indexByKey.size);
21
+ }
22
+ }
23
+ return {
24
+ kind: "ordinal",
25
+ domain,
26
+ map: (value) =>
27
+ palette[(indexByKey.get(categoryKey(value)) ?? 0) % palette.length],
28
+ };
29
+ }
30
+
31
+ const PERCENT_PRECISION = 10;
32
+
33
+ /** A ramp between two colours, mixed by the browser in OKLab so the steps look even. */
34
+ export function sequentialColorScale(
35
+ domain: readonly [number, number],
36
+ [from, to]: readonly [string, string],
37
+ ): ChartColorScale {
38
+ const [low, high] = domain;
39
+ const span = high - low;
40
+ return {
41
+ kind: "sequential",
42
+ domain,
43
+ map: (value) => {
44
+ const share = span === 0 ? 1 : (toNumber(value) - low) / span;
45
+ const percent =
46
+ Math.round(Math.min(1, Math.max(0, share)) * 100 * PERCENT_PRECISION) /
47
+ PERCENT_PRECISION;
48
+ return `color-mix(in oklab, ${to} ${percent}%, ${from})`;
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,111 @@
1
+ import { formatCompactNumber, formatTickValue } from "#/core/format.ts";
2
+ import { niceTicks } from "#/core/ticks.ts";
3
+ import type { ChartPositionScale, ChartValue } from "#/core/types.ts";
4
+
5
+ export interface ContinuousScaleOptions {
6
+ readonly domain: readonly [number, number];
7
+ readonly range: readonly [number, number];
8
+ readonly locale: string;
9
+ /** How many ticks the axis has room for. */
10
+ readonly tickCount: number;
11
+ }
12
+
13
+ export function toNumber(value: ChartValue): number {
14
+ if (value instanceof Date) {
15
+ return value.getTime();
16
+ }
17
+ return typeof value === "number" ? value : Number(value);
18
+ }
19
+
20
+ function interpolator(
21
+ [domainStart, domainEnd]: readonly [number, number],
22
+ [rangeStart, rangeEnd]: readonly [number, number],
23
+ ) {
24
+ const domainSpan = domainEnd - domainStart;
25
+ const rangeSpan = rangeEnd - rangeStart;
26
+ return {
27
+ // A zero-width domain collapses to the middle of the range: a flat series
28
+ // draws a centred line instead of dividing by zero.
29
+ forward: (value: number) =>
30
+ domainSpan === 0
31
+ ? rangeStart + rangeSpan / 2
32
+ : rangeStart + ((value - domainStart) / domainSpan) * rangeSpan,
33
+ backward: (pixel: number) =>
34
+ rangeSpan === 0
35
+ ? domainStart
36
+ : domainStart + ((pixel - rangeStart) / rangeSpan) * domainSpan,
37
+ };
38
+ }
39
+
40
+ /** Numbers in, pixels out, linearly. */
41
+ export function linearScale(
42
+ options: ContinuousScaleOptions,
43
+ ): ChartPositionScale {
44
+ const { domain, range, locale, tickCount } = options;
45
+ const { forward, backward } = interpolator(domain, range);
46
+ const map = (value: ChartValue) => forward(toNumber(value));
47
+ return {
48
+ kind: "linear",
49
+ domain,
50
+ range,
51
+ bandwidth: 0,
52
+ map,
53
+ center: map,
54
+ invert: backward,
55
+ ticks: (count = tickCount) =>
56
+ niceTicks(
57
+ Math.min(domain[0], domain[1]),
58
+ Math.max(domain[0], domain[1]),
59
+ count,
60
+ ),
61
+ format: (value) => formatTickValue(toNumber(value), locale),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Orders of magnitude, for data that spans several of them. Values at or
67
+ * below zero have no logarithm: they are clamped onto the bottom of the scale
68
+ * rather than drawn at minus infinity.
69
+ */
70
+ export function logScale(options: ContinuousScaleOptions): ChartPositionScale {
71
+ const { range, locale, tickCount } = options;
72
+ const low = Math.max(Number.MIN_VALUE, Math.min(...options.domain));
73
+ const high = Math.max(low, ...options.domain);
74
+ const logDomain: readonly [number, number] = [
75
+ Math.log10(low),
76
+ Math.log10(high),
77
+ ];
78
+ const { forward, backward } = interpolator(logDomain, range);
79
+ const map = (value: ChartValue) =>
80
+ forward(Math.log10(Math.max(low, toNumber(value))));
81
+
82
+ function ticks(count = tickCount): ReadonlyArray<number> {
83
+ const first = Math.floor(logDomain[0]);
84
+ const last = Math.ceil(logDomain[1]);
85
+ const decades = last - first;
86
+ // Few decades leave room for 2 and 5 between the powers of ten.
87
+ const multiples = decades * 3 <= count ? [1, 2, 5] : [1];
88
+ const values: number[] = [];
89
+ for (let power = first; power <= last; power += 1) {
90
+ for (const multiple of multiples) {
91
+ const value = multiple * 10 ** power;
92
+ if (value >= low && value <= high) {
93
+ values.push(value);
94
+ }
95
+ }
96
+ }
97
+ return values;
98
+ }
99
+
100
+ return {
101
+ kind: "log",
102
+ domain: [low, high],
103
+ range,
104
+ bandwidth: 0,
105
+ map,
106
+ center: map,
107
+ invert: (pixel) => 10 ** backward(pixel),
108
+ ticks,
109
+ format: (value) => formatCompactNumber(toNumber(value), locale),
110
+ };
111
+ }
@@ -0,0 +1,156 @@
1
+ import { dateFormat, formatDate } from "#/core/format.ts";
2
+ import type { ChartPositionScale, ChartValue } from "#/core/types.ts";
3
+
4
+ /**
5
+ * Categories in, slots out. `band` gives each category a slot of `bandwidth`
6
+ * pixels (what bars sit on), `point` a position with no width (what lines sit
7
+ * on, first and last touching the plot edges).
8
+ */
9
+
10
+ export interface DiscreteScaleOptions {
11
+ readonly domain: ReadonlyArray<ChartValue>;
12
+ readonly range: readonly [number, number];
13
+ readonly locale: string;
14
+ /** Share of a step left empty between two bands. 0 = touching bars. */
15
+ readonly paddingInner?: number;
16
+ /** Share of a step left empty before the first and after the last slot. */
17
+ readonly paddingOuter?: number;
18
+ }
19
+
20
+ const DEFAULT_BAND_PADDING_INNER = 0.2;
21
+ const DEFAULT_BAND_PADDING_OUTER = 0.1;
22
+
23
+ /** A category's identity: two equal dates are one category. */
24
+ export function categoryKey(value: ChartValue): string {
25
+ return value instanceof Date ? `${value.getTime()}` : String(value);
26
+ }
27
+
28
+ /** A category as a label. A year stays "2026", never "2 026". */
29
+ export function formatCategory(value: ChartValue, locale: string): string {
30
+ return value instanceof Date ? formatDate(value, locale) : String(value);
31
+ }
32
+
33
+ /**
34
+ * Labels for a row of date categories, as short as the dates allow: years
35
+ * when every date is a New Year, months when every date is a first of the
36
+ * month (with the year where it turns), days when every date is a midnight.
37
+ */
38
+ function dateCategoryFormatter(
39
+ domain: ReadonlyArray<ChartValue>,
40
+ locale: string,
41
+ ): ((value: ChartValue) => string) | undefined {
42
+ const dates = domain.filter((value): value is Date => value instanceof Date);
43
+ if (dates.length === 0 || dates.length !== domain.length) {
44
+ return undefined;
45
+ }
46
+ const midnight = dates.every((date) => date.getTime() % 86_400_000 === 0);
47
+ const firstOfMonth =
48
+ midnight && dates.every((date) => date.getUTCDate() === 1);
49
+ const newYear =
50
+ firstOfMonth && dates.every((date) => date.getUTCMonth() === 0);
51
+ if (newYear) {
52
+ const year = dateFormat(locale, { year: "numeric" });
53
+ return (value) => year.format(value as Date);
54
+ }
55
+ if (firstOfMonth) {
56
+ const month = dateFormat(locale, { month: "short" });
57
+ const monthYear = dateFormat(locale, { month: "short", year: "numeric" });
58
+ const first = dates[0].getTime();
59
+ return (value) => {
60
+ const date = value as Date;
61
+ return date.getUTCMonth() === 0 || date.getTime() === first
62
+ ? monthYear.format(date)
63
+ : month.format(date);
64
+ };
65
+ }
66
+ if (midnight) {
67
+ const day = dateFormat(locale, { day: "numeric", month: "short" });
68
+ return (value) => day.format(value as Date);
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ function clamp(value: number, low: number, high: number): number {
74
+ return Math.min(Math.max(value, low), high);
75
+ }
76
+
77
+ function indexer(domain: ReadonlyArray<ChartValue>) {
78
+ const indexByKey = new Map<string, number>();
79
+ for (const [index, value] of domain.entries()) {
80
+ const key = categoryKey(value);
81
+ if (!indexByKey.has(key)) {
82
+ indexByKey.set(key, index);
83
+ }
84
+ }
85
+ return (value: ChartValue) => indexByKey.get(categoryKey(value)) ?? 0;
86
+ }
87
+
88
+ export function bandScale(options: DiscreteScaleOptions): ChartPositionScale {
89
+ const {
90
+ domain,
91
+ range,
92
+ locale,
93
+ paddingInner = DEFAULT_BAND_PADDING_INNER,
94
+ paddingOuter = DEFAULT_BAND_PADDING_OUTER,
95
+ } = options;
96
+ const [start, end] = range;
97
+ const count = domain.length;
98
+ const step =
99
+ (end - start) / Math.max(1, count - paddingInner + paddingOuter * 2);
100
+ const bandwidth = Math.abs(step * (1 - paddingInner));
101
+ const origin = start + step * paddingOuter;
102
+ const indexOf = indexer(domain);
103
+ // A reversed range (a y band running top to bottom is not, but a flipped
104
+ // one is) still opens each band towards larger pixels.
105
+ const map = (value: ChartValue) =>
106
+ origin + step * indexOf(value) - (step < 0 ? bandwidth : 0);
107
+
108
+ return {
109
+ kind: "band",
110
+ domain,
111
+ range,
112
+ bandwidth,
113
+ map,
114
+ center: (value) => map(value) + bandwidth / 2,
115
+ invert: (pixel) =>
116
+ domain[
117
+ count === 0
118
+ ? 0
119
+ : clamp(Math.floor((pixel - origin) / step), 0, count - 1)
120
+ ],
121
+ ticks: () => domain,
122
+ format:
123
+ dateCategoryFormatter(domain, locale) ??
124
+ ((value) => formatCategory(value, locale)),
125
+ };
126
+ }
127
+
128
+ export function pointScale(options: DiscreteScaleOptions): ChartPositionScale {
129
+ const { domain, range, locale, paddingOuter = 0 } = options;
130
+ const [start, end] = range;
131
+ const count = domain.length;
132
+ const step = (end - start) / Math.max(1, count - 1 + paddingOuter * 2);
133
+ const origin = start + step * paddingOuter;
134
+ const indexOf = indexer(domain);
135
+ const map = (value: ChartValue) =>
136
+ count <= 1 ? start + (end - start) / 2 : origin + step * indexOf(value);
137
+
138
+ return {
139
+ kind: "point",
140
+ domain,
141
+ range,
142
+ bandwidth: 0,
143
+ map,
144
+ center: map,
145
+ invert: (pixel) =>
146
+ domain[
147
+ count === 0
148
+ ? 0
149
+ : clamp(Math.round((pixel - origin) / step), 0, count - 1)
150
+ ],
151
+ ticks: () => domain,
152
+ format:
153
+ dateCategoryFormatter(domain, locale) ??
154
+ ((value) => formatCategory(value, locale)),
155
+ };
156
+ }
@@ -0,0 +1,158 @@
1
+ import type { ChartPositionScaleOptions } from "#/core/define-chart.ts";
2
+ import { linearScale, logScale, toNumber } from "#/core/scales/continuous.ts";
3
+ import { bandScale, categoryKey, pointScale } from "#/core/scales/discrete.ts";
4
+ import { timeScale } from "#/core/scales/time.ts";
5
+ import { niceDomain } from "#/core/ticks.ts";
6
+ import type {
7
+ ChartChannel,
8
+ ChartPositionScale,
9
+ ChartPositionScaleKind,
10
+ ChartValue,
11
+ } from "#/core/types.ts";
12
+
13
+ /**
14
+ * What a scale will be once the plot size is known: its kind and its domain,
15
+ * read off every channel the marks put on it. Split from the range so the
16
+ * axes can measure their tick labels before the plot rectangle exists.
17
+ */
18
+ export interface ScalePlan {
19
+ readonly kind: ChartPositionScaleKind;
20
+ readonly domain: ReadonlyArray<ChartValue>;
21
+ readonly label: string | undefined;
22
+ /** The band padding a mark asked for, when the options name none. */
23
+ readonly padding?: number;
24
+ readonly paddingOuter?: number;
25
+ }
26
+
27
+ function inferKind(
28
+ channels: ReadonlyArray<ChartChannel>,
29
+ values: ReadonlyArray<ChartValue>,
30
+ ): ChartPositionScaleKind {
31
+ if (channels.some((channel) => channel.discrete === "band")) {
32
+ return "band";
33
+ }
34
+ const askedPoint = channels.some((channel) => channel.discrete === "point");
35
+ if (values.length > 0 && values.every((value) => value instanceof Date)) {
36
+ return askedPoint ? "point" : "time";
37
+ }
38
+ if (values.length > 0 && values.every((value) => typeof value === "number")) {
39
+ return askedPoint ? "point" : "linear";
40
+ }
41
+ return "point";
42
+ }
43
+
44
+ function distinct(
45
+ values: ReadonlyArray<ChartValue>,
46
+ ): ReadonlyArray<ChartValue> {
47
+ const seen = new Set<string>();
48
+ const kept: ChartValue[] = [];
49
+ for (const value of values) {
50
+ const key = categoryKey(value);
51
+ if (!seen.has(key)) {
52
+ seen.add(key);
53
+ kept.push(value);
54
+ }
55
+ }
56
+ return kept;
57
+ }
58
+
59
+ function continuousDomain(
60
+ values: ReadonlyArray<ChartValue>,
61
+ options: { readonly zero: boolean; readonly nice: boolean },
62
+ kind: ChartPositionScaleKind,
63
+ ): readonly [number, number] {
64
+ let low = Number.POSITIVE_INFINITY;
65
+ let high = Number.NEGATIVE_INFINITY;
66
+ for (const value of values) {
67
+ const number = toNumber(value);
68
+ if (Number.isFinite(number)) {
69
+ low = Math.min(low, number);
70
+ high = Math.max(high, number);
71
+ }
72
+ }
73
+ if (!Number.isFinite(low)) {
74
+ return [0, 1];
75
+ }
76
+ if (options.zero && kind === "linear") {
77
+ low = Math.min(low, 0);
78
+ high = Math.max(high, 0);
79
+ }
80
+ if (options.nice && kind === "linear") {
81
+ return niceDomain(low, high);
82
+ }
83
+ return [low, high];
84
+ }
85
+
86
+ export function planScale(
87
+ channels: ReadonlyArray<ChartChannel>,
88
+ options: ChartPositionScaleOptions | undefined,
89
+ axis: "x" | "y",
90
+ ): ScalePlan | undefined {
91
+ if (channels.length === 0) {
92
+ return undefined;
93
+ }
94
+ const values = channels.flatMap((channel) => channel.values);
95
+ const kind = options?.type ?? inferKind(channels, values);
96
+ const label =
97
+ options?.label ?? channels.find((channel) => channel.label)?.label;
98
+ if (kind === "band" || kind === "point") {
99
+ const padding = channels.find(
100
+ (channel) => channel.padding !== undefined,
101
+ )?.padding;
102
+ const paddingOuter = channels.find(
103
+ (channel) => channel.paddingOuter !== undefined,
104
+ )?.paddingOuter;
105
+ return {
106
+ kind,
107
+ domain: options?.domain ?? distinct(values),
108
+ label,
109
+ padding,
110
+ paddingOuter: paddingOuter ?? padding,
111
+ };
112
+ }
113
+ if (options?.domain !== undefined) {
114
+ return { kind, domain: options.domain.map(toNumber), label };
115
+ }
116
+ const zero =
117
+ options?.zero ?? channels.some((channel) => channel.includeZero === true);
118
+ // A value axis (y, or the x of horizontal bars) reads best on round
119
+ // numbers; a position axis (the x of a scatter) on the data's own extent.
120
+ const nice = options?.nice ?? (axis === "y" || zero);
121
+ return {
122
+ kind,
123
+ domain: continuousDomain(values, { zero, nice }, kind),
124
+ label,
125
+ };
126
+ }
127
+
128
+ export function buildScale(
129
+ plan: ScalePlan,
130
+ options: ChartPositionScaleOptions | undefined,
131
+ range: readonly [number, number],
132
+ locale: string,
133
+ tickCount: number,
134
+ ): ChartPositionScale {
135
+ const ordered: readonly [number, number] = options?.reverse
136
+ ? [range[1], range[0]]
137
+ : range;
138
+ if (plan.kind === "band" || plan.kind === "point") {
139
+ const discrete = {
140
+ domain: plan.domain,
141
+ range: ordered,
142
+ locale,
143
+ paddingInner: options?.paddingInner ?? plan.padding,
144
+ paddingOuter: options?.paddingOuter ?? plan.paddingOuter,
145
+ };
146
+ return plan.kind === "band" ? bandScale(discrete) : pointScale(discrete);
147
+ }
148
+ const continuous = {
149
+ domain: [toNumber(plan.domain[0]), toNumber(plan.domain[1])] as const,
150
+ range: ordered,
151
+ locale,
152
+ tickCount,
153
+ };
154
+ if (plan.kind === "time") {
155
+ return timeScale(continuous);
156
+ }
157
+ return plan.kind === "log" ? logScale(continuous) : linearScale(continuous);
158
+ }
@@ -0,0 +1,180 @@
1
+ import { dateFormat } from "#/core/format.ts";
2
+ import {
3
+ type ContinuousScaleOptions,
4
+ linearScale,
5
+ toNumber,
6
+ } from "#/core/scales/continuous.ts";
7
+ import { niceTicks } from "#/core/ticks.ts";
8
+ import type { ChartPositionScale, ChartValue } from "#/core/types.ts";
9
+
10
+ /**
11
+ * Dates in, pixels out. Ticks land on calendar boundaries (midnight, the first
12
+ * of the month, New Year) rather than on round millisecond counts, and their
13
+ * labels only say what changes between two ticks.
14
+ *
15
+ * Everything is UTC. Chart data is mostly day or month buckets written as
16
+ * `2026-03-01`, which JavaScript parses as UTC midnight: reading them in the
17
+ * viewer's zone would push every bucket onto the previous day west of
18
+ * Greenwich.
19
+ */
20
+
21
+ const SECOND = 1000;
22
+ const MINUTE = 60 * SECOND;
23
+ const HOUR = 60 * MINUTE;
24
+ const DAY = 24 * HOUR;
25
+ const WEEK = 7 * DAY;
26
+ const MONTH = 30 * DAY;
27
+ const YEAR = 365 * DAY;
28
+
29
+ type Unit = "second" | "minute" | "hour" | "day" | "week" | "month" | "year";
30
+
31
+ interface TimeInterval {
32
+ readonly unit: Unit;
33
+ readonly step: number;
34
+ /** Approximate length, only to pick the interval closest to the tick count. */
35
+ readonly duration: number;
36
+ }
37
+
38
+ const INTERVALS: ReadonlyArray<TimeInterval> = [
39
+ { unit: "second", step: 1, duration: SECOND },
40
+ { unit: "second", step: 5, duration: 5 * SECOND },
41
+ { unit: "second", step: 15, duration: 15 * SECOND },
42
+ { unit: "second", step: 30, duration: 30 * SECOND },
43
+ { unit: "minute", step: 1, duration: MINUTE },
44
+ { unit: "minute", step: 5, duration: 5 * MINUTE },
45
+ { unit: "minute", step: 15, duration: 15 * MINUTE },
46
+ { unit: "minute", step: 30, duration: 30 * MINUTE },
47
+ { unit: "hour", step: 1, duration: HOUR },
48
+ { unit: "hour", step: 3, duration: 3 * HOUR },
49
+ { unit: "hour", step: 6, duration: 6 * HOUR },
50
+ { unit: "hour", step: 12, duration: 12 * HOUR },
51
+ { unit: "day", step: 1, duration: DAY },
52
+ { unit: "day", step: 2, duration: 2 * DAY },
53
+ { unit: "week", step: 1, duration: WEEK },
54
+ { unit: "month", step: 1, duration: MONTH },
55
+ { unit: "month", step: 3, duration: 3 * MONTH },
56
+ { unit: "month", step: 6, duration: 6 * MONTH },
57
+ { unit: "year", step: 1, duration: YEAR },
58
+ ];
59
+
60
+ const UNIT_MS: Record<"second" | "minute" | "hour" | "day", number> = {
61
+ second: SECOND,
62
+ minute: MINUTE,
63
+ hour: HOUR,
64
+ day: DAY,
65
+ };
66
+
67
+ function chooseInterval(span: number, count: number): TimeInterval {
68
+ const target = span / Math.max(1, count);
69
+ let best = INTERVALS[0];
70
+ for (const interval of INTERVALS) {
71
+ if (
72
+ Math.abs(Math.log(interval.duration / target)) <
73
+ Math.abs(Math.log(best.duration / target))
74
+ ) {
75
+ best = interval;
76
+ }
77
+ }
78
+ return best;
79
+ }
80
+
81
+ function monthTicks(start: number, end: number, step: number): number[] {
82
+ const from = new Date(start);
83
+ const year = from.getUTCFullYear();
84
+ let month = from.getUTCMonth();
85
+ month = Math.ceil(month / step) * step;
86
+ const ticks: number[] = [];
87
+ for (;;) {
88
+ const time = Date.UTC(year + Math.floor(month / 12), month % 12, 1);
89
+ if (time > end) {
90
+ return ticks;
91
+ }
92
+ if (time >= start) {
93
+ ticks.push(time);
94
+ }
95
+ month += step;
96
+ }
97
+ }
98
+
99
+ function intervalTicks(
100
+ start: number,
101
+ end: number,
102
+ interval: TimeInterval,
103
+ ): number[] {
104
+ const { unit, step } = interval;
105
+ if (unit === "year") {
106
+ const years = niceTicks(
107
+ new Date(start).getUTCFullYear(),
108
+ new Date(end).getUTCFullYear(),
109
+ Math.max(1, Math.round((end - start) / YEAR)),
110
+ ).filter(Number.isInteger);
111
+ return years
112
+ .map((year) => Date.UTC(year, 0, 1))
113
+ .filter((time) => time >= start && time <= end);
114
+ }
115
+ if (unit === "month") {
116
+ return monthTicks(start, end, step);
117
+ }
118
+ if (unit === "week") {
119
+ // Weeks start on Monday: 1970-01-05 was one.
120
+ const monday = Date.UTC(1970, 0, 5);
121
+ const first = monday + Math.ceil((start - monday) / WEEK) * WEEK;
122
+ const ticks: number[] = [];
123
+ for (let time = first; time <= end; time += WEEK) {
124
+ ticks.push(time);
125
+ }
126
+ return ticks;
127
+ }
128
+ const size = UNIT_MS[unit] * step;
129
+ const ticks: number[] = [];
130
+ for (let time = Math.ceil(start / size) * size; time <= end; time += size) {
131
+ ticks.push(time);
132
+ }
133
+ return ticks;
134
+ }
135
+
136
+ function tickLabel(time: number, unit: Unit, locale: string): string {
137
+ const date = new Date(time);
138
+ if (unit === "year") {
139
+ return dateFormat(locale, { year: "numeric" }).format(date);
140
+ }
141
+ if (unit === "month") {
142
+ // The year only where it turns, so a row of months stays short.
143
+ return date.getUTCMonth() === 0
144
+ ? dateFormat(locale, { month: "short", year: "numeric" }).format(date)
145
+ : dateFormat(locale, { month: "short" }).format(date);
146
+ }
147
+ if (unit === "day" || unit === "week") {
148
+ return dateFormat(locale, { day: "numeric", month: "short" }).format(date);
149
+ }
150
+ if (unit === "second") {
151
+ return dateFormat(locale, {
152
+ hour: "2-digit",
153
+ minute: "2-digit",
154
+ second: "2-digit",
155
+ }).format(date);
156
+ }
157
+ return dateFormat(locale, { hour: "2-digit", minute: "2-digit" }).format(
158
+ date,
159
+ );
160
+ }
161
+
162
+ export function timeScale(options: ContinuousScaleOptions): ChartPositionScale {
163
+ const { domain, locale, tickCount } = options;
164
+ const linear = linearScale(options);
165
+ const [start, end] = [Math.min(...domain), Math.max(...domain)];
166
+ let unit = chooseInterval(end - start, tickCount).unit;
167
+
168
+ return {
169
+ ...linear,
170
+ kind: "time",
171
+ domain: [new Date(domain[0]), new Date(domain[1])],
172
+ invert: (pixel) => new Date(toNumber(linear.invert(pixel))),
173
+ ticks: (count = tickCount) => {
174
+ const interval = chooseInterval(end - start, count);
175
+ unit = interval.unit;
176
+ return intervalTicks(start, end, interval).map((time) => new Date(time));
177
+ },
178
+ format: (value: ChartValue) => tickLabel(toNumber(value), unit, locale),
179
+ };
180
+ }
@@ -0,0 +1,42 @@
1
+ import type { ChartTextMeasurer } from "#/core/types.ts";
2
+
3
+ /**
4
+ * Text width without a browser. The server and the first client render both
5
+ * lay out with this estimate, so the hydrated markup matches; the browser
6
+ * swaps in real metrics right after.
7
+ *
8
+ * Widths are in ems for a typical UI sans-serif: narrow letters, wide
9
+ * letters, digits (tabular in most UI fonts) and everything else.
10
+ */
11
+ const NARROW = /[ijlt.,:;'!|()[\]\s]/;
12
+ const WIDE = /[mwMW@%]/;
13
+ const DIGIT = /[0-9]/;
14
+ const UPPER = /[A-Z]/;
15
+
16
+ const EM_NARROW = 0.3;
17
+ const EM_WIDE = 0.85;
18
+ const EM_DIGIT = 0.58;
19
+ const EM_UPPER = 0.66;
20
+ const EM_OTHER = 0.54;
21
+ const BOLD_FACTOR = 1.06;
22
+ const BOLD_WEIGHT = 600;
23
+
24
+ function emWidth(character: string): number {
25
+ if (NARROW.test(character)) return EM_NARROW;
26
+ if (WIDE.test(character)) return EM_WIDE;
27
+ if (DIGIT.test(character)) return EM_DIGIT;
28
+ if (UPPER.test(character)) return EM_UPPER;
29
+ return EM_OTHER;
30
+ }
31
+
32
+ export const estimateTextWidth: ChartTextMeasurer = (
33
+ text,
34
+ fontSize,
35
+ fontWeight = 400,
36
+ ) => {
37
+ let ems = 0;
38
+ for (const character of text) {
39
+ ems += emWidth(character);
40
+ }
41
+ return ems * fontSize * (fontWeight >= BOLD_WEIGHT ? BOLD_FACTOR : 1);
42
+ };