basalt-ui 1.30.2 → 1.32.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 (357) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +54 -0
  3. package/MIGRATING.md +256 -12
  4. package/agent/rules/basalt-controls.md +19 -19
  5. package/agent/rules/basalt-mantine.md +5 -5
  6. package/configs/oxlint-plugin.js +371 -4
  7. package/configs/oxlint-plugin.test.ts +327 -2
  8. package/configs/oxlint.json +2 -1
  9. package/dist/agent-chat/composer.d.ts.map +1 -1
  10. package/dist/agent-chat/composer.js +41 -43
  11. package/dist/agent-chat/thread-workspace.d.ts +2 -2
  12. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  13. package/dist/agent-chat/thread-workspace.js +3 -4
  14. package/dist/agent-chat/use-keyboard-inset.d.ts +2 -0
  15. package/dist/agent-chat/use-keyboard-inset.d.ts.map +1 -0
  16. package/dist/agent-chat/use-keyboard-inset.js +34 -0
  17. package/dist/charts/cursor/touch-pin.d.ts +45 -0
  18. package/dist/charts/cursor/touch-pin.d.ts.map +1 -0
  19. package/dist/charts/cursor/touch-pin.js +79 -0
  20. package/dist/charts/hooks/use-discrete-cursor.d.ts +66 -0
  21. package/dist/charts/hooks/use-discrete-cursor.d.ts.map +1 -0
  22. package/dist/charts/hooks/use-discrete-cursor.js +198 -0
  23. package/dist/charts/hooks/use-hysteresis.d.ts +18 -0
  24. package/dist/charts/hooks/use-hysteresis.d.ts.map +1 -0
  25. package/dist/charts/hooks/use-hysteresis.js +12 -0
  26. package/dist/charts/hooks/useBandPlot.d.ts +7 -0
  27. package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
  28. package/dist/charts/hooks/useBandPlot.js +62 -18
  29. package/dist/charts/hooks/useChartCursor.d.ts +40 -11
  30. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  31. package/dist/charts/hooks/useChartCursor.js +102 -11
  32. package/dist/charts/index.d.ts +0 -1
  33. package/dist/charts/index.d.ts.map +1 -1
  34. package/dist/charts/index.js +0 -10
  35. package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
  36. package/dist/charts/kinds/BandStrip.js +47 -82
  37. package/dist/charts/kinds/Donut.d.ts +9 -3
  38. package/dist/charts/kinds/Donut.d.ts.map +1 -1
  39. package/dist/charts/kinds/Donut.js +110 -48
  40. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  41. package/dist/charts/kinds/DualPanel.js +138 -145
  42. package/dist/charts/kinds/Heatmap.d.ts +7 -0
  43. package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
  44. package/dist/charts/kinds/Heatmap.js +37 -16
  45. package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
  46. package/dist/charts/kinds/MirroredBars.js +68 -111
  47. package/dist/charts/kinds/band-chrome.d.ts +31 -0
  48. package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
  49. package/dist/charts/kinds/band-chrome.js +71 -0
  50. package/dist/charts/kinds/donut-layout.d.ts +47 -0
  51. package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
  52. package/dist/charts/kinds/donut-layout.js +52 -0
  53. package/dist/charts/layout/auto-margin.d.ts +7 -2
  54. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  55. package/dist/charts/layout/auto-margin.js +12 -3
  56. package/dist/charts/layout/end-labels.d.ts +64 -0
  57. package/dist/charts/layout/end-labels.d.ts.map +1 -0
  58. package/dist/charts/layout/end-labels.js +76 -0
  59. package/dist/charts/primitives/Axes.d.ts +32 -4
  60. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  61. package/dist/charts/primitives/Axes.js +62 -16
  62. package/dist/charts/primitives/CartesianChart.d.ts +7 -6
  63. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  64. package/dist/charts/primitives/CartesianChart.js +264 -145
  65. package/dist/charts/primitives/ChartCard.d.ts +0 -14
  66. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartCard.js +56 -21
  68. package/dist/charts/primitives/ChartFrame.d.ts +28 -22
  69. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  70. package/dist/charts/primitives/ChartFrame.js +116 -34
  71. package/dist/charts/primitives/ChartLegend.d.ts +19 -15
  72. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  73. package/dist/charts/primitives/ChartLegend.js +86 -73
  74. package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
  75. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  76. package/dist/charts/primitives/ChartTooltip.js +20 -4
  77. package/dist/charts/primitives/Crosshair.js +2 -2
  78. package/dist/charts/primitives/HoverOverlay.d.ts +29 -9
  79. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  80. package/dist/charts/primitives/HoverOverlay.js +54 -27
  81. package/dist/charts/primitives/HoverOverlay.module.css +13 -0
  82. package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  83. package/dist/charts/primitives/chart-card-context.d.ts +33 -0
  84. package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
  85. package/dist/charts/primitives/chart-card-context.js +18 -0
  86. package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
  87. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  88. package/dist/charts/primitives/chart-frame-layout.js +75 -34
  89. package/dist/charts/primitives/chart-layout.d.ts +112 -0
  90. package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
  91. package/dist/charts/primitives/chart-layout.js +175 -0
  92. package/dist/charts/primitives/chart-tier.d.ts +13 -11
  93. package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
  94. package/dist/charts/primitives/chart-tier.js +42 -13
  95. package/dist/charts/primitives/legend-disclosure.d.ts +27 -0
  96. package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
  97. package/dist/charts/primitives/legend-disclosure.js +153 -0
  98. package/dist/charts/series.d.ts +4 -1
  99. package/dist/charts/series.d.ts.map +1 -1
  100. package/dist/charts/utils/ticks.d.ts +27 -8
  101. package/dist/charts/utils/ticks.d.ts.map +1 -1
  102. package/dist/charts/utils/ticks.js +35 -11
  103. package/dist/common/errors.d.ts +11 -1
  104. package/dist/common/errors.d.ts.map +1 -1
  105. package/dist/common/errors.js +16 -2
  106. package/dist/common/hit-floor.d.ts +10 -0
  107. package/dist/common/hit-floor.d.ts.map +1 -0
  108. package/dist/common/hit-floor.js +6 -0
  109. package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
  110. package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
  111. package/dist/common/use-measured-widths.d.ts +37 -0
  112. package/dist/common/use-measured-widths.d.ts.map +1 -0
  113. package/dist/common/use-measured-widths.js +51 -0
  114. package/dist/common/use-media-query.d.ts +6 -0
  115. package/dist/common/use-media-query.d.ts.map +1 -0
  116. package/dist/common/use-media-query.js +40 -0
  117. package/dist/common/validate.d.ts +9 -0
  118. package/dist/common/validate.d.ts.map +1 -1
  119. package/dist/common/validate.js +9 -4
  120. package/dist/content/article-layout.d.ts +4 -2
  121. package/dist/content/article-layout.d.ts.map +1 -1
  122. package/dist/content/article-layout.js +43 -3
  123. package/dist/content/article-layout.module.css +45 -4
  124. package/dist/content/article-layout.module.css.d.ts +3 -0
  125. package/dist/content/code-block.d.ts.map +1 -1
  126. package/dist/content/code-block.js +40 -2
  127. package/dist/content/code-block.module.css +32 -2
  128. package/dist/content/prose.module.css +13 -8
  129. package/dist/controls/actions.d.ts +29 -6
  130. package/dist/controls/actions.d.ts.map +1 -1
  131. package/dist/controls/actions.js +158 -27
  132. package/dist/controls/actions.module.css +14 -1
  133. package/dist/controls/actions.module.css.d.ts +1 -0
  134. package/dist/controls/controls.module.css +13 -31
  135. package/dist/controls/enum-filter.js +2 -2
  136. package/dist/controls/filter-context.d.ts +10 -0
  137. package/dist/controls/filter-context.d.ts.map +1 -1
  138. package/dist/controls/filter-context.js +2 -0
  139. package/dist/controls/filter-pill.d.ts.map +1 -1
  140. package/dist/controls/filter-pill.js +1 -0
  141. package/dist/controls/index.d.ts +1 -1
  142. package/dist/controls/index.d.ts.map +1 -1
  143. package/dist/controls/index.js +0 -2
  144. package/dist/controls/multi-select-filter.js +2 -2
  145. package/dist/controls/sync-button.module.css +2 -1
  146. package/dist/dashboard/section.d.ts.map +1 -1
  147. package/dist/dashboard/section.js +1 -0
  148. package/dist/dashboard/section.module.css +1 -26
  149. package/dist/dashboard/stat-card.d.ts +1 -1
  150. package/dist/dashboard/stat-card.d.ts.map +1 -1
  151. package/dist/dashboard/stat-card.js +4 -1
  152. package/dist/dashboard/stat-card.module.css +6 -9
  153. package/dist/dashboard/stat-group.module.css +33 -66
  154. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  155. package/dist/dashboard/widget-grid.module.css +11 -38
  156. package/dist/dashboard/widget-header.d.ts +14 -4
  157. package/dist/dashboard/widget-header.d.ts.map +1 -1
  158. package/dist/dashboard/widget-header.js +23 -6
  159. package/dist/dashboard/widget-header.module.css +145 -133
  160. package/dist/dashboard/widget-header.module.css.d.ts +2 -0
  161. package/dist/data/data-table-state.d.ts +83 -0
  162. package/dist/data/data-table-state.d.ts.map +1 -0
  163. package/dist/data/data-table-state.js +139 -0
  164. package/dist/data/data-table.d.ts +56 -2
  165. package/dist/data/data-table.d.ts.map +1 -1
  166. package/dist/data/data-table.js +436 -336
  167. package/dist/data/data-table.module.css +94 -0
  168. package/dist/data/data-table.module.css.d.ts +11 -0
  169. package/dist/data/virtual-list.d.ts.map +1 -1
  170. package/dist/data/virtual-list.js +10 -0
  171. package/dist/forms/form-layout.d.ts.map +1 -1
  172. package/dist/forms/form-layout.js +3 -3
  173. package/dist/forms/form-layout.module.css +20 -5
  174. package/dist/forms/form-layout.module.css.d.ts +1 -0
  175. package/dist/guard/index.d.ts +8 -2
  176. package/dist/guard/index.d.ts.map +1 -1
  177. package/dist/guard/index.js +68 -2
  178. package/dist/guard/types.d.ts +3 -3
  179. package/dist/guard/types.d.ts.map +1 -1
  180. package/dist/index.d.ts +1 -1
  181. package/dist/index.d.ts.map +1 -1
  182. package/dist/index.js +3 -1
  183. package/dist/notifications/center.d.ts.map +1 -1
  184. package/dist/provider/index.d.ts +20 -1
  185. package/dist/provider/index.d.ts.map +1 -1
  186. package/dist/provider/index.js +30 -8
  187. package/dist/shell/app-brand.d.ts.map +1 -1
  188. package/dist/shell/app-brand.js +1 -0
  189. package/dist/shell/app-brand.module.css +2 -2
  190. package/dist/shell/app-header.module.css +18 -8
  191. package/dist/shell/app-main.module.css +54 -3
  192. package/dist/shell/app-main.module.css.d.ts +1 -0
  193. package/dist/shell/app-mobile-nav.module.css +16 -4
  194. package/dist/shell/app-sidebar.module.css +4 -6
  195. package/dist/shell/aside-docking.d.ts +31 -0
  196. package/dist/shell/aside-docking.d.ts.map +1 -0
  197. package/dist/shell/aside-docking.js +19 -0
  198. package/dist/shell/index.d.ts +1 -0
  199. package/dist/shell/index.d.ts.map +1 -1
  200. package/dist/shell/index.js +56 -16
  201. package/dist/shell/mobile-nav-model.d.ts +1 -1
  202. package/dist/shell/page-aside.d.ts +22 -3
  203. package/dist/shell/page-aside.d.ts.map +1 -1
  204. package/dist/shell/page-aside.js +27 -13
  205. package/dist/shell/page-bar.d.ts.map +1 -1
  206. package/dist/shell/page-bar.js +10 -5
  207. package/dist/shell/page-bar.module.css +10 -34
  208. package/dist/shell/use-breakpoint.d.ts +3 -4
  209. package/dist/shell/use-breakpoint.d.ts.map +1 -1
  210. package/dist/shell/use-breakpoint.js +3 -25
  211. package/dist/shell/use-size-class.d.ts +6 -0
  212. package/dist/shell/use-size-class.d.ts.map +1 -0
  213. package/dist/shell/use-size-class.js +19 -0
  214. package/dist/state/persisted-or-local.d.ts +4 -1
  215. package/dist/state/persisted-or-local.d.ts.map +1 -1
  216. package/dist/state/persisted-or-local.js +4 -3
  217. package/dist/state/persisted.d.ts +10 -0
  218. package/dist/state/persisted.d.ts.map +1 -1
  219. package/dist/state/persisted.js +44 -14
  220. package/dist/styles.css +41 -0
  221. package/dist/surfaces.d.ts +4 -4
  222. package/dist/surfaces.d.ts.map +1 -1
  223. package/dist/surfaces.js +11 -3
  224. package/dist/theme/controls.module.css +7 -0
  225. package/dist/theme/ctl-theme.d.ts +1 -0
  226. package/dist/theme/ctl-theme.d.ts.map +1 -1
  227. package/dist/theme/ctl-theme.js +12 -4
  228. package/dist/theme/floating.module.css +37 -0
  229. package/dist/theme/floating.module.css.d.ts +3 -0
  230. package/dist/theme/index.d.ts +2 -2
  231. package/dist/theme/index.d.ts.map +1 -1
  232. package/dist/theme/index.js +34 -5
  233. package/dist/theme/nav-link.module.css +7 -0
  234. package/dist/theme-lab/derive-controls.d.ts.map +1 -1
  235. package/dist/tokens/index.d.ts +7 -16
  236. package/dist/tokens/index.d.ts.map +1 -1
  237. package/dist/tokens/index.js +23 -28
  238. package/dist/tokens/palette.d.ts +5 -27
  239. package/dist/tokens/palette.d.ts.map +1 -1
  240. package/dist/tokens/palette.js +8 -30
  241. package/dist/tokens/size-classes.d.ts +40 -0
  242. package/dist/tokens/size-classes.d.ts.map +1 -0
  243. package/dist/tokens/size-classes.js +29 -0
  244. package/dist/tokens.css +8 -4
  245. package/llms.txt +3 -3
  246. package/package.json +18 -17
  247. package/src/agent-chat/composer.tsx +74 -47
  248. package/src/agent-chat/thread-workspace.tsx +8 -11
  249. package/src/agent-chat/use-keyboard-inset.ts +68 -0
  250. package/src/charts/cursor/touch-pin.ts +138 -0
  251. package/src/charts/hooks/use-discrete-cursor.ts +335 -0
  252. package/src/charts/hooks/use-hysteresis.ts +26 -0
  253. package/src/charts/hooks/useBandPlot.ts +90 -27
  254. package/src/charts/hooks/useChartCursor.ts +211 -26
  255. package/src/charts/index.ts +0 -8
  256. package/src/charts/kinds/BandStrip.tsx +25 -61
  257. package/src/charts/kinds/Donut.tsx +153 -62
  258. package/src/charts/kinds/DualPanel.tsx +21 -26
  259. package/src/charts/kinds/Heatmap.tsx +61 -22
  260. package/src/charts/kinds/MirroredBars.tsx +23 -65
  261. package/src/charts/kinds/band-chrome.tsx +91 -0
  262. package/src/charts/kinds/donut-layout.ts +82 -0
  263. package/src/charts/layout/auto-margin.ts +24 -5
  264. package/src/charts/layout/end-labels.ts +135 -0
  265. package/src/charts/primitives/Axes.tsx +112 -21
  266. package/src/charts/primitives/CartesianChart.tsx +282 -111
  267. package/src/charts/primitives/ChartCard.tsx +119 -48
  268. package/src/charts/primitives/ChartFrame.tsx +204 -60
  269. package/src/charts/primitives/ChartLegend.tsx +115 -78
  270. package/src/charts/primitives/ChartTooltip.tsx +24 -3
  271. package/src/charts/primitives/Crosshair.tsx +2 -2
  272. package/src/charts/primitives/HoverOverlay.module.css +13 -0
  273. package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  274. package/src/charts/primitives/HoverOverlay.tsx +62 -20
  275. package/src/charts/primitives/chart-card-context.ts +46 -0
  276. package/src/charts/primitives/chart-frame-layout.ts +207 -182
  277. package/src/charts/primitives/chart-layout.ts +318 -0
  278. package/src/charts/primitives/chart-tier.tsx +66 -21
  279. package/src/charts/primitives/legend-disclosure.tsx +177 -0
  280. package/src/charts/series.ts +4 -1
  281. package/src/charts/utils/ticks.ts +95 -19
  282. package/src/common/errors.ts +25 -1
  283. package/src/common/hit-floor.ts +9 -0
  284. package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
  285. package/src/common/use-measured-widths.ts +88 -0
  286. package/src/common/use-media-query.ts +56 -0
  287. package/src/common/validate.ts +17 -4
  288. package/src/content/article-layout.module.css +45 -4
  289. package/src/content/article-layout.module.css.d.ts +3 -0
  290. package/src/content/article-layout.tsx +60 -5
  291. package/src/content/code-block.module.css +32 -2
  292. package/src/content/code-block.tsx +47 -3
  293. package/src/content/prose.module.css +13 -8
  294. package/src/controls/actions.module.css +14 -1
  295. package/src/controls/actions.module.css.d.ts +1 -0
  296. package/src/controls/actions.tsx +223 -75
  297. package/src/controls/controls.module.css +13 -31
  298. package/src/controls/enum-filter.tsx +2 -2
  299. package/src/controls/filter-context.tsx +11 -0
  300. package/src/controls/filter-pill.tsx +1 -0
  301. package/src/controls/index.ts +0 -1
  302. package/src/controls/multi-select-filter.tsx +2 -2
  303. package/src/controls/sync-button.module.css +2 -1
  304. package/src/dashboard/section.module.css +1 -26
  305. package/src/dashboard/section.tsx +1 -0
  306. package/src/dashboard/stat-card.module.css +6 -9
  307. package/src/dashboard/stat-card.tsx +6 -3
  308. package/src/dashboard/stat-group.module.css +33 -66
  309. package/src/dashboard/widget-grid.module.css +11 -38
  310. package/src/dashboard/widget-grid.tsx +7 -6
  311. package/src/dashboard/widget-header.module.css +145 -133
  312. package/src/dashboard/widget-header.module.css.d.ts +2 -0
  313. package/src/dashboard/widget-header.tsx +45 -11
  314. package/src/data/data-table-state.ts +218 -0
  315. package/src/data/data-table.module.css +94 -0
  316. package/src/data/data-table.module.css.d.ts +11 -0
  317. package/src/data/data-table.tsx +547 -299
  318. package/src/data/virtual-list.tsx +31 -0
  319. package/src/forms/form-layout.module.css +20 -5
  320. package/src/forms/form-layout.module.css.d.ts +1 -0
  321. package/src/forms/form-layout.tsx +12 -10
  322. package/src/guard/index.ts +188 -3
  323. package/src/guard/types.ts +3 -2
  324. package/src/index.ts +2 -0
  325. package/src/notifications/center.tsx +3 -0
  326. package/src/provider/index.tsx +60 -7
  327. package/src/shell/app-brand.module.css +2 -2
  328. package/src/shell/app-brand.tsx +1 -0
  329. package/src/shell/app-header.module.css +18 -8
  330. package/src/shell/app-main.module.css +54 -3
  331. package/src/shell/app-main.module.css.d.ts +1 -0
  332. package/src/shell/app-mobile-nav.module.css +16 -4
  333. package/src/shell/app-sidebar.module.css +4 -6
  334. package/src/shell/aside-docking.ts +54 -0
  335. package/src/shell/index.tsx +178 -129
  336. package/src/shell/mobile-nav-model.ts +1 -1
  337. package/src/shell/page-aside.tsx +70 -21
  338. package/src/shell/page-bar.module.css +10 -34
  339. package/src/shell/page-bar.tsx +10 -5
  340. package/src/shell/use-breakpoint.ts +9 -45
  341. package/src/shell/use-size-class.ts +31 -0
  342. package/src/state/persisted-or-local.ts +10 -4
  343. package/src/state/persisted.ts +76 -17
  344. package/src/styles.css +41 -0
  345. package/src/surfaces.ts +9 -1
  346. package/src/theme/controls.module.css +7 -0
  347. package/src/theme/ctl-theme.tsx +15 -3
  348. package/src/theme/floating.module.css +37 -0
  349. package/src/theme/floating.module.css.d.ts +3 -0
  350. package/src/theme/index.ts +46 -8
  351. package/src/theme/nav-link.module.css +7 -0
  352. package/src/theme-lab/derive-controls.tsx +2 -0
  353. package/src/tokens/index.ts +20 -30
  354. package/src/tokens/palette.ts +6 -28
  355. package/src/tokens/size-classes.ts +46 -0
  356. package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
  357. /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
@@ -4,25 +4,37 @@ import { scaleLinear, scaleLog, scalePoint } from '@visx/scale'
4
4
  import { useMemo, useRef, useState } from 'react'
5
5
  import type { ReactNode } from 'react'
6
6
  import type { BasaltProps } from '../../common/props'
7
- import { VX } from '../../tokens'
7
+ import { VX, alpha } from '../../tokens'
8
8
  import type { ChartMargin } from '../../tokens'
9
+ import { classifyDomain } from '../cursor/resolve'
9
10
  import type { CursorResolution } from '../cursor/resolve'
10
11
  import { useChartCursor } from '../hooks/useChartCursor'
11
12
  import { autoMargin, probeAxisLabels } from '../layout/auto-margin'
13
+ import type { AutoMarginInput } from '../layout/auto-margin'
14
+ import { END_LABEL_GAP, planEndLabels } from '../layout/end-labels'
12
15
  import { logTickValues, niceLogDomain } from '../layout/log-ticks'
13
16
  import { deriveLegend, deriveTooltipRows } from '../series'
14
17
  import type { ChartLegendConfig, ChartSeries } from '../series'
15
18
  import { padAutoLower, padAutoUpper } from '../utils/domain'
16
19
  import { fmtAxisDate } from '../utils/format'
17
- import { autoXLabelRotate, smartTicks, smartTicksEvery, xLabelPxFor } from '../utils/ticks'
18
- import { AxisBottomDate, AxisLeftNumeric, AxisRightNumeric } from './Axes'
20
+ import { rotatedXLabelPx, smartTicks, smartTicksEvery, xLabelPxFor } from '../utils/ticks'
21
+ import { AxisBottomDate, AxisLeftNumeric, AxisRightNumeric, TICK_FONT_FAMILY } from './Axes'
19
22
  import { ChartFrame, resolveLegend } from './ChartFrame'
20
23
  import type { ResponsiveChartHeight } from './ChartFrame'
21
- import { useChartTierMetrics } from './chart-tier'
24
+ import {
25
+ compactNumber,
26
+ isTightClass,
27
+ planXLabels,
28
+ resolveMarginFloor,
29
+ resolveYPlacement,
30
+ shouldCompactYLabels,
31
+ yTickCount,
32
+ } from './chart-layout'
33
+ import { useChartContainerClass, useChartMetrics } from './chart-tier'
22
34
  import type { ChartState } from './ChartPending'
23
- import { ChartTooltipFloat, TooltipBody, TooltipHeader, TooltipRow } from './ChartTooltip'
35
+ import { ChartTooltipFloat, TooltipBody, TooltipHeader, TooltipRows } from './ChartTooltip'
24
36
  import { Crosshair, SeriesDot } from './Crosshair'
25
- import { HoverOverlay } from './HoverOverlay'
37
+ import { cursorSliderProps, HoverOverlay } from './HoverOverlay'
26
38
  import { XZoneRects } from './XZoneRects'
27
39
  import type { XZoneSpec } from './XZoneRects'
28
40
  import { ZoneRects } from './ZoneRects'
@@ -35,9 +47,11 @@ import type { ZoneSpec } from './ZoneRects'
35
47
  type ContinuousScale = ReturnType<typeof scaleLinear<number>> | ReturnType<typeof scaleLog<number>>
36
48
  type PointScale = ReturnType<typeof scalePoint<string>>
37
49
 
38
- const DEFAULT_TICKS = 5
39
50
  const DEFAULT_AUTO_PAD = 1.1
40
51
 
52
+ /** Faint tint a dual-axis chart lays over its tick labels: the series colour, kept quiet. */
53
+ const AXIS_TINT_ALPHA = 0.8
54
+
41
55
  /** One y-axis, fully described. Collapses the removed `yDomain` / `yAutoMaxFloor` / `yAutoMinCeil` /
42
56
  * `yAutoPad` / `numTicksY` / `formatYTick` prop soup into a single object per axis. */
43
57
  export type AxisConfig<T> = {
@@ -195,14 +209,15 @@ export type CartesianChartProps<T> = BasaltProps & {
195
209
  * bottom gutter deepens by the rotated label's projected height, measured like every other side
196
210
  * (`docs/CHARTS-SPEC.md` §1), so nothing clips.
197
211
  *
198
- * This is the phone answer to a label too wide to repeat horizontally: rotating keeps every tick
199
- * the axis would otherwise have to thin away — which is why, LEFT UNSET, it is now the phone
200
- * tier's DEFAULT: below `VX.phoneChartWidth`, labels too wide to fit three ticks side by side
201
- * auto-rotate to 45 (`autoXLabelRotate`). Nothing changes at desktop width, and nothing changes
202
- * for a chart that already passes a value.
212
+ * This is the phone answer to a **categorical** axis whose labels are too wide to repeat
213
+ * horizontally: rotating keeps every tick the axis would otherwise have to thin away — which is
214
+ * why, LEFT UNSET, it is now the phone tier's DEFAULT for a band domain, below
215
+ * `CONTAINER_CLASSES.regular` (`planXLabels`). A time or number domain never auto-rotates (or
216
+ * wraps) — it thins instead, however narrow the plot. Nothing changes at desktop width, and
217
+ * nothing changes for a chart that already passes a value.
203
218
  *
204
219
  * **`0` is the opt-out** — "never rotate, thin the axis instead" — and is the only way to get
205
- * the pre-tier behaviour back on a phone-width chart with very wide labels.
220
+ * the pre-tier behaviour back on a phone-width categorical chart with very wide labels.
206
221
  */
207
222
  xLabelRotate?: 0 | 45 | 90
208
223
  /** Value-range bands on the left scale, drawn behind the marks. */
@@ -321,6 +336,41 @@ export function resolveAxisDomain<T>(
321
336
  return [lower, upper]
322
337
  }
323
338
 
339
+ /**
340
+ * Probe one y axis' labels. With no consumer `format`, labels over 4 chars switch to the compact
341
+ * number format (`12500` -> `12.5k`, ladder step 1); an explicit `format` is never touched.
342
+ */
343
+ function probeYAxis(opts: {
344
+ domain: [number, number]
345
+ ticks: number
346
+ nice: boolean
347
+ scale: 'linear' | 'log' | undefined
348
+ format: ((v: number) => string) | undefined
349
+ }): { labels: string[]; format: (v: number) => string } {
350
+ const { domain, ticks, nice, scale, format } = opts
351
+ const base = probeAxisLabels({
352
+ domain,
353
+ ticks,
354
+ nice,
355
+ ...(scale !== undefined && { scale }),
356
+ ...(format !== undefined && { format }),
357
+ })
358
+ if (format !== undefined || !shouldCompactYLabels(base.labels)) {
359
+ return base
360
+ }
361
+ const compact = (v: number): string => compactNumber(v) ?? base.format(v)
362
+ return {
363
+ labels: probeAxisLabels({
364
+ domain,
365
+ ticks,
366
+ nice,
367
+ format: compact,
368
+ ...(scale !== undefined && { scale }),
369
+ }).labels,
370
+ format: compact,
371
+ }
372
+ }
373
+
324
374
  /**
325
375
  * One y-axis' real scale, built from its resolved domain — shared by the left and right axis so
326
376
  * the log branch (`scaleLog` + `nice` + the 1-2-5 tick override) is written and commented exactly
@@ -354,6 +404,54 @@ function buildYScale<T>(
354
404
  })
355
405
  }
356
406
 
407
+ /**
408
+ * The x-axis margin ladder, as one pure decision. The flat (unrotated) margin is what every case
409
+ * resolves to except a rotated or wrapped axis, and it is what the rotation decision itself is
410
+ * measured against — deciding needs an `xMax`, and `xMax` needs a margin, so the flat pass breaks
411
+ * the loop. An explicit `xLabelRotate` always wins — including `0`, the documented "never rotate"
412
+ * opt-out; unset, only a categorical (band) axis wraps, and rotates only when even wrapped it
413
+ * doesn't fit (`planXLabels`) — a time or number domain only thins.
414
+ *
415
+ * Per rotation: 45° reuses its pre-measured margin and measures its tick room through
416
+ * {@link rotatedXLabelPx} (a rotated label projects wider than its glyph box); 90° measures fresh
417
+ * (one line box wide, so the flat label room governs its thinning); a wrapped flat axis re-measures
418
+ * for its extra line(s) and thins by the wrapped width.
419
+ */
420
+ function resolveXAxisLayout(input: {
421
+ marginInput: AutoMarginInput
422
+ flatMargin: ChartMargin
423
+ rotated45Margin: ChartMargin
424
+ flatLabelPx: number
425
+ xLabels: string[]
426
+ fontPx: number
427
+ plotWidth: number
428
+ categorical: boolean
429
+ xLabelRotate: 0 | 45 | 90 | undefined
430
+ }): {
431
+ baseMargin: ChartMargin
432
+ rotate: 0 | 45 | 90
433
+ wrapWidth: number | undefined
434
+ tickLabelPx: number
435
+ } {
436
+ const { marginInput, flatMargin, rotated45Margin, flatLabelPx, xLabels, fontPx, categorical } =
437
+ input
438
+ const flatXMax = Math.max(input.plotWidth - flatMargin.left - flatMargin.right, 0)
439
+ const xPlan = planXLabels({ labels: xLabels, plotWidth: flatXMax, fontPx, categorical })
440
+ const rotate = input.xLabelRotate ?? (categorical ? xPlan.rotate : 0)
441
+ if (rotate === 45) {
442
+ const tickLabelPx = rotatedXLabelPx(xLabels, fontPx)
443
+ return { baseMargin: rotated45Margin, rotate, wrapWidth: undefined, tickLabelPx }
444
+ }
445
+ if (rotate === 90) {
446
+ const baseMargin = autoMargin({ ...marginInput, rotate })
447
+ return { baseMargin, rotate, wrapWidth: undefined, tickLabelPx: flatLabelPx }
448
+ }
449
+ if (!xPlan.wrap)
450
+ return { baseMargin: flatMargin, rotate, wrapWidth: undefined, tickLabelPx: flatLabelPx }
451
+ const baseMargin = autoMargin({ ...marginInput, bottomLines: xPlan.lines })
452
+ return { baseMargin, rotate, wrapWidth: xPlan.wrapPx, tickLabelPx: xPlan.labelPx }
453
+ }
454
+
357
455
  /**
358
456
  * The cartesian chart primitive — the rung that was missing between `ChartFrame` and raw visx
359
457
  * (`docs/CHARTS-SPEC.md` §2).
@@ -458,7 +556,7 @@ type CartesianPlotProps<T> = Omit<
458
556
  | 'className'
459
557
  | 'style'
460
558
  > & {
461
- plot: { width: number; height: number; hidden: ReadonlySet<string> }
559
+ plot: { width: number; height: number; hidden: ReadonlySet<string>; legendInHeader: boolean }
462
560
  highlighted: string | null
463
561
  marginOverride?: Partial<ChartMargin>
464
562
  }
@@ -492,7 +590,9 @@ function CartesianPlot<T>({
492
590
  // The tier `ChartFrame` resolved from its own MEASURED width. Everything sized by it here is
493
591
  // the half a hook cannot reach: the tick font the margins are MEASURED at, and the margin
494
592
  // floors themselves (`docs/CHARTS-SPEC.md` §8).
495
- const tier = useChartTierMetrics()
593
+ const tier = useChartMetrics()
594
+ const containerClass = useChartContainerClass()
595
+ const tight = isTightClass(containerClass)
496
596
 
497
597
  const visible = useMemo(() => series.filter((s) => !hidden.has(s.key)), [series, hidden])
498
598
  const leftSeries = useMemo(() => visible.filter((s) => s.axis !== 'right'), [visible])
@@ -526,17 +626,21 @@ function CartesianPlot<T>({
526
626
  [y2, data, rightSeries, rightBounds],
527
627
  )
528
628
 
529
- const leftTicks = y?.ticks ?? DEFAULT_TICKS
530
- const rightTicks = y2?.ticks ?? DEFAULT_TICKS
629
+ // Tick count is a law of the plot height (one per ~44px, 2 under `VX.minPlotHeight`), estimated
630
+ // from the token floors because the real margins depend on the labels these ticks produce. A
631
+ // consumer's `ticks` always wins.
632
+ const estimatedPlotHeight = plot.height - tier.margin.top - tier.margin.bottom
633
+ const leftTicks = y?.ticks ?? yTickCount(estimatedPlotHeight)
634
+ const rightTicks = y2?.ticks ?? yTickCount(estimatedPlotHeight)
531
635
 
532
636
  const { labels: leftLabels, format: leftFormat } = useMemo(
533
637
  () =>
534
- probeAxisLabels({
638
+ probeYAxis({
535
639
  domain: leftDomain,
536
640
  ticks: leftTicks,
537
641
  nice: y?.nice ?? false,
538
- ...(y?.scale !== undefined && { scale: y.scale }),
539
- ...(y?.format !== undefined && { format: y.format }),
642
+ scale: y?.scale,
643
+ format: y?.format,
540
644
  }),
541
645
  [leftDomain, leftTicks, y?.format, y?.nice, y?.scale],
542
646
  )
@@ -545,110 +649,153 @@ function CartesianPlot<T>({
545
649
  () =>
546
650
  rightDomain === null
547
651
  ? { labels: [], format: (v: number) => String(v) }
548
- : probeAxisLabels({
652
+ : probeYAxis({
549
653
  domain: rightDomain,
550
654
  ticks: rightTicks,
551
655
  nice: y2?.nice ?? false,
552
- ...(y2?.scale !== undefined && { scale: y2.scale }),
553
- ...(y2?.format !== undefined && { format: y2.format }),
656
+ scale: y2?.scale,
657
+ format: y2?.format,
554
658
  }),
555
659
  [rightDomain, rightTicks, y2?.format, y2?.nice, y2?.scale],
556
660
  )
557
661
 
558
662
  const xLabels = useMemo(() => keys.map(formatX), [keys, formatX])
559
663
 
560
- /** The horizontal room one x tick label needs: the widest string that could be painted, plus
561
- * breathing space to its neighbour. Feeds `smartTicks`, which otherwise thinned the axis by a
562
- * constant that knew nothing about the label. Measured at the tick font the axis will actually
563
- * paint, which the tier moves (`docs/CHARTS-SPEC.md` §8). */
564
- const xLabelPx = useMemo(() => xLabelPxFor(xLabels, tier.axisFont), [xLabels, tier.axisFont])
565
-
566
- /** Everything `autoMargin` measures except the rotation — shared by all three passes below so a
567
- * measured label can never differ between the one that DECIDES and the one that PAINTS. */
568
- const marginInput = useMemo(
569
- () => ({
570
- left: leftLabels,
664
+ // Ladder step 5: at compact/micro the y labels sit inside the plot (micro: no y axis), so the
665
+ // left gutter drops to its floor. An explicit `margin.left` means the consumer laid out an
666
+ // outside gutter — it keeps the outside axis. `CartesianChart` always has a left axis.
667
+ const yPlacement = resolveYPlacement(containerClass, { marginOverrideLeft: marginOverride?.left })
668
+ // A whitespace test misclassified any spaced date format ("Mar 01") as categorical, which then
669
+ // wrapped or rotated a time axis that should only ever thin (§ ladder step 4). Domain kind is
670
+ // the actual law: only a band (category) domain wraps or rotates; time and number domains thin.
671
+ const categorical = tight && classifyDomain(keys) === 'band'
672
+
673
+ // The room one flat x tick label needs at the tick font the axis will actually paint
674
+ // (`docs/CHARTS-SPEC.md` §8) — keyed on the labels and font alone, so a y-label or margin change
675
+ // never re-measures every x label.
676
+ const flatLabelPx = useMemo(() => xLabelPxFor(xLabels, tier.axisFont), [xLabels, tier.axisFont])
677
+
678
+ // Everything `autoMargin` measures except the rotation, measured once for every pass so a label
679
+ // can never differ between the one that DECIDES and the one that PAINTS — plus the 45° margin (a
680
+ // rotated label reaches into the left gutter).
681
+ const measured = useMemo(() => {
682
+ const marginInput: AutoMarginInput = {
683
+ left: yPlacement === 'outside' ? leftLabels : [],
571
684
  right: rightLabels,
572
685
  bottom: xLabels,
573
686
  fontPx: tier.axisFont,
574
- floor: tier.margin,
687
+ // An inside-placed y label paints IN the plot, so the left gutter it used to reserve
688
+ // (the phone tier's own margin floor) is dead space — only the inside-label floor applies.
689
+ floor: resolveMarginFloor(yPlacement, tier.margin),
690
+ anchorTerminals: tight,
575
691
  ...(marginOverride !== undefined && { override: marginOverride }),
576
- }),
577
- [leftLabels, rightLabels, xLabels, tier.axisFont, tier.margin, marginOverride],
692
+ }
693
+ return {
694
+ marginInput,
695
+ flatMargin: autoMargin(marginInput),
696
+ rotated45Margin: autoMargin({ ...marginInput, rotate: 45 }),
697
+ }
698
+ }, [
699
+ leftLabels,
700
+ rightLabels,
701
+ xLabels,
702
+ tier.axisFont,
703
+ tier.margin,
704
+ marginOverride,
705
+ tight,
706
+ yPlacement,
707
+ ])
708
+ const { baseMargin, rotate, wrapWidth, tickLabelPx } = useMemo(
709
+ () =>
710
+ resolveXAxisLayout({
711
+ ...measured,
712
+ flatLabelPx,
713
+ xLabels,
714
+ fontPx: tier.axisFont,
715
+ plotWidth: plot.width,
716
+ categorical,
717
+ xLabelRotate,
718
+ }),
719
+ [measured, flatLabelPx, xLabels, tier.axisFont, plot.width, categorical, xLabelRotate],
578
720
  )
579
721
 
580
- /** The margin an UNROTATED axis resolves to. It is the final margin in every case except an
581
- * auto-rotated phone axis, and it is what the rotation decision itself is measured against —
582
- * deciding needs an `xMax`, and `xMax` needs a margin, so the unrotated pass breaks the loop. */
583
- const flatMargin = useMemo(() => autoMargin(marginInput), [marginInput])
584
-
585
- /** And the margin a 45° axis would resolve to — the OTHER half of the decision. A rotated label
586
- * reaches into the left gutter, so rotating spends plot width; `autoXLabelRotate` refuses the
587
- * trade when what is left cannot hold three labels at their projected pitch, which is how block
588
- * (f1) at 320 used to rotate itself into a clipped axis (§8). */
589
- const rotated45Margin = useMemo(() => autoMargin({ ...marginInput, rotate: 45 }), [marginInput])
590
-
591
- // An explicit `xLabelRotate` always wins — including `0`, which is the documented "never rotate"
592
- // opt-out. Only an unset one falls through to the phone tier's own default.
593
- const rotate =
594
- xLabelRotate ??
595
- autoXLabelRotate({
596
- tier: tier.tier,
597
- xMax: Math.max(plot.width - flatMargin.left - flatMargin.right, 0),
598
- labelPx: xLabelPx,
599
- rotatedXMax: Math.max(plot.width - rotated45Margin.left - rotated45Margin.right, 0),
600
- })
601
-
722
+ // End-of-line labels are planned FIRST, against the base margin: the right gutter is reserved
723
+ // only when the labels will actually draw. `yScale` depends on the plot height alone, so it is
724
+ // safe to build before the right margin is settled.
725
+ const yMax = Math.max(plot.height - baseMargin.top - baseMargin.bottom, 0)
726
+ const yScale = useMemo(
727
+ () => buildYScale(y, leftDomain, yMax, leftTicks),
728
+ [y, leftDomain, yMax, leftTicks],
729
+ )
730
+ const endPlan = useMemo(
731
+ () =>
732
+ planEndLabels({
733
+ containerClass,
734
+ hasY2: y2 !== undefined,
735
+ hasMarginRight: marginOverride?.right !== undefined,
736
+ hasHeaderLegend: plot.legendInHeader,
737
+ visible,
738
+ data,
739
+ yScale,
740
+ yMax,
741
+ fontPx: tier.axisFont,
742
+ plotWidth: plot.width - baseMargin.left - baseMargin.right,
743
+ }),
744
+ [
745
+ containerClass,
746
+ y2,
747
+ marginOverride?.right,
748
+ plot.legendInHeader,
749
+ visible,
750
+ data,
751
+ yScale,
752
+ yMax,
753
+ tier.axisFont,
754
+ plot.width,
755
+ baseMargin.left,
756
+ baseMargin.right,
757
+ ],
758
+ )
759
+ const endLabels = endPlan?.labels ?? []
602
760
  const margin = useMemo(
603
761
  () =>
604
- rotate === 0
605
- ? flatMargin
606
- : rotate === 45
607
- ? rotated45Margin
608
- : autoMargin({ ...marginInput, rotate }),
609
- [flatMargin, rotated45Margin, marginInput, rotate],
762
+ endPlan === null ? baseMargin : { ...baseMargin, right: baseMargin.right + endPlan.gutter },
763
+ [baseMargin, endPlan],
610
764
  )
611
765
 
612
766
  // ── Pass 2: the real scales, now that the plot rect is known ────────────────────────────────
613
767
  const xMax = Math.max(plot.width - margin.left - margin.right, 0)
614
- const yMax = Math.max(plot.height - margin.top - margin.bottom, 0)
615
768
 
616
769
  const xScale = useMemo(
617
770
  () => scalePoint<string>({ domain: keys, range: [0, xMax], padding: 0.5 }),
618
771
  [keys, xMax],
619
772
  )
620
- const yScale = useMemo(
621
- () => buildYScale(y, leftDomain, yMax, leftTicks),
622
- [y, leftDomain, yMax, leftTicks],
623
- )
624
773
  const y2Scale = useMemo(
625
774
  () => (rightDomain === null ? null : buildYScale(y2, rightDomain, yMax, rightTicks)),
626
775
  [y2, rightDomain, yMax, rightTicks],
627
776
  )
628
777
 
629
778
  // Resolution order: explicit VALUES win, then an explicit COUNT, then as many as fit.
630
- const tickValues = useMemo(
631
- () =>
632
- xTickValues !== undefined
633
- ? [...xTickValues(keys, xMax)]
634
- : xTicks === undefined
635
- ? // A rotated label stacks diagonally instead of beside its neighbour, so its WIDTH no
636
- // longer governs tick spacing — the constant floor takes over, exactly as before.
637
- smartTicks(keys, xMax, rotate === 0 ? xLabelPx : undefined)
638
- : smartTicksEvery(keys, xTicks),
639
- [keys, xMax, xTicks, xTickValues, xLabelPx, rotate],
640
- )
779
+ const tickValues = useMemo(() => {
780
+ if (xTickValues !== undefined) return [...xTickValues(keys, xMax)]
781
+ if (xTicks !== undefined) return smartTicksEvery(keys, xTicks)
782
+ // Micro: the two terminals only.
783
+ if (containerClass === 'micro')
784
+ return keys.length > 1 ? [keys[0]!, keys[keys.length - 1]!] : keys
785
+ return smartTicks(keys, xMax, rotate === 0 || rotate === 45 ? tickLabelPx : undefined, tight)
786
+ }, [keys, xMax, xTicks, xTickValues, tickLabelPx, rotate, containerClass, tight])
641
787
 
788
+ const svgRef = useRef<SVGSVGElement>(null)
642
789
  const cursor = useChartCursor<T>({
643
790
  data,
644
791
  chartId,
645
792
  getKey: getX,
646
793
  xScale,
647
794
  marginLeft: margin.left,
795
+ boundaryRef: svgRef,
648
796
  ...(cursorResolution !== undefined && { resolution: cursorResolution }),
649
797
  })
650
798
 
651
- const svgRef = useRef<SVGSVGElement>(null)
652
799
  const tooltipEnabled = tooltip !== false
653
800
  const tooltipCfg = tooltip === false ? undefined : tooltip
654
801
  const point = cursor.point
@@ -668,6 +815,12 @@ function CartesianPlot<T>({
668
815
  [visible, leftFormat, rightFormat],
669
816
  )
670
817
 
818
+ // Dual axis with exactly one visible series per side: the tick labels carry that series' colour,
819
+ // so the axis says which line it reads. Any other split leaves both axes neutral.
820
+ const tintAxes = y2 !== undefined && leftSeries.length === 1 && rightSeries.length === 1
821
+ const leftTint = tintAxes ? alpha(leftSeries[0]!.color, AXIS_TINT_ALPHA) : undefined
822
+ const rightTint = tintAxes ? alpha(rightSeries[0]!.color, AXIS_TINT_ALPHA) : undefined
823
+
671
824
  const rows =
672
825
  tooltipEnabled && point !== null ? deriveTooltipRows(tooltipSeries, point, leftFormat) : []
673
826
 
@@ -679,8 +832,13 @@ function CartesianPlot<T>({
679
832
 
680
833
  // Anchored mode resolves the crosshair's plot-local x into viewport space; following mode uses
681
834
  // the pointer position `useChartCursor` already recorded. A follower render always anchors, since
682
- // `follow`'s pointer-tracking path has no pointer to read on this chart.
683
- const useAnchoredPosition = tooltipCfg?.follow === false || isFollowerRender
835
+ // `follow`'s pointer-tracking path has no pointer to read on this chart. An unset `follow` also
836
+ // anchors on touch (spec §5) — the readout would otherwise sit under the finger; an EXPLICIT
837
+ // `follow` (true or false) always wins over that default.
838
+ const useAnchoredPosition =
839
+ tooltipCfg?.follow === false ||
840
+ (tooltipCfg?.follow === undefined && cursor.isTouch) ||
841
+ isFollowerRender
684
842
  const anchorX = useAnchoredPosition && point !== null && cursorX !== null ? cursorX : null
685
843
  const svgRect = anchorX === null ? undefined : svgRef.current?.getBoundingClientRect()
686
844
  const tooltipAnchor =
@@ -704,7 +862,14 @@ function CartesianPlot<T>({
704
862
 
705
863
  return (
706
864
  <>
707
- <svg ref={svgRef} width={plot.width} height={plot.height}>
865
+ <svg
866
+ ref={svgRef}
867
+ width={plot.width}
868
+ height={plot.height}
869
+ // `touch-action` on the plot `<svg>`, not the overlay `<rect>`: Chrome ignores it on an SVG
870
+ // child, so a horizontal scrub was claimed as a pan and cancelled (`R2C-2`).
871
+ style={{ touchAction: 'pan-y' }}
872
+ >
708
873
  <Group left={margin.left} top={margin.top}>
709
874
  {(y?.grid ?? true) && (
710
875
  <GridRows scale={yScale} width={xMax} stroke={VX.grid} numTicks={leftTicks} />
@@ -762,17 +927,22 @@ function CartesianPlot<T>({
762
927
  </>
763
928
  )}
764
929
 
765
- <AxisLeftNumeric
766
- scale={yScale}
767
- numTicks={leftTicks}
768
- tickFormat={(v) => leftFormat(Number(v))}
769
- />
930
+ {yPlacement !== 'none' && (
931
+ <AxisLeftNumeric
932
+ scale={yScale}
933
+ numTicks={leftTicks}
934
+ tickFormat={(v) => leftFormat(Number(v))}
935
+ inside={yPlacement === 'inside'}
936
+ {...(leftTint !== undefined && { tickColor: leftTint })}
937
+ />
938
+ )}
770
939
  {y2Scale !== null && (
771
940
  <AxisRightNumeric
772
941
  scale={y2Scale}
773
942
  left={xMax}
774
943
  numTicks={rightTicks}
775
944
  tickFormat={(v) => rightFormat(Number(v))}
945
+ {...(rightTint !== undefined && { tickColor: rightTint })}
776
946
  />
777
947
  )}
778
948
  <AxisBottomDate
@@ -781,21 +951,32 @@ function CartesianPlot<T>({
781
951
  tickValues={tickValues}
782
952
  tickFormat={(v) => formatX(String(v))}
783
953
  {...(rotate !== 0 && { rotate })}
954
+ anchorTerminals={tight && rotate === 0}
955
+ {...(wrapWidth !== undefined && { wrapWidth })}
784
956
  />
785
957
 
958
+ {endLabels.map(({ series: s, y: labelY }) => (
959
+ <text
960
+ key={`end-${s.key}`}
961
+ x={xMax + END_LABEL_GAP}
962
+ y={labelY}
963
+ dy="0.35em"
964
+ fill={s.color}
965
+ fontFamily={TICK_FONT_FAMILY}
966
+ fontSize={tier.axisFont}
967
+ opacity={
968
+ highlighted === null || s.key === highlighted || s.parent === highlighted ? 1 : 0.25
969
+ }
970
+ pointerEvents="none"
971
+ >
972
+ {s.label}
973
+ </text>
974
+ ))}
975
+
786
976
  <HoverOverlay
787
977
  width={xMax}
788
978
  height={yMax}
789
- onMove={cursor.onPointerMove}
790
- onLeave={cursor.onPointerLeave}
791
- onKeyDown={cursor.onKeyDown}
792
- onBlur={cursor.onBlur}
793
- valueMax={Math.max(data.length - 1, 0)}
794
- {...(point !== null && {
795
- valueNow: data.indexOf(point),
796
- valueText: formatX(getX(point)),
797
- })}
798
- {...(ariaLabel !== undefined && { ariaLabel })}
979
+ {...cursorSliderProps({ cursor: cursor, data: data, getX, formatX, ariaLabel })}
799
980
  />
800
981
  </Group>
801
982
  </svg>
@@ -811,17 +992,7 @@ function CartesianPlot<T>({
811
992
  />
812
993
  <TooltipBody>
813
994
  {tooltipCfg?.prependRows?.(point, { visible, hidden })}
814
- {rows.map((row) => (
815
- <TooltipRow
816
- key={row.key}
817
- color={row.color}
818
- label={row.label}
819
- value={row.value}
820
- shape={row.shape}
821
- dashed={row.dashed}
822
- {...(row.strokeWidth !== undefined && { strokeWidth: row.strokeWidth })}
823
- />
824
- ))}
995
+ <TooltipRows rows={rows} />
825
996
  {tooltipCfg?.extraRows?.(point, { visible, hidden })}
826
997
  </TooltipBody>
827
998
  </ChartTooltipFloat>