basalt-ui 1.30.2 → 1.31.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 (334) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +31 -0
  3. package/MIGRATING.md +172 -12
  4. package/agent/rules/basalt-controls.md +19 -19
  5. package/agent/rules/basalt-mantine.md +1 -1
  6. package/configs/oxlint-plugin.js +254 -4
  7. package/configs/oxlint-plugin.test.ts +145 -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 +54 -41
  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 +145 -131
  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 +83 -68
  47. package/dist/charts/kinds/donut-layout.d.ts +47 -0
  48. package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
  49. package/dist/charts/kinds/donut-layout.js +52 -0
  50. package/dist/charts/layout/auto-margin.d.ts +7 -2
  51. package/dist/charts/layout/auto-margin.d.ts.map +1 -1
  52. package/dist/charts/layout/auto-margin.js +12 -3
  53. package/dist/charts/layout/end-labels.d.ts +64 -0
  54. package/dist/charts/layout/end-labels.d.ts.map +1 -0
  55. package/dist/charts/layout/end-labels.js +76 -0
  56. package/dist/charts/primitives/Axes.d.ts +32 -4
  57. package/dist/charts/primitives/Axes.d.ts.map +1 -1
  58. package/dist/charts/primitives/Axes.js +62 -16
  59. package/dist/charts/primitives/CartesianChart.d.ts +7 -6
  60. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  61. package/dist/charts/primitives/CartesianChart.js +271 -125
  62. package/dist/charts/primitives/ChartCard.d.ts +0 -14
  63. package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
  64. package/dist/charts/primitives/ChartCard.js +74 -21
  65. package/dist/charts/primitives/ChartFrame.d.ts +28 -22
  66. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  67. package/dist/charts/primitives/ChartFrame.js +133 -34
  68. package/dist/charts/primitives/ChartLegend.d.ts +19 -15
  69. package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
  70. package/dist/charts/primitives/ChartLegend.js +212 -71
  71. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  72. package/dist/charts/primitives/ChartTooltip.js +4 -3
  73. package/dist/charts/primitives/Crosshair.js +2 -2
  74. package/dist/charts/primitives/HoverOverlay.d.ts +6 -1
  75. package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
  76. package/dist/charts/primitives/HoverOverlay.js +37 -26
  77. package/dist/charts/primitives/HoverOverlay.module.css +13 -0
  78. package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  79. package/dist/charts/primitives/chart-card-context.d.ts +29 -0
  80. package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
  81. package/dist/charts/primitives/chart-card-context.js +13 -0
  82. package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
  83. package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
  84. package/dist/charts/primitives/chart-frame-layout.js +76 -29
  85. package/dist/charts/primitives/chart-layout.d.ts +112 -0
  86. package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
  87. package/dist/charts/primitives/chart-layout.js +175 -0
  88. package/dist/charts/primitives/chart-tier.d.ts +13 -11
  89. package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
  90. package/dist/charts/primitives/chart-tier.js +42 -13
  91. package/dist/charts/series.d.ts +4 -1
  92. package/dist/charts/series.d.ts.map +1 -1
  93. package/dist/charts/utils/ticks.d.ts +27 -8
  94. package/dist/charts/utils/ticks.d.ts.map +1 -1
  95. package/dist/charts/utils/ticks.js +35 -11
  96. package/dist/common/errors.d.ts +11 -1
  97. package/dist/common/errors.d.ts.map +1 -1
  98. package/dist/common/errors.js +16 -2
  99. package/dist/common/hit-floor.d.ts +10 -0
  100. package/dist/common/hit-floor.d.ts.map +1 -0
  101. package/dist/common/hit-floor.js +6 -0
  102. package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
  103. package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
  104. package/dist/common/use-measured-widths.d.ts +37 -0
  105. package/dist/common/use-measured-widths.d.ts.map +1 -0
  106. package/dist/common/use-measured-widths.js +51 -0
  107. package/dist/common/use-media-query.d.ts +6 -0
  108. package/dist/common/use-media-query.d.ts.map +1 -0
  109. package/dist/common/use-media-query.js +40 -0
  110. package/dist/common/validate.d.ts +9 -0
  111. package/dist/common/validate.d.ts.map +1 -1
  112. package/dist/common/validate.js +9 -4
  113. package/dist/content/article-layout.d.ts +4 -2
  114. package/dist/content/article-layout.d.ts.map +1 -1
  115. package/dist/content/article-layout.js +43 -3
  116. package/dist/content/article-layout.module.css +45 -4
  117. package/dist/content/article-layout.module.css.d.ts +3 -0
  118. package/dist/content/code-block.d.ts.map +1 -1
  119. package/dist/content/code-block.js +40 -2
  120. package/dist/content/code-block.module.css +32 -2
  121. package/dist/content/prose.module.css +13 -8
  122. package/dist/controls/actions.d.ts +29 -6
  123. package/dist/controls/actions.d.ts.map +1 -1
  124. package/dist/controls/actions.js +158 -27
  125. package/dist/controls/actions.module.css +14 -1
  126. package/dist/controls/actions.module.css.d.ts +1 -0
  127. package/dist/controls/controls.module.css +8 -31
  128. package/dist/controls/enum-filter.js +2 -2
  129. package/dist/controls/filter-context.d.ts +10 -0
  130. package/dist/controls/filter-context.d.ts.map +1 -1
  131. package/dist/controls/filter-context.js +2 -0
  132. package/dist/controls/filter-pill.d.ts.map +1 -1
  133. package/dist/controls/filter-pill.js +1 -0
  134. package/dist/controls/index.d.ts +1 -1
  135. package/dist/controls/index.d.ts.map +1 -1
  136. package/dist/controls/index.js +0 -2
  137. package/dist/controls/multi-select-filter.js +2 -2
  138. package/dist/controls/sync-button.module.css +1 -1
  139. package/dist/dashboard/section.d.ts.map +1 -1
  140. package/dist/dashboard/section.js +1 -0
  141. package/dist/dashboard/section.module.css +1 -26
  142. package/dist/dashboard/stat-card.d.ts +1 -1
  143. package/dist/dashboard/stat-card.d.ts.map +1 -1
  144. package/dist/dashboard/stat-card.js +4 -1
  145. package/dist/dashboard/stat-card.module.css +6 -9
  146. package/dist/dashboard/stat-group.module.css +16 -4
  147. package/dist/dashboard/widget-grid.d.ts.map +1 -1
  148. package/dist/dashboard/widget-header.d.ts +14 -4
  149. package/dist/dashboard/widget-header.d.ts.map +1 -1
  150. package/dist/dashboard/widget-header.js +23 -6
  151. package/dist/dashboard/widget-header.module.css +145 -133
  152. package/dist/dashboard/widget-header.module.css.d.ts +2 -0
  153. package/dist/data/data-table.d.ts +23 -0
  154. package/dist/data/data-table.d.ts.map +1 -1
  155. package/dist/data/data-table.js +339 -141
  156. package/dist/data/data-table.module.css +94 -0
  157. package/dist/data/data-table.module.css.d.ts +11 -0
  158. package/dist/data/virtual-list.d.ts.map +1 -1
  159. package/dist/data/virtual-list.js +10 -0
  160. package/dist/forms/form-layout.d.ts.map +1 -1
  161. package/dist/forms/form-layout.js +3 -3
  162. package/dist/forms/form-layout.module.css +30 -5
  163. package/dist/forms/form-layout.module.css.d.ts +1 -0
  164. package/dist/guard/index.d.ts +8 -2
  165. package/dist/guard/index.d.ts.map +1 -1
  166. package/dist/guard/index.js +67 -2
  167. package/dist/guard/types.d.ts +3 -3
  168. package/dist/guard/types.d.ts.map +1 -1
  169. package/dist/index.d.ts +1 -1
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +3 -1
  172. package/dist/provider/index.d.ts +20 -1
  173. package/dist/provider/index.d.ts.map +1 -1
  174. package/dist/provider/index.js +22 -2
  175. package/dist/shell/app-brand.d.ts.map +1 -1
  176. package/dist/shell/app-brand.js +1 -0
  177. package/dist/shell/app-brand.module.css +2 -2
  178. package/dist/shell/app-header.module.css +18 -8
  179. package/dist/shell/app-main.module.css +54 -3
  180. package/dist/shell/app-main.module.css.d.ts +1 -0
  181. package/dist/shell/app-mobile-nav.module.css +16 -4
  182. package/dist/shell/app-sidebar.module.css +4 -6
  183. package/dist/shell/index.d.ts +12 -0
  184. package/dist/shell/index.d.ts.map +1 -1
  185. package/dist/shell/index.js +57 -16
  186. package/dist/shell/mobile-nav-model.d.ts +1 -1
  187. package/dist/shell/page-aside.d.ts +22 -3
  188. package/dist/shell/page-aside.d.ts.map +1 -1
  189. package/dist/shell/page-aside.js +27 -13
  190. package/dist/shell/page-bar.d.ts.map +1 -1
  191. package/dist/shell/page-bar.js +10 -5
  192. package/dist/shell/page-bar.module.css +10 -34
  193. package/dist/shell/use-breakpoint.d.ts +3 -4
  194. package/dist/shell/use-breakpoint.d.ts.map +1 -1
  195. package/dist/shell/use-breakpoint.js +3 -25
  196. package/dist/shell/use-size-class.d.ts +6 -0
  197. package/dist/shell/use-size-class.d.ts.map +1 -0
  198. package/dist/shell/use-size-class.js +19 -0
  199. package/dist/state/persisted-or-local.d.ts +4 -1
  200. package/dist/state/persisted-or-local.d.ts.map +1 -1
  201. package/dist/state/persisted-or-local.js +4 -3
  202. package/dist/state/persisted.d.ts +10 -0
  203. package/dist/state/persisted.d.ts.map +1 -1
  204. package/dist/state/persisted.js +44 -14
  205. package/dist/styles.css +41 -0
  206. package/dist/surfaces.d.ts +4 -4
  207. package/dist/surfaces.d.ts.map +1 -1
  208. package/dist/surfaces.js +11 -3
  209. package/dist/theme/controls.module.css +7 -0
  210. package/dist/theme/ctl-theme.d.ts +1 -0
  211. package/dist/theme/ctl-theme.d.ts.map +1 -1
  212. package/dist/theme/ctl-theme.js +12 -4
  213. package/dist/theme/floating.module.css +36 -0
  214. package/dist/theme/floating.module.css.d.ts +3 -0
  215. package/dist/theme/index.d.ts +2 -2
  216. package/dist/theme/index.d.ts.map +1 -1
  217. package/dist/theme/index.js +34 -5
  218. package/dist/theme/nav-link.module.css +7 -0
  219. package/dist/tokens/index.d.ts +7 -16
  220. package/dist/tokens/index.d.ts.map +1 -1
  221. package/dist/tokens/index.js +23 -28
  222. package/dist/tokens/palette.d.ts +5 -27
  223. package/dist/tokens/palette.d.ts.map +1 -1
  224. package/dist/tokens/palette.js +8 -30
  225. package/dist/tokens/size-classes.d.ts +40 -0
  226. package/dist/tokens/size-classes.d.ts.map +1 -0
  227. package/dist/tokens/size-classes.js +29 -0
  228. package/dist/tokens.css +8 -4
  229. package/llms.txt +3 -3
  230. package/package.json +17 -17
  231. package/src/agent-chat/composer.tsx +74 -47
  232. package/src/agent-chat/thread-workspace.tsx +8 -11
  233. package/src/agent-chat/use-keyboard-inset.ts +68 -0
  234. package/src/charts/cursor/touch-pin.ts +138 -0
  235. package/src/charts/hooks/use-discrete-cursor.ts +335 -0
  236. package/src/charts/hooks/use-hysteresis.ts +26 -0
  237. package/src/charts/hooks/useBandPlot.ts +90 -27
  238. package/src/charts/hooks/useChartCursor.ts +211 -26
  239. package/src/charts/index.ts +0 -8
  240. package/src/charts/kinds/BandStrip.tsx +11 -1
  241. package/src/charts/kinds/Donut.tsx +153 -62
  242. package/src/charts/kinds/DualPanel.tsx +19 -4
  243. package/src/charts/kinds/Heatmap.tsx +61 -22
  244. package/src/charts/kinds/MirroredBars.tsx +15 -3
  245. package/src/charts/kinds/donut-layout.ts +82 -0
  246. package/src/charts/layout/auto-margin.ts +24 -5
  247. package/src/charts/layout/end-labels.ts +135 -0
  248. package/src/charts/primitives/Axes.tsx +112 -21
  249. package/src/charts/primitives/CartesianChart.tsx +282 -68
  250. package/src/charts/primitives/ChartCard.tsx +141 -48
  251. package/src/charts/primitives/ChartFrame.tsx +243 -60
  252. package/src/charts/primitives/ChartLegend.tsx +257 -73
  253. package/src/charts/primitives/ChartTooltip.tsx +8 -3
  254. package/src/charts/primitives/Crosshair.tsx +2 -2
  255. package/src/charts/primitives/HoverOverlay.module.css +13 -0
  256. package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
  257. package/src/charts/primitives/HoverOverlay.tsx +15 -4
  258. package/src/charts/primitives/chart-card-context.ts +37 -0
  259. package/src/charts/primitives/chart-frame-layout.ts +214 -177
  260. package/src/charts/primitives/chart-layout.ts +318 -0
  261. package/src/charts/primitives/chart-tier.tsx +66 -21
  262. package/src/charts/series.ts +4 -1
  263. package/src/charts/utils/ticks.ts +95 -19
  264. package/src/common/errors.ts +25 -1
  265. package/src/common/hit-floor.ts +9 -0
  266. package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
  267. package/src/common/use-measured-widths.ts +88 -0
  268. package/src/common/use-media-query.ts +56 -0
  269. package/src/common/validate.ts +17 -4
  270. package/src/content/article-layout.module.css +45 -4
  271. package/src/content/article-layout.module.css.d.ts +3 -0
  272. package/src/content/article-layout.tsx +60 -5
  273. package/src/content/code-block.module.css +32 -2
  274. package/src/content/code-block.tsx +47 -3
  275. package/src/content/prose.module.css +13 -8
  276. package/src/controls/actions.module.css +14 -1
  277. package/src/controls/actions.module.css.d.ts +1 -0
  278. package/src/controls/actions.tsx +223 -75
  279. package/src/controls/controls.module.css +8 -31
  280. package/src/controls/enum-filter.tsx +2 -2
  281. package/src/controls/filter-context.tsx +11 -0
  282. package/src/controls/filter-pill.tsx +1 -0
  283. package/src/controls/index.ts +0 -1
  284. package/src/controls/multi-select-filter.tsx +2 -2
  285. package/src/controls/sync-button.module.css +1 -1
  286. package/src/dashboard/section.module.css +1 -26
  287. package/src/dashboard/section.tsx +1 -0
  288. package/src/dashboard/stat-card.module.css +6 -9
  289. package/src/dashboard/stat-card.tsx +6 -3
  290. package/src/dashboard/stat-group.module.css +16 -4
  291. package/src/dashboard/widget-grid.tsx +7 -6
  292. package/src/dashboard/widget-header.module.css +145 -133
  293. package/src/dashboard/widget-header.module.css.d.ts +2 -0
  294. package/src/dashboard/widget-header.tsx +45 -11
  295. package/src/data/data-table.module.css +94 -0
  296. package/src/data/data-table.module.css.d.ts +11 -0
  297. package/src/data/data-table.tsx +503 -164
  298. package/src/data/virtual-list.tsx +31 -0
  299. package/src/forms/form-layout.module.css +30 -5
  300. package/src/forms/form-layout.module.css.d.ts +1 -0
  301. package/src/forms/form-layout.tsx +12 -10
  302. package/src/guard/index.ts +183 -3
  303. package/src/guard/types.ts +3 -2
  304. package/src/index.ts +2 -0
  305. package/src/provider/index.tsx +53 -1
  306. package/src/shell/app-brand.module.css +2 -2
  307. package/src/shell/app-brand.tsx +1 -0
  308. package/src/shell/app-header.module.css +18 -8
  309. package/src/shell/app-main.module.css +54 -3
  310. package/src/shell/app-main.module.css.d.ts +1 -0
  311. package/src/shell/app-mobile-nav.module.css +16 -4
  312. package/src/shell/app-sidebar.module.css +4 -6
  313. package/src/shell/index.tsx +200 -129
  314. package/src/shell/mobile-nav-model.ts +1 -1
  315. package/src/shell/page-aside.tsx +70 -21
  316. package/src/shell/page-bar.module.css +10 -34
  317. package/src/shell/page-bar.tsx +10 -5
  318. package/src/shell/use-breakpoint.ts +9 -45
  319. package/src/shell/use-size-class.ts +31 -0
  320. package/src/state/persisted-or-local.ts +10 -4
  321. package/src/state/persisted.ts +76 -17
  322. package/src/styles.css +41 -0
  323. package/src/surfaces.ts +9 -1
  324. package/src/theme/controls.module.css +7 -0
  325. package/src/theme/ctl-theme.tsx +15 -3
  326. package/src/theme/floating.module.css +36 -0
  327. package/src/theme/floating.module.css.d.ts +3 -0
  328. package/src/theme/index.ts +46 -8
  329. package/src/theme/nav-link.module.css +7 -0
  330. package/src/tokens/index.ts +20 -30
  331. package/src/tokens/palette.ts +6 -28
  332. package/src/tokens/size-classes.ts +46 -0
  333. package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
  334. /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
@@ -4,21 +4,33 @@ 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
35
  import { ChartTooltipFloat, TooltipBody, TooltipHeader, TooltipRow } from './ChartTooltip'
24
36
  import { Crosshair, SeriesDot } from './Crosshair'
@@ -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,49 @@ function buildYScale<T>(
354
404
  })
355
405
  }
356
406
 
407
+ /**
408
+ * `tickLabelPx`'s resolver, split out of a two-deep ternary: a 45° label projects wider than its
409
+ * flat glyph box, so it measures through {@link rotatedXLabelPx} instead of the flat/wrapped
410
+ * numbers every other rotation uses; a wrapped flat label (`wrapWidth` set) uses ITS width, an
411
+ * unwrapped one the plain flat width. `rotate === 90` falls through to the flat/wrapped branch
412
+ * unchanged — `wrapWidth` is only ever set for `rotate === 0` (see its own definition), so a 90°
413
+ * axis always resolves to `flatLabelPx` here, matching the pre-split behaviour exactly.
414
+ */
415
+ function resolveTickLabelPx(input: {
416
+ rotate: 0 | 45 | 90
417
+ wrapWidth: number | undefined
418
+ xLabels: string[]
419
+ axisFont: number
420
+ flatLabelPx: number
421
+ wrappedLabelPx: number
422
+ }): number {
423
+ const { rotate, wrapWidth, xLabels, axisFont, flatLabelPx, wrappedLabelPx } = input
424
+ if (rotate === 45) return rotatedXLabelPx(xLabels, axisFont)
425
+ return wrapWidth === undefined ? flatLabelPx : wrappedLabelPx
426
+ }
427
+
428
+ /**
429
+ * `baseMargin`'s resolver, split out of the same two-deep ternary: an unrotated, unwrapped axis
430
+ * keeps the already-measured flat margin; wrapping re-measures for the extra line(s)
431
+ * (`bottomLines`); a 45° rotation reuses its own pre-measured margin ({@link rotated45Margin}); a
432
+ * 90° one measures fresh (no pre-measured twin exists for it, unlike 45°).
433
+ */
434
+ function resolveBaseMargin(input: {
435
+ rotate: 0 | 45 | 90
436
+ wrapWidth: number | undefined
437
+ flatMargin: ChartMargin
438
+ rotated45Margin: ChartMargin
439
+ marginInput: AutoMarginInput
440
+ xPlanLines: number
441
+ }): ChartMargin {
442
+ const { rotate, wrapWidth, flatMargin, rotated45Margin, marginInput, xPlanLines } = input
443
+ if (rotate === 45) return rotated45Margin
444
+ if (rotate === 90) return autoMargin({ ...marginInput, rotate })
445
+ return wrapWidth === undefined
446
+ ? flatMargin
447
+ : autoMargin({ ...marginInput, bottomLines: xPlanLines })
448
+ }
449
+
357
450
  /**
358
451
  * The cartesian chart primitive — the rung that was missing between `ChartFrame` and raw visx
359
452
  * (`docs/CHARTS-SPEC.md` §2).
@@ -458,7 +551,7 @@ type CartesianPlotProps<T> = Omit<
458
551
  | 'className'
459
552
  | 'style'
460
553
  > & {
461
- plot: { width: number; height: number; hidden: ReadonlySet<string> }
554
+ plot: { width: number; height: number; hidden: ReadonlySet<string>; legendInHeader: boolean }
462
555
  highlighted: string | null
463
556
  marginOverride?: Partial<ChartMargin>
464
557
  }
@@ -492,7 +585,9 @@ function CartesianPlot<T>({
492
585
  // The tier `ChartFrame` resolved from its own MEASURED width. Everything sized by it here is
493
586
  // the half a hook cannot reach: the tick font the margins are MEASURED at, and the margin
494
587
  // floors themselves (`docs/CHARTS-SPEC.md` §8).
495
- const tier = useChartTierMetrics()
588
+ const tier = useChartMetrics()
589
+ const containerClass = useChartContainerClass()
590
+ const tight = isTightClass(containerClass)
496
591
 
497
592
  const visible = useMemo(() => series.filter((s) => !hidden.has(s.key)), [series, hidden])
498
593
  const leftSeries = useMemo(() => visible.filter((s) => s.axis !== 'right'), [visible])
@@ -526,17 +621,21 @@ function CartesianPlot<T>({
526
621
  [y2, data, rightSeries, rightBounds],
527
622
  )
528
623
 
529
- const leftTicks = y?.ticks ?? DEFAULT_TICKS
530
- const rightTicks = y2?.ticks ?? DEFAULT_TICKS
624
+ // Tick count is a law of the plot height (one per ~44px, 2 under `VX.minPlotHeight`), estimated
625
+ // from the token floors because the real margins depend on the labels these ticks produce. A
626
+ // consumer's `ticks` always wins.
627
+ const estimatedPlotHeight = plot.height - tier.margin.top - tier.margin.bottom
628
+ const leftTicks = y?.ticks ?? yTickCount(estimatedPlotHeight)
629
+ const rightTicks = y2?.ticks ?? yTickCount(estimatedPlotHeight)
531
630
 
532
631
  const { labels: leftLabels, format: leftFormat } = useMemo(
533
632
  () =>
534
- probeAxisLabels({
633
+ probeYAxis({
535
634
  domain: leftDomain,
536
635
  ticks: leftTicks,
537
636
  nice: y?.nice ?? false,
538
- ...(y?.scale !== undefined && { scale: y.scale }),
539
- ...(y?.format !== undefined && { format: y.format }),
637
+ scale: y?.scale,
638
+ format: y?.format,
540
639
  }),
541
640
  [leftDomain, leftTicks, y?.format, y?.nice, y?.scale],
542
641
  )
@@ -545,18 +644,27 @@ function CartesianPlot<T>({
545
644
  () =>
546
645
  rightDomain === null
547
646
  ? { labels: [], format: (v: number) => String(v) }
548
- : probeAxisLabels({
647
+ : probeYAxis({
549
648
  domain: rightDomain,
550
649
  ticks: rightTicks,
551
650
  nice: y2?.nice ?? false,
552
- ...(y2?.scale !== undefined && { scale: y2.scale }),
553
- ...(y2?.format !== undefined && { format: y2.format }),
651
+ scale: y2?.scale,
652
+ format: y2?.format,
554
653
  }),
555
654
  [rightDomain, rightTicks, y2?.format, y2?.nice, y2?.scale],
556
655
  )
557
656
 
558
657
  const xLabels = useMemo(() => keys.map(formatX), [keys, formatX])
559
658
 
659
+ // Ladder step 5: at compact/micro the y labels sit inside the plot (micro: no y axis), so the
660
+ // left gutter drops to its floor. An explicit `margin.left` means the consumer laid out an
661
+ // outside gutter — it keeps the outside axis. `CartesianChart` always has a left axis.
662
+ const yPlacement = resolveYPlacement(containerClass, { marginOverrideLeft: marginOverride?.left })
663
+ // A whitespace test misclassified any spaced date format ("Mar 01") as categorical, which then
664
+ // wrapped or rotated a time axis that should only ever thin (§ ladder step 4). Domain kind is
665
+ // the actual law: only a band (category) domain wraps or rotates; time and number domains thin.
666
+ const categorical = tight && classifyDomain(keys) === 'band'
667
+
560
668
  /** The horizontal room one x tick label needs: the widest string that could be painted, plus
561
669
  * breathing space to its neighbour. Feeds `smartTicks`, which otherwise thinned the axis by a
562
670
  * constant that knew nothing about the label. Measured at the tick font the axis will actually
@@ -567,14 +675,26 @@ function CartesianPlot<T>({
567
675
  * measured label can never differ between the one that DECIDES and the one that PAINTS. */
568
676
  const marginInput = useMemo(
569
677
  () => ({
570
- left: leftLabels,
678
+ left: yPlacement === 'outside' ? leftLabels : [],
571
679
  right: rightLabels,
572
680
  bottom: xLabels,
573
681
  fontPx: tier.axisFont,
574
- floor: tier.margin,
682
+ // An inside-placed y label paints IN the plot, so the left gutter it used to reserve
683
+ // (the phone tier's own margin floor) is dead space — only the inside-label floor applies.
684
+ floor: resolveMarginFloor(yPlacement, tier.margin),
685
+ anchorTerminals: tight,
575
686
  ...(marginOverride !== undefined && { override: marginOverride }),
576
687
  }),
577
- [leftLabels, rightLabels, xLabels, tier.axisFont, tier.margin, marginOverride],
688
+ [
689
+ leftLabels,
690
+ rightLabels,
691
+ xLabels,
692
+ tier.axisFont,
693
+ tier.margin,
694
+ marginOverride,
695
+ tight,
696
+ yPlacement,
697
+ ],
578
698
  )
579
699
 
580
700
  /** The margin an UNROTATED axis resolves to. It is the final margin in every case except an
@@ -582,73 +702,122 @@ function CartesianPlot<T>({
582
702
  * deciding needs an `xMax`, and `xMax` needs a margin, so the unrotated pass breaks the loop. */
583
703
  const flatMargin = useMemo(() => autoMargin(marginInput), [marginInput])
584
704
 
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). */
705
+ /** And the margin a 45° axis would resolve to — the OTHER half of the decision, needed only for
706
+ * a categorical (band) domain that still overflows once wrapped (`planXLabels`'s own rotate
707
+ * case). A rotated label reaches into the left gutter, so rotating spends plot width. */
589
708
  const rotated45Margin = useMemo(() => autoMargin({ ...marginInput, rotate: 45 }), [marginInput])
590
709
 
591
710
  // 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
- })
711
+ // opt-out. Only an unset one falls through to the domain-driven default: a categorical (band)
712
+ // axis wraps before it rotates, and rotates only when even wrapped it doesn't fit
713
+ // (`planXLabels`). A time or number domain never wraps or rotates — it only thins.
714
+ const flatXMax = Math.max(plot.width - flatMargin.left - flatMargin.right, 0)
715
+ const xPlan = useMemo(
716
+ () => planXLabels({ labels: xLabels, plotWidth: flatXMax, fontPx: tier.axisFont, categorical }),
717
+ [xLabels, flatXMax, tier.axisFont, categorical],
718
+ )
719
+ const rotate = xLabelRotate ?? (categorical ? xPlan.rotate : 0)
720
+ const wrapWidth = rotate === 0 && xPlan.wrap ? xPlan.wrapPx : undefined
721
+ // A 90° label is one line box wide, so the constant floor governs it; a 45° one projects wider.
722
+ const tickLabelPx = resolveTickLabelPx({
723
+ rotate,
724
+ wrapWidth,
725
+ xLabels,
726
+ axisFont: tier.axisFont,
727
+ flatLabelPx: xLabelPx,
728
+ wrappedLabelPx: xPlan.labelPx,
729
+ })
730
+
731
+ const baseMargin = useMemo(
732
+ () =>
733
+ resolveBaseMargin({
734
+ rotate,
735
+ wrapWidth,
736
+ flatMargin,
737
+ rotated45Margin,
738
+ marginInput,
739
+ xPlanLines: xPlan.lines,
740
+ }),
741
+ [flatMargin, rotated45Margin, marginInput, rotate, wrapWidth, xPlan.lines],
742
+ )
601
743
 
744
+ // End-of-line labels are planned FIRST, against the base margin: the right gutter is reserved
745
+ // only when the labels will actually draw. `yScale` depends on the plot height alone, so it is
746
+ // safe to build before the right margin is settled.
747
+ const yMax = Math.max(plot.height - baseMargin.top - baseMargin.bottom, 0)
748
+ const yScale = useMemo(
749
+ () => buildYScale(y, leftDomain, yMax, leftTicks),
750
+ [y, leftDomain, yMax, leftTicks],
751
+ )
752
+ const endPlan = useMemo(
753
+ () =>
754
+ planEndLabels({
755
+ containerClass,
756
+ hasY2: y2 !== undefined,
757
+ hasMarginRight: marginOverride?.right !== undefined,
758
+ hasHeaderLegend: plot.legendInHeader,
759
+ visible,
760
+ data,
761
+ yScale,
762
+ yMax,
763
+ fontPx: tier.axisFont,
764
+ plotWidth: plot.width - baseMargin.left - baseMargin.right,
765
+ }),
766
+ [
767
+ containerClass,
768
+ y2,
769
+ marginOverride?.right,
770
+ plot.legendInHeader,
771
+ visible,
772
+ data,
773
+ yScale,
774
+ yMax,
775
+ tier.axisFont,
776
+ plot.width,
777
+ baseMargin.left,
778
+ baseMargin.right,
779
+ ],
780
+ )
781
+ const endLabels = endPlan?.labels ?? []
602
782
  const margin = useMemo(
603
783
  () =>
604
- rotate === 0
605
- ? flatMargin
606
- : rotate === 45
607
- ? rotated45Margin
608
- : autoMargin({ ...marginInput, rotate }),
609
- [flatMargin, rotated45Margin, marginInput, rotate],
784
+ endPlan === null ? baseMargin : { ...baseMargin, right: baseMargin.right + endPlan.gutter },
785
+ [baseMargin, endPlan],
610
786
  )
611
787
 
612
788
  // ── Pass 2: the real scales, now that the plot rect is known ────────────────────────────────
613
789
  const xMax = Math.max(plot.width - margin.left - margin.right, 0)
614
- const yMax = Math.max(plot.height - margin.top - margin.bottom, 0)
615
790
 
616
791
  const xScale = useMemo(
617
792
  () => scalePoint<string>({ domain: keys, range: [0, xMax], padding: 0.5 }),
618
793
  [keys, xMax],
619
794
  )
620
- const yScale = useMemo(
621
- () => buildYScale(y, leftDomain, yMax, leftTicks),
622
- [y, leftDomain, yMax, leftTicks],
623
- )
624
795
  const y2Scale = useMemo(
625
796
  () => (rightDomain === null ? null : buildYScale(y2, rightDomain, yMax, rightTicks)),
626
797
  [y2, rightDomain, yMax, rightTicks],
627
798
  )
628
799
 
629
800
  // 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
- )
801
+ const tickValues = useMemo(() => {
802
+ if (xTickValues !== undefined) return [...xTickValues(keys, xMax)]
803
+ if (xTicks !== undefined) return smartTicksEvery(keys, xTicks)
804
+ // Micro: the two terminals only.
805
+ if (containerClass === 'micro')
806
+ return keys.length > 1 ? [keys[0]!, keys[keys.length - 1]!] : keys
807
+ return smartTicks(keys, xMax, rotate === 0 || rotate === 45 ? tickLabelPx : undefined, tight)
808
+ }, [keys, xMax, xTicks, xTickValues, tickLabelPx, rotate, containerClass, tight])
641
809
 
810
+ const svgRef = useRef<SVGSVGElement>(null)
642
811
  const cursor = useChartCursor<T>({
643
812
  data,
644
813
  chartId,
645
814
  getKey: getX,
646
815
  xScale,
647
816
  marginLeft: margin.left,
817
+ boundaryRef: svgRef,
648
818
  ...(cursorResolution !== undefined && { resolution: cursorResolution }),
649
819
  })
650
820
 
651
- const svgRef = useRef<SVGSVGElement>(null)
652
821
  const tooltipEnabled = tooltip !== false
653
822
  const tooltipCfg = tooltip === false ? undefined : tooltip
654
823
  const point = cursor.point
@@ -668,6 +837,12 @@ function CartesianPlot<T>({
668
837
  [visible, leftFormat, rightFormat],
669
838
  )
670
839
 
840
+ // Dual axis with exactly one visible series per side: the tick labels carry that series' colour,
841
+ // so the axis says which line it reads. Any other split leaves both axes neutral.
842
+ const tintAxes = y2 !== undefined && leftSeries.length === 1 && rightSeries.length === 1
843
+ const leftTint = tintAxes ? alpha(leftSeries[0]!.color, AXIS_TINT_ALPHA) : undefined
844
+ const rightTint = tintAxes ? alpha(rightSeries[0]!.color, AXIS_TINT_ALPHA) : undefined
845
+
671
846
  const rows =
672
847
  tooltipEnabled && point !== null ? deriveTooltipRows(tooltipSeries, point, leftFormat) : []
673
848
 
@@ -679,8 +854,13 @@ function CartesianPlot<T>({
679
854
 
680
855
  // Anchored mode resolves the crosshair's plot-local x into viewport space; following mode uses
681
856
  // 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
857
+ // `follow`'s pointer-tracking path has no pointer to read on this chart. An unset `follow` also
858
+ // anchors on touch (spec §5) — the readout would otherwise sit under the finger; an EXPLICIT
859
+ // `follow` (true or false) always wins over that default.
860
+ const useAnchoredPosition =
861
+ tooltipCfg?.follow === false ||
862
+ (tooltipCfg?.follow === undefined && cursor.isTouch) ||
863
+ isFollowerRender
684
864
  const anchorX = useAnchoredPosition && point !== null && cursorX !== null ? cursorX : null
685
865
  const svgRect = anchorX === null ? undefined : svgRef.current?.getBoundingClientRect()
686
866
  const tooltipAnchor =
@@ -704,7 +884,14 @@ function CartesianPlot<T>({
704
884
 
705
885
  return (
706
886
  <>
707
- <svg ref={svgRef} width={plot.width} height={plot.height}>
887
+ <svg
888
+ ref={svgRef}
889
+ width={plot.width}
890
+ height={plot.height}
891
+ // `touch-action` on the plot `<svg>`, not the overlay `<rect>`: Chrome ignores it on an SVG
892
+ // child, so a horizontal scrub was claimed as a pan and cancelled (`R2C-2`).
893
+ style={{ touchAction: 'pan-y' }}
894
+ >
708
895
  <Group left={margin.left} top={margin.top}>
709
896
  {(y?.grid ?? true) && (
710
897
  <GridRows scale={yScale} width={xMax} stroke={VX.grid} numTicks={leftTicks} />
@@ -762,17 +949,22 @@ function CartesianPlot<T>({
762
949
  </>
763
950
  )}
764
951
 
765
- <AxisLeftNumeric
766
- scale={yScale}
767
- numTicks={leftTicks}
768
- tickFormat={(v) => leftFormat(Number(v))}
769
- />
952
+ {yPlacement !== 'none' && (
953
+ <AxisLeftNumeric
954
+ scale={yScale}
955
+ numTicks={leftTicks}
956
+ tickFormat={(v) => leftFormat(Number(v))}
957
+ inside={yPlacement === 'inside'}
958
+ {...(leftTint !== undefined && { tickColor: leftTint })}
959
+ />
960
+ )}
770
961
  {y2Scale !== null && (
771
962
  <AxisRightNumeric
772
963
  scale={y2Scale}
773
964
  left={xMax}
774
965
  numTicks={rightTicks}
775
966
  tickFormat={(v) => rightFormat(Number(v))}
967
+ {...(rightTint !== undefined && { tickColor: rightTint })}
776
968
  />
777
969
  )}
778
970
  <AxisBottomDate
@@ -781,12 +973,34 @@ function CartesianPlot<T>({
781
973
  tickValues={tickValues}
782
974
  tickFormat={(v) => formatX(String(v))}
783
975
  {...(rotate !== 0 && { rotate })}
976
+ anchorTerminals={tight && rotate === 0}
977
+ {...(wrapWidth !== undefined && { wrapWidth })}
784
978
  />
785
979
 
980
+ {endLabels.map(({ series: s, y: labelY }) => (
981
+ <text
982
+ key={`end-${s.key}`}
983
+ x={xMax + END_LABEL_GAP}
984
+ y={labelY}
985
+ dy="0.35em"
986
+ fill={s.color}
987
+ fontFamily={TICK_FONT_FAMILY}
988
+ fontSize={tier.axisFont}
989
+ opacity={
990
+ highlighted === null || s.key === highlighted || s.parent === highlighted ? 1 : 0.25
991
+ }
992
+ pointerEvents="none"
993
+ >
994
+ {s.label}
995
+ </text>
996
+ ))}
997
+
786
998
  <HoverOverlay
787
999
  width={xMax}
788
1000
  height={yMax}
789
1001
  onMove={cursor.onPointerMove}
1002
+ onDown={cursor.onPointerDown}
1003
+ onUp={cursor.onPointerUp}
790
1004
  onLeave={cursor.onPointerLeave}
791
1005
  onKeyDown={cursor.onKeyDown}
792
1006
  onBlur={cursor.onBlur}