@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,26 @@
1
+ import type { ChartTheme } from "#/core/types.ts";
2
+
3
+ /** What the palette falls back to on a page without the kit's `--chart-N` tokens. */
4
+ const FALLBACK_PALETTE = [
5
+ "oklch(0.646 0.222 41.116)",
6
+ "oklch(0.6 0.118 184.704)",
7
+ "oklch(0.398 0.07 227.392)",
8
+ "oklch(0.828 0.189 84.429)",
9
+ "oklch(0.769 0.188 70.08)",
10
+ ];
11
+
12
+ /**
13
+ * Every colour is a CSS variable with a fallback, so a chart picks up the
14
+ * kit's tokens (`--chart-1`…, `--muted-foreground`, `--border`) when the page
15
+ * has them and still draws legibly when it does not.
16
+ */
17
+ export const DEFAULT_THEME: ChartTheme = {
18
+ palette: FALLBACK_PALETTE.map(
19
+ (fallback, index) => `var(--chart-${index + 1}, ${fallback})`,
20
+ ),
21
+ foreground: "var(--foreground, currentColor)",
22
+ muted: "var(--muted-foreground, currentColor)",
23
+ grid: "var(--border, color-mix(in oklab, currentColor 15%, transparent))",
24
+ background: "var(--background, Canvas)",
25
+ fontSize: 12,
26
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Round tick values for a continuous axis. The step is always 1, 2, 5 or 10
3
+ * times a power of ten, which is what makes an axis readable: 0/25/50/75/100
4
+ * rather than 0/23.4/46.8.
5
+ */
6
+
7
+ /** Steps the eye reads as round, in units of the current power of ten. */
8
+ const NICE_FACTORS = [
9
+ { threshold: Math.SQRT2, factor: 1 },
10
+ { threshold: Math.sqrt(10), factor: 2 },
11
+ { threshold: Math.sqrt(50), factor: 5 },
12
+ ] as const;
13
+
14
+ const DEFAULT_TICK_COUNT = 5;
15
+ /** Beyond this many decimals the rounding helper stops fighting IEEE noise. */
16
+ const MAX_TICK_PRECISION = 12;
17
+
18
+ /**
19
+ * Drops the floating-point dust `i * step` leaves behind (0.30000000000000004)
20
+ * by rounding to the number of decimals the step itself carries.
21
+ */
22
+ function roundToStep(value: number, step: number): number {
23
+ if (step === 0 || !Number.isFinite(step)) {
24
+ return value;
25
+ }
26
+ const decimals = Math.max(0, -Math.floor(Math.log10(Math.abs(step))));
27
+ return Number(value.toFixed(Math.min(decimals, MAX_TICK_PRECISION)));
28
+ }
29
+
30
+ /**
31
+ * The round step closest to covering `[min, max]` in about `count` intervals.
32
+ * Returns `0` for a degenerate span, which callers read as "no ticks".
33
+ */
34
+ export function tickStep(
35
+ min: number,
36
+ max: number,
37
+ count: number = DEFAULT_TICK_COUNT,
38
+ ): number {
39
+ const span = max - min;
40
+ if (!Number.isFinite(span) || span <= 0 || count <= 0) {
41
+ return 0;
42
+ }
43
+ const rough = span / count;
44
+ const power = Math.floor(Math.log10(rough));
45
+ const error = rough / 10 ** power;
46
+ const match = NICE_FACTORS.find((candidate) => error < candidate.threshold);
47
+ return (match?.factor ?? 10) * 10 ** power;
48
+ }
49
+
50
+ /**
51
+ * Round values strictly inside `[min, max]`. An axis that has been through
52
+ * `niceDomain` first gets ticks on both bounds; a raw domain does not, on
53
+ * purpose — a tick outside the data would be a lie.
54
+ */
55
+ export function niceTicks(
56
+ min: number,
57
+ max: number,
58
+ count: number = DEFAULT_TICK_COUNT,
59
+ ): ReadonlyArray<number> {
60
+ if (min === max) {
61
+ return [min];
62
+ }
63
+ const step = tickStep(min, max, count);
64
+ if (step === 0) {
65
+ return [];
66
+ }
67
+ const first = Math.ceil(min / step);
68
+ const last = Math.floor(max / step);
69
+ const ticks: number[] = [];
70
+ for (let index = first; index <= last; index += 1) {
71
+ ticks.push(roundToStep(index * step, step));
72
+ }
73
+ return ticks;
74
+ }
75
+
76
+ /**
77
+ * Widens `[min, max]` outwards to the nearest round step, so the axis starts
78
+ * and ends on a tick instead of mid-air.
79
+ */
80
+ export function niceDomain(
81
+ min: number,
82
+ max: number,
83
+ count: number = DEFAULT_TICK_COUNT,
84
+ ): readonly [number, number] {
85
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
86
+ return [0, 1];
87
+ }
88
+ if (min === max) {
89
+ return min === 0 ? [0, 1] : [Math.min(0, min), Math.max(0, max)];
90
+ }
91
+ const step = tickStep(min, max, count);
92
+ if (step === 0) {
93
+ return [min, max];
94
+ }
95
+ return [
96
+ roundToStep(Math.floor(min / step) * step, step),
97
+ roundToStep(Math.ceil(max / step) * step, step),
98
+ ];
99
+ }
100
+
101
+ /**
102
+ * The `[min, max]` of a set of values, always including zero so bars are
103
+ * measured from a real baseline rather than from an arbitrary floor.
104
+ */
105
+ export function extentFromZero(
106
+ values: ReadonlyArray<number>,
107
+ ): readonly [number, number] {
108
+ let min = 0;
109
+ let max = 0;
110
+ for (const value of values) {
111
+ if (!Number.isFinite(value)) {
112
+ continue;
113
+ }
114
+ min = Math.min(min, value);
115
+ max = Math.max(max, value);
116
+ }
117
+ return [min, max];
118
+ }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * The vocabulary of the chart pipeline. A definition lists marks; compiling it
3
+ * resolves scales from the marks' channels, lays out the guides, and asks each
4
+ * mark for its scene. The scene is plain data with stable keys: the SVG and
5
+ * Canvas renderers draw the same scene, and hit-testing, focus, tooltips and
6
+ * the data table all read it, never the DOM.
7
+ */
8
+
9
+ /** A value a channel can carry onto a scale. */
10
+ export type ChartValue = number | string | Date;
11
+
12
+ /** A field of the datum, or a function of it. */
13
+ export type ChartAccessor<TDatum, TValue> =
14
+ | (keyof TDatum & string)
15
+ | ((datum: TDatum, index: number) => TValue);
16
+
17
+ export interface ChartRect {
18
+ readonly x: number;
19
+ readonly y: number;
20
+ readonly width: number;
21
+ readonly height: number;
22
+ }
23
+
24
+ export interface ChartMargin {
25
+ readonly top: number;
26
+ readonly right: number;
27
+ readonly bottom: number;
28
+ readonly left: number;
29
+ }
30
+
31
+ /** `[topLeft, topRight, bottomRight, bottomLeft]`, clockwise. */
32
+ export type ChartCorners = readonly [number, number, number, number];
33
+
34
+ /** How a line or area joins its points. */
35
+ export type ChartCurve = "linear" | "monotone" | "step";
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Scales
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export type ChartPositionScaleKind =
42
+ | "linear"
43
+ | "log"
44
+ | "time"
45
+ | "band"
46
+ | "point";
47
+
48
+ /**
49
+ * One positional scale. Every kind answers the same questions, so a mark never
50
+ * branches on the kind: `map` places the start of a value's slot, `center` its
51
+ * middle (they differ only on a band scale), and `invert` reads a pixel back.
52
+ */
53
+ export interface ChartPositionScale {
54
+ readonly kind: ChartPositionScaleKind;
55
+ /** `[min, max]` for continuous kinds, every category for discrete ones. */
56
+ readonly domain: ReadonlyArray<ChartValue>;
57
+ readonly range: readonly [number, number];
58
+ readonly map: (value: ChartValue) => number;
59
+ readonly center: (value: ChartValue) => number;
60
+ readonly invert: (pixel: number) => ChartValue;
61
+ /** Slot width on a band scale, `0` everywhere else. */
62
+ readonly bandwidth: number;
63
+ readonly ticks: (count?: number) => ReadonlyArray<ChartValue>;
64
+ readonly format: (value: ChartValue) => string;
65
+ }
66
+
67
+ export interface ChartColorScale {
68
+ readonly kind: "ordinal" | "sequential";
69
+ readonly domain: ReadonlyArray<ChartValue>;
70
+ readonly map: (value: ChartValue) => string;
71
+ }
72
+
73
+ export interface ChartScales {
74
+ readonly x?: ChartPositionScale;
75
+ readonly y?: ChartPositionScale;
76
+ readonly color?: ChartColorScale;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Scene
81
+ // ---------------------------------------------------------------------------
82
+
83
+ export interface ChartPaint {
84
+ readonly fill?: string;
85
+ readonly stroke?: string;
86
+ readonly strokeWidth?: number;
87
+ readonly strokeDasharray?: string;
88
+ readonly strokeLinecap?: "butt" | "round" | "square";
89
+ readonly strokeLinejoin?: "miter" | "round" | "bevel";
90
+ readonly opacity?: number;
91
+ readonly fillOpacity?: number;
92
+ readonly strokeOpacity?: number;
93
+ }
94
+
95
+ export interface ChartTextPaint extends ChartPaint {
96
+ readonly fontSize?: number;
97
+ readonly fontWeight?: number;
98
+ readonly textAnchor?: "start" | "middle" | "end";
99
+ readonly baseline?: "top" | "middle" | "bottom" | "alphabetic";
100
+ }
101
+
102
+ interface SceneNodeBase {
103
+ /** Stable across renders of the same data: what renderers reconcile on. */
104
+ readonly key: string;
105
+ /** The series the node draws, so a legend toggle can hide it. */
106
+ readonly series?: string;
107
+ /** A role for styling and tests: "mark", "axis", "grid", "label"… */
108
+ readonly role?: string;
109
+ }
110
+
111
+ export interface SceneGroup extends SceneNodeBase {
112
+ readonly kind: "group";
113
+ /** Shifts the children: a facet's cell drawn at its place in the grid. Clips are in the shifted space. */
114
+ readonly translate?: { readonly x: number; readonly y: number };
115
+ readonly clip?: ChartRect;
116
+ readonly children: ReadonlyArray<SceneNode>;
117
+ }
118
+
119
+ export interface SceneRect extends SceneNodeBase {
120
+ readonly kind: "rect";
121
+ readonly x: number;
122
+ readonly y: number;
123
+ readonly width: number;
124
+ readonly height: number;
125
+ readonly corners?: ChartCorners;
126
+ readonly paint: ChartPaint;
127
+ }
128
+
129
+ /** Any outline: a line, an area, a wedge, a country. `d` is SVG path data, which `Path2D` reads as well. */
130
+ export interface ScenePath extends SceneNodeBase {
131
+ readonly kind: "path";
132
+ readonly d: string;
133
+ readonly paint: ChartPaint;
134
+ }
135
+
136
+ export interface SceneCircle extends SceneNodeBase {
137
+ readonly kind: "circle";
138
+ readonly cx: number;
139
+ readonly cy: number;
140
+ readonly r: number;
141
+ readonly paint: ChartPaint;
142
+ }
143
+
144
+ export interface SceneLine extends SceneNodeBase {
145
+ readonly kind: "line";
146
+ readonly x1: number;
147
+ readonly y1: number;
148
+ readonly x2: number;
149
+ readonly y2: number;
150
+ readonly paint: ChartPaint;
151
+ }
152
+
153
+ export interface SceneText extends SceneNodeBase {
154
+ readonly kind: "text";
155
+ readonly x: number;
156
+ readonly y: number;
157
+ readonly text: string;
158
+ /** Degrees, clockwise, around `(x, y)`. */
159
+ readonly rotate?: number;
160
+ readonly paint: ChartTextPaint;
161
+ }
162
+
163
+ export type SceneNode =
164
+ | SceneGroup
165
+ | SceneRect
166
+ | ScenePath
167
+ | SceneCircle
168
+ | SceneLine
169
+ | SceneText;
170
+
171
+ /** The area a pointer has to land in to hit a point, beyond plain proximity. */
172
+ export type ChartHitShape =
173
+ | { readonly kind: "rect"; readonly rect: ChartRect }
174
+ | {
175
+ readonly kind: "arc";
176
+ readonly cx: number;
177
+ readonly cy: number;
178
+ readonly innerRadius: number;
179
+ readonly outerRadius: number;
180
+ /** Degrees clockwise from twelve o'clock. */
181
+ readonly startAngle: number;
182
+ readonly endAngle: number;
183
+ };
184
+
185
+ /**
186
+ * One datum as the reader meets it: what the keyboard steps through, what the
187
+ * tooltip and the live region describe, what the data table lists.
188
+ */
189
+ export interface ChartPoint<TDatum = unknown> {
190
+ readonly key: string;
191
+ readonly markId: string;
192
+ readonly index: number;
193
+ readonly datum: TDatum;
194
+ /** Anchor in chart pixels: where the focus ring and the tooltip go. */
195
+ readonly x: number;
196
+ readonly y: number;
197
+ readonly xValue?: ChartValue;
198
+ readonly yValue?: ChartValue;
199
+ readonly series?: string;
200
+ readonly seriesLabel?: string;
201
+ readonly color: string;
202
+ /** What the point reads as, without its series: "March 2026", "Paris". */
203
+ readonly title: string;
204
+ /** Its value, formatted: "1 240". */
205
+ readonly value: string;
206
+ readonly hit?: ChartHitShape;
207
+ /** The facet cell the point belongs to, on a faceted chart. */
208
+ readonly facet?: string;
209
+ }
210
+
211
+ export interface ChartLegendItem {
212
+ readonly key: string;
213
+ readonly label: string;
214
+ readonly color: string;
215
+ readonly shape: "square" | "line" | "dashed" | "dot";
216
+ }
217
+
218
+ /**
219
+ * How the keyboard walks the points. `x` groups every point that shares an x
220
+ * value into one stop (a column of a multi-series chart), `y` does the same
221
+ * along y for horizontal bars, `point` visits each point on its own.
222
+ */
223
+ export type ChartFocusOrder = "x" | "y" | "point";
224
+
225
+ export interface ChartScene {
226
+ readonly width: number;
227
+ readonly height: number;
228
+ readonly plot: ChartRect;
229
+ readonly nodes: ReadonlyArray<SceneNode>;
230
+ readonly points: ReadonlyArray<ChartPoint>;
231
+ readonly legend: ReadonlyArray<ChartLegendItem>;
232
+ readonly focusOrder: ChartFocusOrder;
233
+ readonly scales: ChartScales;
234
+ /** Column headings of the data table and tooltip: the axis labels. */
235
+ readonly xLabel: string;
236
+ readonly yLabel: string;
237
+ readonly theme: ChartTheme;
238
+ readonly locale: string;
239
+ /** A faceted chart's cells, each with its own plot, in reading order. */
240
+ readonly cells?: ReadonlyArray<{
241
+ readonly key: string;
242
+ readonly label: string;
243
+ readonly plot: ChartRect;
244
+ }>;
245
+ /** The sequential colour scale's ramp, for its legend: the colours and the ends' labels. */
246
+ readonly colorRamp?: {
247
+ readonly stops: ReadonlyArray<string>;
248
+ readonly low: string;
249
+ readonly high: string;
250
+ };
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Marks
255
+ // ---------------------------------------------------------------------------
256
+
257
+ /** The values a mark puts on a scale, so the scale's domain can cover them. */
258
+ export interface ChartChannel {
259
+ readonly values: ReadonlyArray<ChartValue>;
260
+ /** Bars need a band; the first mark that asks for one gets it. */
261
+ readonly discrete?: "band" | "point";
262
+ /** A bar or an area measures from zero, so zero must be on the scale. */
263
+ readonly includeZero?: boolean;
264
+ /** The field the values come from: the axis title when none is given. */
265
+ readonly label?: string;
266
+ /** Pixels the mark reaches past its value (a dot's radius), kept inside the plot. */
267
+ readonly inset?: number;
268
+ /** Band padding the mark draws best with: cells touch, bars breathe. */
269
+ readonly padding?: number;
270
+ /** Outer band padding, when it differs: room for a ridge rising past its row. */
271
+ readonly paddingOuter?: number;
272
+ }
273
+
274
+ /**
275
+ * A map projection, described without naming the library that implements it
276
+ * (`@voila.dev/chart/geo` does, with d3-geo). Declared once on the
277
+ * definition and fitted to the plot, so every map mark draws on the same one.
278
+ */
279
+ export interface ChartProjection {
280
+ readonly fit: (plot: ChartRect) => ChartFittedProjection;
281
+ }
282
+
283
+ export interface ChartFittedProjection {
284
+ /** Longitude and latitude to chart pixels; `undefined` when clipped away. */
285
+ readonly project: (
286
+ longitude: number,
287
+ latitude: number,
288
+ ) => { x: number; y: number } | undefined;
289
+ /** Path data for a GeoJSON object. */
290
+ readonly path: (geometry: unknown) => string;
291
+ readonly centroid: (geometry: unknown) => { x: number; y: number };
292
+ readonly bounds: (geometry: unknown) => ChartRect;
293
+ }
294
+
295
+ export type ChartTextMeasurer = (
296
+ text: string,
297
+ fontSize: number,
298
+ fontWeight?: number,
299
+ ) => number;
300
+
301
+ export interface ChartTheme {
302
+ readonly palette: ReadonlyArray<string>;
303
+ readonly foreground: string;
304
+ readonly muted: string;
305
+ readonly grid: string;
306
+ readonly background: string;
307
+ readonly fontSize: number;
308
+ }
309
+
310
+ export interface ChartMarkContext {
311
+ readonly scales: ChartScales;
312
+ readonly plot: ChartRect;
313
+ readonly theme: ChartTheme;
314
+ readonly measureText: ChartTextMeasurer;
315
+ readonly locale: string;
316
+ /** A colour for a mark with no color channel: its place in the palette. */
317
+ readonly paletteColor: (index: number) => string;
318
+ /** The colour of one value of the color channel. */
319
+ readonly colorOf: (value: ChartValue) => string;
320
+ /** The display name of one value of the color channel. */
321
+ readonly seriesLabel: (value: ChartValue) => string;
322
+ readonly formatX: (value: ChartValue) => string;
323
+ readonly formatY: (value: ChartValue) => string;
324
+ readonly markIndex: number;
325
+ /** The definition's projection, fitted to the plot. Map marks need it. */
326
+ readonly projection?: ChartFittedProjection;
327
+ }
328
+
329
+ export interface ChartMarkScene {
330
+ readonly nodes: ReadonlyArray<SceneNode>;
331
+ readonly points?: ReadonlyArray<ChartPoint>;
332
+ readonly legend?: ReadonlyArray<ChartLegendItem>;
333
+ }
334
+
335
+ /**
336
+ * What every mark factory returns. A cartesian mark declares channels so the
337
+ * scales cover it; a `frame` mark (pie, treemap, map) owns the whole plot and
338
+ * ignores the x and y scales.
339
+ */
340
+ export interface ChartMark {
341
+ readonly id?: string;
342
+ readonly coordinate: "cartesian" | "frame";
343
+ readonly channels: {
344
+ readonly x?: ChartChannel;
345
+ readonly y?: ChartChannel;
346
+ readonly color?: ChartChannel;
347
+ };
348
+ /** The keyboard order this mark reads best in. */
349
+ readonly focusOrder?: ChartFocusOrder;
350
+ /** `false` when the mark already names its colours itself (a labelled funnel). */
351
+ readonly colorLegend?: boolean;
352
+ /** What a frame mark's categories and values are called, for the table and the tooltip. */
353
+ readonly titles?: {
354
+ readonly category?: string;
355
+ readonly value?: string;
356
+ };
357
+ readonly render: (context: ChartMarkContext) => ChartMarkScene;
358
+ }
@@ -0,0 +1,200 @@
1
+ import {
2
+ forceCenter,
3
+ forceCollide,
4
+ forceLink,
5
+ forceManyBody,
6
+ forceSimulation,
7
+ forceX,
8
+ forceY,
9
+ type SimulationLinkDatum,
10
+ type SimulationNodeDatum,
11
+ } from "d3-force";
12
+ import { markId } from "#/core/marks/shared.ts";
13
+ import type { ChartMark, ChartPoint, SceneNode } from "#/core/types.ts";
14
+
15
+ export interface ForceNode {
16
+ readonly id: string;
17
+ readonly label?: string;
18
+ /** Colour group. */
19
+ readonly group?: string;
20
+ }
21
+
22
+ export interface ForceLink {
23
+ readonly source: string;
24
+ readonly target: string;
25
+ }
26
+
27
+ export interface ForceOptions {
28
+ readonly id?: string;
29
+ readonly radius?: number;
30
+ /** Settling steps. More is tidier and slower. */
31
+ readonly ticks?: number;
32
+ readonly labels?: boolean;
33
+ readonly tip?: boolean;
34
+ }
35
+
36
+ interface SimulatedNode extends SimulationNodeDatum, ForceNode {}
37
+
38
+ const DEFAULT_RADIUS = 5;
39
+ const DEFAULT_TICKS = 300;
40
+
41
+ /** A fixed-seed random stream: the same graph settles the same way on the server and in the browser. */
42
+ function seeded(seed: number): () => number {
43
+ let state = seed;
44
+ return () => {
45
+ state = (state * 1664525 + 1013904223) % 4294967296;
46
+ return state / 4294967296;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * A network laid out by forces: linked nodes pull together, all nodes push
52
+ * apart. The simulation runs to rest before drawing, with a fixed seed, so
53
+ * the picture is the same every time; a reader moves through the nodes with
54
+ * the keyboard, each announced with how many links it has.
55
+ */
56
+ export function forceGraph(
57
+ data: {
58
+ readonly nodes: ReadonlyArray<ForceNode>;
59
+ readonly links: ReadonlyArray<ForceLink>;
60
+ },
61
+ options: ForceOptions = {},
62
+ ): ChartMark {
63
+ const groups = data.nodes.map((node) => node.group ?? "");
64
+ return {
65
+ id: options.id,
66
+ coordinate: "frame",
67
+ focusOrder: "point",
68
+ channels: groups.some((group) => group !== "")
69
+ ? { color: { values: groups.filter((group) => group !== "") } }
70
+ : {},
71
+ render(context) {
72
+ const id = markId(options.id, "force", context);
73
+ const { plot, theme } = context;
74
+ const radius = options.radius ?? DEFAULT_RADIUS;
75
+ const simulated: SimulatedNode[] = data.nodes.map((node) => ({
76
+ ...node,
77
+ }));
78
+ const links = data.links.map((link) => ({
79
+ ...link,
80
+ })) as SimulationLinkDatum<SimulatedNode>[];
81
+ const simulation = forceSimulation(simulated)
82
+ .randomSource(seeded(1))
83
+ .force(
84
+ "link",
85
+ forceLink<SimulatedNode, SimulationLinkDatum<SimulatedNode>>(links)
86
+ .id((node) => node.id)
87
+ .distance(radius * 10),
88
+ )
89
+ .force("charge", forceManyBody().strength(-radius * 20))
90
+ .force("collide", forceCollide(radius * 3))
91
+ .force("center", forceCenter(0, 0))
92
+ // A gentle pull to the middle keeps separate groups on one picture.
93
+ .force("x", forceX(0).strength(0.08))
94
+ .force("y", forceY(0).strength(0.08))
95
+ .stop();
96
+ simulation.tick(options.ticks ?? DEFAULT_TICKS);
97
+ const xs = simulated.map((node) => node.x ?? 0);
98
+ const ys = simulated.map((node) => node.y ?? 0);
99
+ const [minX, maxX, minY, maxY] = [
100
+ Math.min(...xs),
101
+ Math.max(...xs),
102
+ Math.min(...ys),
103
+ Math.max(...ys),
104
+ ];
105
+ const pad = radius * 2;
106
+ const scale = Math.min(
107
+ (plot.width - pad * 2) / Math.max(1, maxX - minX),
108
+ (plot.height - pad * 2) / Math.max(1, maxY - minY),
109
+ 1.5,
110
+ );
111
+ const place = (node: SimulatedNode) => ({
112
+ x:
113
+ plot.x + plot.width / 2 + ((node.x ?? 0) - (minX + maxX) / 2) * scale,
114
+ y:
115
+ plot.y +
116
+ plot.height / 2 +
117
+ ((node.y ?? 0) - (minY + maxY) / 2) * scale,
118
+ });
119
+ const degree = new Map<string, number>();
120
+ const nodes: SceneNode[] = [];
121
+ for (const link of links) {
122
+ const source = link.source as SimulatedNode;
123
+ const target = link.target as SimulatedNode;
124
+ degree.set(source.id, (degree.get(source.id) ?? 0) + 1);
125
+ degree.set(target.id, (degree.get(target.id) ?? 0) + 1);
126
+ const from = place(source);
127
+ const to = place(target);
128
+ nodes.push({
129
+ kind: "line",
130
+ key: `${id}:link:${source.id}:${target.id}`,
131
+ role: "grid",
132
+ x1: from.x,
133
+ y1: from.y,
134
+ x2: to.x,
135
+ y2: to.y,
136
+ paint: { stroke: theme.grid, strokeWidth: 1.5 },
137
+ });
138
+ }
139
+ const points: ChartPoint[] = [];
140
+ for (const [index, node] of simulated.entries()) {
141
+ const at = place(node);
142
+ const color = node.group
143
+ ? context.colorOf(node.group)
144
+ : context.paletteColor(0);
145
+ nodes.push({
146
+ kind: "circle",
147
+ key: `${id}:${node.id}`,
148
+ series: node.group,
149
+ role: "mark",
150
+ cx: at.x,
151
+ cy: at.y,
152
+ r: radius,
153
+ paint: { fill: color, stroke: theme.background, strokeWidth: 1.5 },
154
+ });
155
+ if (options.labels) {
156
+ nodes.push({
157
+ kind: "text",
158
+ key: `${id}:label:${node.id}`,
159
+ role: "label",
160
+ x: at.x + radius + 3,
161
+ y: at.y,
162
+ text: node.label ?? node.id,
163
+ paint: {
164
+ fill: theme.foreground,
165
+ fontSize: theme.fontSize - 1,
166
+ baseline: "middle",
167
+ },
168
+ });
169
+ }
170
+ if (options.tip !== false) {
171
+ points.push({
172
+ key: `${id}:${node.id}`,
173
+ markId: id,
174
+ index,
175
+ datum: node,
176
+ x: at.x,
177
+ y: at.y,
178
+ xValue: node.label ?? node.id,
179
+ yValue: degree.get(node.id) ?? 0,
180
+ series: node.group,
181
+ seriesLabel: node.group,
182
+ color,
183
+ title: node.label ?? node.id,
184
+ value: context.formatY(degree.get(node.id) ?? 0),
185
+ hit: {
186
+ kind: "rect",
187
+ rect: {
188
+ x: at.x - radius - 2,
189
+ y: at.y - radius - 2,
190
+ width: radius * 2 + 4,
191
+ height: radius * 2 + 4,
192
+ },
193
+ },
194
+ });
195
+ }
196
+ }
197
+ return { nodes, points };
198
+ },
199
+ };
200
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ type ForceLink,
3
+ type ForceNode,
4
+ type ForceOptions,
5
+ forceGraph,
6
+ } from "#/force/force.ts";