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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +31 -0
- package/MIGRATING.md +172 -12
- package/agent/rules/basalt-controls.md +19 -19
- package/agent/rules/basalt-mantine.md +1 -1
- package/configs/oxlint-plugin.js +254 -4
- package/configs/oxlint-plugin.test.ts +145 -2
- package/configs/oxlint.json +2 -1
- package/dist/agent-chat/composer.d.ts.map +1 -1
- package/dist/agent-chat/composer.js +41 -43
- package/dist/agent-chat/thread-workspace.d.ts +2 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +3 -4
- package/dist/agent-chat/use-keyboard-inset.d.ts +2 -0
- package/dist/agent-chat/use-keyboard-inset.d.ts.map +1 -0
- package/dist/agent-chat/use-keyboard-inset.js +34 -0
- package/dist/charts/cursor/touch-pin.d.ts +45 -0
- package/dist/charts/cursor/touch-pin.d.ts.map +1 -0
- package/dist/charts/cursor/touch-pin.js +79 -0
- package/dist/charts/hooks/use-discrete-cursor.d.ts +66 -0
- package/dist/charts/hooks/use-discrete-cursor.d.ts.map +1 -0
- package/dist/charts/hooks/use-discrete-cursor.js +198 -0
- package/dist/charts/hooks/use-hysteresis.d.ts +18 -0
- package/dist/charts/hooks/use-hysteresis.d.ts.map +1 -0
- package/dist/charts/hooks/use-hysteresis.js +12 -0
- package/dist/charts/hooks/useBandPlot.d.ts +7 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -1
- package/dist/charts/hooks/useBandPlot.js +62 -18
- package/dist/charts/hooks/useChartCursor.d.ts +40 -11
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +102 -11
- package/dist/charts/index.d.ts +0 -1
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +0 -10
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +54 -41
- package/dist/charts/kinds/Donut.d.ts +9 -3
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +110 -48
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +145 -131
- package/dist/charts/kinds/Heatmap.d.ts +7 -0
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +37 -16
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +83 -68
- package/dist/charts/kinds/donut-layout.d.ts +47 -0
- package/dist/charts/kinds/donut-layout.d.ts.map +1 -0
- package/dist/charts/kinds/donut-layout.js +52 -0
- package/dist/charts/layout/auto-margin.d.ts +7 -2
- package/dist/charts/layout/auto-margin.d.ts.map +1 -1
- package/dist/charts/layout/auto-margin.js +12 -3
- package/dist/charts/layout/end-labels.d.ts +64 -0
- package/dist/charts/layout/end-labels.d.ts.map +1 -0
- package/dist/charts/layout/end-labels.js +76 -0
- package/dist/charts/primitives/Axes.d.ts +32 -4
- package/dist/charts/primitives/Axes.d.ts.map +1 -1
- package/dist/charts/primitives/Axes.js +62 -16
- package/dist/charts/primitives/CartesianChart.d.ts +7 -6
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +271 -125
- package/dist/charts/primitives/ChartCard.d.ts +0 -14
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +74 -21
- package/dist/charts/primitives/ChartFrame.d.ts +28 -22
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +133 -34
- package/dist/charts/primitives/ChartLegend.d.ts +19 -15
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +212 -71
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +4 -3
- package/dist/charts/primitives/Crosshair.js +2 -2
- package/dist/charts/primitives/HoverOverlay.d.ts +6 -1
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +37 -26
- package/dist/charts/primitives/HoverOverlay.module.css +13 -0
- package/dist/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
- package/dist/charts/primitives/chart-card-context.d.ts +29 -0
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -0
- package/dist/charts/primitives/chart-card-context.js +13 -0
- package/dist/charts/primitives/chart-frame-layout.d.ts +93 -113
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +76 -29
- package/dist/charts/primitives/chart-layout.d.ts +112 -0
- package/dist/charts/primitives/chart-layout.d.ts.map +1 -0
- package/dist/charts/primitives/chart-layout.js +175 -0
- package/dist/charts/primitives/chart-tier.d.ts +13 -11
- package/dist/charts/primitives/chart-tier.d.ts.map +1 -1
- package/dist/charts/primitives/chart-tier.js +42 -13
- package/dist/charts/series.d.ts +4 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/utils/ticks.d.ts +27 -8
- package/dist/charts/utils/ticks.d.ts.map +1 -1
- package/dist/charts/utils/ticks.js +35 -11
- package/dist/common/errors.d.ts +11 -1
- package/dist/common/errors.d.ts.map +1 -1
- package/dist/common/errors.js +16 -2
- package/dist/common/hit-floor.d.ts +10 -0
- package/dist/common/hit-floor.d.ts.map +1 -0
- package/dist/common/hit-floor.js +6 -0
- package/dist/{shell → common}/isomorphic-layout-effect.d.ts +4 -3
- package/dist/common/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/common/use-measured-widths.d.ts +37 -0
- package/dist/common/use-measured-widths.d.ts.map +1 -0
- package/dist/common/use-measured-widths.js +51 -0
- package/dist/common/use-media-query.d.ts +6 -0
- package/dist/common/use-media-query.d.ts.map +1 -0
- package/dist/common/use-media-query.js +40 -0
- package/dist/common/validate.d.ts +9 -0
- package/dist/common/validate.d.ts.map +1 -1
- package/dist/common/validate.js +9 -4
- package/dist/content/article-layout.d.ts +4 -2
- package/dist/content/article-layout.d.ts.map +1 -1
- package/dist/content/article-layout.js +43 -3
- package/dist/content/article-layout.module.css +45 -4
- package/dist/content/article-layout.module.css.d.ts +3 -0
- package/dist/content/code-block.d.ts.map +1 -1
- package/dist/content/code-block.js +40 -2
- package/dist/content/code-block.module.css +32 -2
- package/dist/content/prose.module.css +13 -8
- package/dist/controls/actions.d.ts +29 -6
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +158 -27
- package/dist/controls/actions.module.css +14 -1
- package/dist/controls/actions.module.css.d.ts +1 -0
- package/dist/controls/controls.module.css +8 -31
- package/dist/controls/enum-filter.js +2 -2
- package/dist/controls/filter-context.d.ts +10 -0
- package/dist/controls/filter-context.d.ts.map +1 -1
- package/dist/controls/filter-context.js +2 -0
- package/dist/controls/filter-pill.d.ts.map +1 -1
- package/dist/controls/filter-pill.js +1 -0
- package/dist/controls/index.d.ts +1 -1
- package/dist/controls/index.d.ts.map +1 -1
- package/dist/controls/index.js +0 -2
- package/dist/controls/multi-select-filter.js +2 -2
- package/dist/controls/sync-button.module.css +1 -1
- package/dist/dashboard/section.d.ts.map +1 -1
- package/dist/dashboard/section.js +1 -0
- package/dist/dashboard/section.module.css +1 -26
- package/dist/dashboard/stat-card.d.ts +1 -1
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +4 -1
- package/dist/dashboard/stat-card.module.css +6 -9
- package/dist/dashboard/stat-group.module.css +16 -4
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-header.d.ts +14 -4
- package/dist/dashboard/widget-header.d.ts.map +1 -1
- package/dist/dashboard/widget-header.js +23 -6
- package/dist/dashboard/widget-header.module.css +145 -133
- package/dist/dashboard/widget-header.module.css.d.ts +2 -0
- package/dist/data/data-table.d.ts +23 -0
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +339 -141
- package/dist/data/data-table.module.css +94 -0
- package/dist/data/data-table.module.css.d.ts +11 -0
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual-list.js +10 -0
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.js +3 -3
- package/dist/forms/form-layout.module.css +30 -5
- package/dist/forms/form-layout.module.css.d.ts +1 -0
- package/dist/guard/index.d.ts +8 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +67 -2
- package/dist/guard/types.d.ts +3 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/provider/index.d.ts +20 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +22 -2
- package/dist/shell/app-brand.d.ts.map +1 -1
- package/dist/shell/app-brand.js +1 -0
- package/dist/shell/app-brand.module.css +2 -2
- package/dist/shell/app-header.module.css +18 -8
- package/dist/shell/app-main.module.css +54 -3
- package/dist/shell/app-main.module.css.d.ts +1 -0
- package/dist/shell/app-mobile-nav.module.css +16 -4
- package/dist/shell/app-sidebar.module.css +4 -6
- package/dist/shell/index.d.ts +12 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +57 -16
- package/dist/shell/mobile-nav-model.d.ts +1 -1
- package/dist/shell/page-aside.d.ts +22 -3
- package/dist/shell/page-aside.d.ts.map +1 -1
- package/dist/shell/page-aside.js +27 -13
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +10 -5
- package/dist/shell/page-bar.module.css +10 -34
- package/dist/shell/use-breakpoint.d.ts +3 -4
- package/dist/shell/use-breakpoint.d.ts.map +1 -1
- package/dist/shell/use-breakpoint.js +3 -25
- package/dist/shell/use-size-class.d.ts +6 -0
- package/dist/shell/use-size-class.d.ts.map +1 -0
- package/dist/shell/use-size-class.js +19 -0
- package/dist/state/persisted-or-local.d.ts +4 -1
- package/dist/state/persisted-or-local.d.ts.map +1 -1
- package/dist/state/persisted-or-local.js +4 -3
- package/dist/state/persisted.d.ts +10 -0
- package/dist/state/persisted.d.ts.map +1 -1
- package/dist/state/persisted.js +44 -14
- package/dist/styles.css +41 -0
- package/dist/surfaces.d.ts +4 -4
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +11 -3
- package/dist/theme/controls.module.css +7 -0
- package/dist/theme/ctl-theme.d.ts +1 -0
- package/dist/theme/ctl-theme.d.ts.map +1 -1
- package/dist/theme/ctl-theme.js +12 -4
- package/dist/theme/floating.module.css +36 -0
- package/dist/theme/floating.module.css.d.ts +3 -0
- package/dist/theme/index.d.ts +2 -2
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +34 -5
- package/dist/theme/nav-link.module.css +7 -0
- package/dist/tokens/index.d.ts +7 -16
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +23 -28
- package/dist/tokens/palette.d.ts +5 -27
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +8 -30
- package/dist/tokens/size-classes.d.ts +40 -0
- package/dist/tokens/size-classes.d.ts.map +1 -0
- package/dist/tokens/size-classes.js +29 -0
- package/dist/tokens.css +8 -4
- package/llms.txt +3 -3
- package/package.json +17 -17
- package/src/agent-chat/composer.tsx +74 -47
- package/src/agent-chat/thread-workspace.tsx +8 -11
- package/src/agent-chat/use-keyboard-inset.ts +68 -0
- package/src/charts/cursor/touch-pin.ts +138 -0
- package/src/charts/hooks/use-discrete-cursor.ts +335 -0
- package/src/charts/hooks/use-hysteresis.ts +26 -0
- package/src/charts/hooks/useBandPlot.ts +90 -27
- package/src/charts/hooks/useChartCursor.ts +211 -26
- package/src/charts/index.ts +0 -8
- package/src/charts/kinds/BandStrip.tsx +11 -1
- package/src/charts/kinds/Donut.tsx +153 -62
- package/src/charts/kinds/DualPanel.tsx +19 -4
- package/src/charts/kinds/Heatmap.tsx +61 -22
- package/src/charts/kinds/MirroredBars.tsx +15 -3
- package/src/charts/kinds/donut-layout.ts +82 -0
- package/src/charts/layout/auto-margin.ts +24 -5
- package/src/charts/layout/end-labels.ts +135 -0
- package/src/charts/primitives/Axes.tsx +112 -21
- package/src/charts/primitives/CartesianChart.tsx +282 -68
- package/src/charts/primitives/ChartCard.tsx +141 -48
- package/src/charts/primitives/ChartFrame.tsx +243 -60
- package/src/charts/primitives/ChartLegend.tsx +257 -73
- package/src/charts/primitives/ChartTooltip.tsx +8 -3
- package/src/charts/primitives/Crosshair.tsx +2 -2
- package/src/charts/primitives/HoverOverlay.module.css +13 -0
- package/src/charts/primitives/HoverOverlay.module.css.d.ts +8 -0
- package/src/charts/primitives/HoverOverlay.tsx +15 -4
- package/src/charts/primitives/chart-card-context.ts +37 -0
- package/src/charts/primitives/chart-frame-layout.ts +214 -177
- package/src/charts/primitives/chart-layout.ts +318 -0
- package/src/charts/primitives/chart-tier.tsx +66 -21
- package/src/charts/series.ts +4 -1
- package/src/charts/utils/ticks.ts +95 -19
- package/src/common/errors.ts +25 -1
- package/src/common/hit-floor.ts +9 -0
- package/src/{shell → common}/isomorphic-layout-effect.ts +4 -3
- package/src/common/use-measured-widths.ts +88 -0
- package/src/common/use-media-query.ts +56 -0
- package/src/common/validate.ts +17 -4
- package/src/content/article-layout.module.css +45 -4
- package/src/content/article-layout.module.css.d.ts +3 -0
- package/src/content/article-layout.tsx +60 -5
- package/src/content/code-block.module.css +32 -2
- package/src/content/code-block.tsx +47 -3
- package/src/content/prose.module.css +13 -8
- package/src/controls/actions.module.css +14 -1
- package/src/controls/actions.module.css.d.ts +1 -0
- package/src/controls/actions.tsx +223 -75
- package/src/controls/controls.module.css +8 -31
- package/src/controls/enum-filter.tsx +2 -2
- package/src/controls/filter-context.tsx +11 -0
- package/src/controls/filter-pill.tsx +1 -0
- package/src/controls/index.ts +0 -1
- package/src/controls/multi-select-filter.tsx +2 -2
- package/src/controls/sync-button.module.css +1 -1
- package/src/dashboard/section.module.css +1 -26
- package/src/dashboard/section.tsx +1 -0
- package/src/dashboard/stat-card.module.css +6 -9
- package/src/dashboard/stat-card.tsx +6 -3
- package/src/dashboard/stat-group.module.css +16 -4
- package/src/dashboard/widget-grid.tsx +7 -6
- package/src/dashboard/widget-header.module.css +145 -133
- package/src/dashboard/widget-header.module.css.d.ts +2 -0
- package/src/dashboard/widget-header.tsx +45 -11
- package/src/data/data-table.module.css +94 -0
- package/src/data/data-table.module.css.d.ts +11 -0
- package/src/data/data-table.tsx +503 -164
- package/src/data/virtual-list.tsx +31 -0
- package/src/forms/form-layout.module.css +30 -5
- package/src/forms/form-layout.module.css.d.ts +1 -0
- package/src/forms/form-layout.tsx +12 -10
- package/src/guard/index.ts +183 -3
- package/src/guard/types.ts +3 -2
- package/src/index.ts +2 -0
- package/src/provider/index.tsx +53 -1
- package/src/shell/app-brand.module.css +2 -2
- package/src/shell/app-brand.tsx +1 -0
- package/src/shell/app-header.module.css +18 -8
- package/src/shell/app-main.module.css +54 -3
- package/src/shell/app-main.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.module.css +16 -4
- package/src/shell/app-sidebar.module.css +4 -6
- package/src/shell/index.tsx +200 -129
- package/src/shell/mobile-nav-model.ts +1 -1
- package/src/shell/page-aside.tsx +70 -21
- package/src/shell/page-bar.module.css +10 -34
- package/src/shell/page-bar.tsx +10 -5
- package/src/shell/use-breakpoint.ts +9 -45
- package/src/shell/use-size-class.ts +31 -0
- package/src/state/persisted-or-local.ts +10 -4
- package/src/state/persisted.ts +76 -17
- package/src/styles.css +41 -0
- package/src/surfaces.ts +9 -1
- package/src/theme/controls.module.css +7 -0
- package/src/theme/ctl-theme.tsx +15 -3
- package/src/theme/floating.module.css +36 -0
- package/src/theme/floating.module.css.d.ts +3 -0
- package/src/theme/index.ts +46 -8
- package/src/theme/nav-link.module.css +7 -0
- package/src/tokens/index.ts +20 -30
- package/src/tokens/palette.ts +6 -28
- package/src/tokens/size-classes.ts +46 -0
- package/dist/shell/isomorphic-layout-effect.d.ts.map +0 -1
- /package/dist/{shell → common}/isomorphic-layout-effect.js +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ContainerClass } from '../../tokens/size-classes'
|
|
2
|
+
|
|
3
|
+
/** Aspect (frame W/H) above which the legend moves beside the ring. */
|
|
4
|
+
export const SIDE_LEGEND_ASPECT = 1.5
|
|
5
|
+
/** Aspect a side layout must fall below before it stacks again — the gap stops a legend whose own
|
|
6
|
+
* placement changes the measured height from flipping the layout back and forth. */
|
|
7
|
+
export const STACK_LEGEND_ASPECT = 1.35
|
|
8
|
+
/** Most slices kept before the rest fold into "Other" (so 7 or more slices fold). */
|
|
9
|
+
export const DONUT_MAX_SLICES = 6
|
|
10
|
+
export const OTHER_KEY = '__other'
|
|
11
|
+
/** Side layout: the ring's diameter never exceeds this share of the frame width. */
|
|
12
|
+
export const SIDE_RING_MAX_WIDTH_SHARE = 0.55
|
|
13
|
+
/** Space kept between the ring's outer edge and the plot box. */
|
|
14
|
+
export const RING_PADDING = 4
|
|
15
|
+
|
|
16
|
+
export type DonutLayout = { side: boolean }
|
|
17
|
+
|
|
18
|
+
/** Side legend only on a wide-enough frame (`micro`/`compact` always stack under the ring).
|
|
19
|
+
* `wasSide` applies hysteresis: an already-side layout stays side until W/H falls below
|
|
20
|
+
* {@link STACK_LEGEND_ASPECT}. */
|
|
21
|
+
export function resolveDonutLayout(input: {
|
|
22
|
+
frameW: number
|
|
23
|
+
frameH: number
|
|
24
|
+
containerClass: ContainerClass
|
|
25
|
+
wasSide?: boolean
|
|
26
|
+
}): DonutLayout {
|
|
27
|
+
const { frameW, frameH, containerClass, wasSide = false } = input
|
|
28
|
+
const roomy = containerClass !== 'micro' && containerClass !== 'compact'
|
|
29
|
+
if (!roomy || frameH <= 0) return { side: false }
|
|
30
|
+
const aspect = frameW / frameH
|
|
31
|
+
return { side: wasSide ? aspect >= STACK_LEGEND_ASPECT : aspect > SIDE_LEGEND_ASPECT }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Ring outer radius: never negative and never wider than the plot; the side layout also caps the
|
|
35
|
+
* diameter at {@link SIDE_RING_MAX_WIDTH_SHARE} of the frame width. */
|
|
36
|
+
export function resolveDonutRadius(input: {
|
|
37
|
+
plotW: number
|
|
38
|
+
plotH: number
|
|
39
|
+
frameW: number
|
|
40
|
+
side: boolean
|
|
41
|
+
}): number {
|
|
42
|
+
const { plotW, plotH, frameW, side } = input
|
|
43
|
+
const diameter = side
|
|
44
|
+
? Math.min(plotW, plotH, SIDE_RING_MAX_WIDTH_SHARE * frameW)
|
|
45
|
+
: Math.min(plotW, plotH)
|
|
46
|
+
return Math.max(0, diameter / 2 - RING_PADDING)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Share of `total` as a whole percent; an empty total is 0%. */
|
|
50
|
+
export function sharePercent(value: number, total: number): number {
|
|
51
|
+
return total > 0 ? Math.round((value / total) * 100) : 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A slice value a ring can draw: non-finite or negative counts as 0. */
|
|
55
|
+
const safeValue = (value: number): number => (Number.isFinite(value) && value > 0 ? value : 0)
|
|
56
|
+
|
|
57
|
+
/** The key for the synthesized "Other" bucket — {@link OTHER_KEY}, prefixed until no input key uses it. */
|
|
58
|
+
export function resolveOtherKey(keys: readonly string[]): string {
|
|
59
|
+
const taken = new Set(keys)
|
|
60
|
+
let key = OTHER_KEY
|
|
61
|
+
while (taken.has(key)) key = `_${key}`
|
|
62
|
+
return key
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Keeps the top {@link DONUT_MAX_SLICES} by value (in original order) and sums the rest into one
|
|
66
|
+
* trailing "Other" entry. Non-finite and negative values count as 0. */
|
|
67
|
+
export function foldOther<T extends { key: string; value: number }>(
|
|
68
|
+
slices: readonly T[],
|
|
69
|
+
): { kept: T[]; other: number | null } {
|
|
70
|
+
const safe = slices.map((s) => ({ ...s, value: safeValue(s.value) }))
|
|
71
|
+
if (safe.length <= DONUT_MAX_SLICES) return { kept: safe, other: null }
|
|
72
|
+
const top = new Set(
|
|
73
|
+
safe
|
|
74
|
+
.toSorted((a, b) => b.value - a.value)
|
|
75
|
+
.slice(0, DONUT_MAX_SLICES)
|
|
76
|
+
.map((s) => s.key),
|
|
77
|
+
)
|
|
78
|
+
return {
|
|
79
|
+
kept: safe.filter((s) => top.has(s.key)),
|
|
80
|
+
other: safe.reduce((sum, s) => (top.has(s.key) ? sum : sum + s.value), 0),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -124,15 +124,20 @@ export type AutoMarginInput = {
|
|
|
124
124
|
/** Degrees of counter-clockwise rotation applied to the x tick labels. Default 0. */
|
|
125
125
|
rotate?: number
|
|
126
126
|
/** Tick-label font size. Default `VX.axisFont`. A phone-tier chart passes its own smaller size
|
|
127
|
-
* (`
|
|
127
|
+
* (`chartMetrics().axisFont`) — the measured label must be the painted label. */
|
|
128
128
|
fontPx?: number
|
|
129
129
|
/**
|
|
130
130
|
* Per-side FLOORS. Default `VX.margin`. The phone tier passes its tightened set
|
|
131
|
-
* (`
|
|
131
|
+
* (`chartMetrics().margin`) so a static 44px left gutter does not spend an eighth of a 360px
|
|
132
132
|
* chart on a three-character label. The law itself does not move: a side may still only ever
|
|
133
133
|
* grow past its floor.
|
|
134
134
|
*/
|
|
135
135
|
floor?: ChartMargin
|
|
136
|
+
/** X labels anchored inward at the terminals (`AxisBottomDate anchorTerminals`): the last label
|
|
137
|
+
* no longer centres on the right edge, so the half-label right reservation is dropped. */
|
|
138
|
+
anchorTerminals?: boolean
|
|
139
|
+
/** Lines the tallest (wrapped) x label takes. Default 1. */
|
|
140
|
+
bottomLines?: number
|
|
136
141
|
/** Explicit per-side overrides — applied LAST, so an escape hatch always wins. */
|
|
137
142
|
override?: Partial<ChartMargin>
|
|
138
143
|
}
|
|
@@ -157,7 +162,16 @@ export type AutoMarginInput = {
|
|
|
157
162
|
*/
|
|
158
163
|
export function autoMargin(input: AutoMarginInput = {}): ChartMargin {
|
|
159
164
|
const fontPx = input.fontPx ?? VX.axisFont
|
|
160
|
-
const {
|
|
165
|
+
const {
|
|
166
|
+
left = [],
|
|
167
|
+
right = [],
|
|
168
|
+
bottom = [],
|
|
169
|
+
rotate = 0,
|
|
170
|
+
floor = VX.margin,
|
|
171
|
+
anchorTerminals = false,
|
|
172
|
+
bottomLines = 1,
|
|
173
|
+
override,
|
|
174
|
+
} = input
|
|
161
175
|
|
|
162
176
|
const leftWidth = maxTextWidth(left, fontPx)
|
|
163
177
|
const rightWidth = maxTextWidth(right, fontPx)
|
|
@@ -166,7 +180,8 @@ export function autoMargin(input: AutoMarginInput = {}): ChartMargin {
|
|
|
166
180
|
// Measured EXACTLY as painted: through the same `dx`/`dy` `primitives/Axes.tsx` nudges a rotated
|
|
167
181
|
// label by, and from the baseline the axis anchors it at.
|
|
168
182
|
const rotated = rotatedLabelExtents({ labelWidth: bottomWidth, fontPx, rotate })
|
|
169
|
-
const bottomExtent =
|
|
183
|
+
const bottomExtent =
|
|
184
|
+
rotate === 0 ? lineHeight(fontPx) * Math.max(bottomLines, 1) + AXIS_TICK_GAP : rotated.below
|
|
170
185
|
|
|
171
186
|
return {
|
|
172
187
|
top: override?.top ?? floor.top,
|
|
@@ -175,7 +190,11 @@ export function autoMargin(input: AutoMarginInput = {}): ChartMargin {
|
|
|
175
190
|
Math.ceil(
|
|
176
191
|
Math.max(
|
|
177
192
|
floor.right,
|
|
178
|
-
right.length > 0
|
|
193
|
+
right.length > 0
|
|
194
|
+
? rightWidth + AXIS_TICK_GAP
|
|
195
|
+
: rotate === 0 && !anchorTerminals
|
|
196
|
+
? bottomWidth / 2
|
|
197
|
+
: 0,
|
|
179
198
|
),
|
|
180
199
|
),
|
|
181
200
|
bottom: override?.bottom ?? Math.ceil(Math.max(floor.bottom, bottomExtent)),
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ContainerClass } from '../../tokens/size-classes'
|
|
2
|
+
import type { ChartSeries } from '../series'
|
|
3
|
+
import { measureText } from '../utils/measure-text'
|
|
4
|
+
|
|
5
|
+
/** Space between the plot edge and an end label's first glyph. */
|
|
6
|
+
export const END_LABEL_GAP = 6
|
|
7
|
+
|
|
8
|
+
/** End labels are for a handful of lines; past this the legend alone names them. */
|
|
9
|
+
export const MAX_END_LABELS = 3
|
|
10
|
+
|
|
11
|
+
/** Line box of one end label, as a multiple of its font size — the collision gap. */
|
|
12
|
+
export const END_LABEL_LINE_HEIGHT = 1.3
|
|
13
|
+
|
|
14
|
+
/** The gutter may take at most this share of the plot width; past it the labels are not drawn. */
|
|
15
|
+
const MAX_GUTTER_SHARE = 0.25
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Greedy collision nudging for direct labels. Each `ys` entry is a label's preferred CENTRE; the
|
|
19
|
+
* result keeps input order and holds every label at least `minGap` from its neighbours, inside
|
|
20
|
+
* `[min, max]`. Sorted by y, each label yields to the one above it, then a backward pass pulls the
|
|
21
|
+
* stack up off the bottom edge. Returns `null` when the labels cannot all fit — the caller draws
|
|
22
|
+
* none (the legend still names every series) rather than a partial or overlapping set.
|
|
23
|
+
*/
|
|
24
|
+
export function nudgeLabels(input: {
|
|
25
|
+
ys: readonly number[]
|
|
26
|
+
minGap: number
|
|
27
|
+
min: number
|
|
28
|
+
max: number
|
|
29
|
+
}): number[] | null {
|
|
30
|
+
const { ys, minGap, min, max } = input
|
|
31
|
+
if (ys.length === 0) return []
|
|
32
|
+
if (ys.some((y) => !Number.isFinite(y))) return null
|
|
33
|
+
if ((ys.length - 1) * minGap > max - min) return null
|
|
34
|
+
|
|
35
|
+
const order = ys.map((y, i) => ({ y, i })).toSorted((a, b) => a.y - b.y)
|
|
36
|
+
|
|
37
|
+
let prev = Number.NEGATIVE_INFINITY
|
|
38
|
+
const forward = order.map(({ y, i }) => {
|
|
39
|
+
prev = Math.max(y, min, prev + minGap)
|
|
40
|
+
return { i, y: prev }
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
let next = Number.POSITIVE_INFINITY
|
|
44
|
+
const back = forward.toReversed().map(({ i, y }) => {
|
|
45
|
+
next = Math.min(y, max, next - minGap)
|
|
46
|
+
return { i, y: next }
|
|
47
|
+
})
|
|
48
|
+
if (next < min) return null
|
|
49
|
+
|
|
50
|
+
return back.toSorted((a, b) => a.i - b.i).map((p) => p.y)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The right gutter the end labels need: the widest label plus the gap. `null` when it would eat
|
|
55
|
+
* more than a quarter of the plot — a label longer than that has no honest place in a gutter.
|
|
56
|
+
*/
|
|
57
|
+
export function endLabelGutter(input: {
|
|
58
|
+
labels: readonly string[]
|
|
59
|
+
fontPx: number
|
|
60
|
+
plotWidth: number
|
|
61
|
+
}): number | null {
|
|
62
|
+
const { labels, fontPx, plotWidth } = input
|
|
63
|
+
if (labels.length === 0) return null
|
|
64
|
+
const widest = labels.reduce((max, label) => Math.max(max, measureText(label, fontPx)), 0)
|
|
65
|
+
const gutter = Math.ceil(widest) + END_LABEL_GAP
|
|
66
|
+
return gutter > plotWidth * MAX_GUTTER_SHARE ? null : gutter
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** One drawn end label: the series it names and its nudged centre y. */
|
|
70
|
+
export type EndLabel<T> = { series: ChartSeries<T>; y: number }
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether and how a chart draws end-of-line labels. Eligible only on a wide container with one y
|
|
74
|
+
* axis, no consumer `margin.right`, no header legend already naming every series for free (R2C-10:
|
|
75
|
+
* end labels REPLACE the legend, they never duplicate it), and 2..{@link MAX_END_LABELS} visible
|
|
76
|
+
* series that are all lines — a single line has its own title doing that job already (the same law
|
|
77
|
+
* that already suppresses a single-entry legend, `ChartFrame.resolveLegend`'s JSDoc). Each label
|
|
78
|
+
* sits at its line's last non-null value, nudged apart; the plan is `null` — no gutter reserved,
|
|
79
|
+
* nothing drawn — when any rule fails, a series has no plottable value, the stack cannot fit, or
|
|
80
|
+
* the gutter would be too wide. The gutter is measured from the label text.
|
|
81
|
+
*/
|
|
82
|
+
export function planEndLabels<T>(input: {
|
|
83
|
+
containerClass: ContainerClass
|
|
84
|
+
hasY2: boolean
|
|
85
|
+
hasMarginRight: boolean
|
|
86
|
+
/** A `ChartCard` header slot already named every visible series — end labels would only repeat
|
|
87
|
+
* it, at the cost of a right-hand gutter the header slot cost nothing (R2C-10). */
|
|
88
|
+
hasHeaderLegend: boolean
|
|
89
|
+
visible: readonly ChartSeries<T>[]
|
|
90
|
+
data: readonly T[]
|
|
91
|
+
yScale: (value: number) => number
|
|
92
|
+
yMax: number
|
|
93
|
+
fontPx: number
|
|
94
|
+
/** Plot width before any gutter is taken. */
|
|
95
|
+
plotWidth: number
|
|
96
|
+
}): { gutter: number; labels: EndLabel<T>[] } | null {
|
|
97
|
+
const {
|
|
98
|
+
containerClass,
|
|
99
|
+
hasY2,
|
|
100
|
+
hasMarginRight,
|
|
101
|
+
hasHeaderLegend,
|
|
102
|
+
visible,
|
|
103
|
+
data,
|
|
104
|
+
yScale,
|
|
105
|
+
yMax,
|
|
106
|
+
fontPx,
|
|
107
|
+
plotWidth,
|
|
108
|
+
} = input
|
|
109
|
+
if (containerClass !== 'wide' || hasY2 || hasMarginRight || hasHeaderLegend) return null
|
|
110
|
+
if (visible.length < 2 || visible.length > MAX_END_LABELS) return null
|
|
111
|
+
if (!visible.every((s) => s.mark === 'line')) return null
|
|
112
|
+
|
|
113
|
+
const gutter = endLabelGutter({ labels: visible.map((s) => s.label), fontPx, plotWidth })
|
|
114
|
+
if (gutter === null) return null
|
|
115
|
+
|
|
116
|
+
const placed = visible.flatMap((s) => {
|
|
117
|
+
const last = data.findLast((d) => s.getValue(d) !== null)
|
|
118
|
+
if (last === undefined) return []
|
|
119
|
+
const value = s.getValue(last)
|
|
120
|
+
if (value === null) return []
|
|
121
|
+
const y = yScale(value)
|
|
122
|
+
return Number.isFinite(y) ? [{ series: s, y }] : []
|
|
123
|
+
})
|
|
124
|
+
if (placed.length !== visible.length) return null
|
|
125
|
+
|
|
126
|
+
const lineHeight = Math.ceil(fontPx * END_LABEL_LINE_HEIGHT)
|
|
127
|
+
const ys = nudgeLabels({
|
|
128
|
+
ys: placed.map((p) => p.y),
|
|
129
|
+
minGap: lineHeight,
|
|
130
|
+
min: lineHeight / 2,
|
|
131
|
+
max: yMax - lineHeight / 2,
|
|
132
|
+
})
|
|
133
|
+
if (ys === null) return null
|
|
134
|
+
return { gutter, labels: placed.map((p, i) => ({ series: p.series, y: ys[i] ?? p.y })) }
|
|
135
|
+
}
|
|
@@ -1,46 +1,109 @@
|
|
|
1
1
|
import { AxisBottom, AxisLeft, AxisRight } from '@visx/axis'
|
|
2
|
-
import type { AxisScale, TickFormatter } from '@visx/axis'
|
|
2
|
+
import type { AxisScale, TickFormatter, TickRendererProps } from '@visx/axis'
|
|
3
3
|
import { VX } from '../../tokens'
|
|
4
4
|
import { ROTATED_LABEL_OFFSET } from '../layout/auto-margin'
|
|
5
5
|
import { fmtAxisDate } from '../utils/format'
|
|
6
|
-
import {
|
|
6
|
+
import { useChartMetrics } from './chart-tier'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Tick label font — mono, per `docs/DESIGN-SPEC.md` §5 ("ticks mono 10.5px faint"). Not a `VX.*`
|
|
10
10
|
* ref (the token layer is off-limits to font-family additions here) — a plain reference to the
|
|
11
|
-
* `--basalt-font-mono` var that `styles.css` already defines.
|
|
11
|
+
* `--basalt-font-mono` var that `styles.css` already defines. Exported so `CartesianChart`'s own
|
|
12
|
+
* end-of-line labels (the same mono tick font, painted outside any `Axis*` primitive here) read it
|
|
13
|
+
* rather than keeping a second, driftable copy of the literal.
|
|
12
14
|
*/
|
|
13
|
-
const TICK_FONT_FAMILY = 'var(--basalt-font-mono)'
|
|
15
|
+
export const TICK_FONT_FAMILY = 'var(--basalt-font-mono)'
|
|
16
|
+
|
|
17
|
+
/** Horizontal padding between the tick's gridline and the near edge of its label's halo. */
|
|
18
|
+
const INSIDE_LABEL_PAD_X = 4
|
|
19
|
+
/** Gap kept between the tick's gridline and the near edge of an inside label. */
|
|
20
|
+
const INSIDE_LABEL_GAP = 3
|
|
21
|
+
/** Halo stroke width, px — wide enough to clear a mark drawn directly under the digits. */
|
|
22
|
+
const INSIDE_LABEL_HALO_WIDTH = 3
|
|
23
|
+
/**
|
|
24
|
+
* A tick within this many line-heights of the plot's top edge (y=0) flips its label BELOW the
|
|
25
|
+
* tick line instead of above it — the SVG viewBox is sized exactly to the plot height, so an
|
|
26
|
+
* "above" label at the very top tick would otherwise clip against it.
|
|
27
|
+
*/
|
|
28
|
+
const TOP_CLEARANCE_LINES = 1
|
|
29
|
+
|
|
30
|
+
/** Tick label colour. Default `VX.faint`; a dual-axis chart tints it with its series' colour. */
|
|
31
|
+
type TickColor = { tickColor?: string }
|
|
32
|
+
const DEFAULT_TICK_COLOR = VX.faint
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom tick renderer for an inside-placed y label: a `paint-order: stroke` halo (the surface
|
|
36
|
+
* colour stroked wide, under the fill) instead of an opaque background chip — the chip covered
|
|
37
|
+
* whatever mark sat under it (P1-5), a halo only pushes the mark's edge outward one stroke-width.
|
|
38
|
+
* Flips to sit BELOW its tick line near the plot's top edge instead of clipping against the SVG's
|
|
39
|
+
* own viewBox.
|
|
40
|
+
*/
|
|
41
|
+
function InsideTickLabel({ x, y, formattedValue, fontSize, fill }: TickRendererProps) {
|
|
42
|
+
if (formattedValue === undefined) return null
|
|
43
|
+
const size = typeof fontSize === 'number' ? fontSize : VX.axisFont
|
|
44
|
+
const lineHeight = Math.round(size * 1.3)
|
|
45
|
+
const nearTop = y <= lineHeight * TOP_CLEARANCE_LINES
|
|
46
|
+
const centerY = nearTop
|
|
47
|
+
? y + INSIDE_LABEL_GAP + lineHeight / 2
|
|
48
|
+
: y - INSIDE_LABEL_GAP - lineHeight / 2
|
|
49
|
+
return (
|
|
50
|
+
<text
|
|
51
|
+
x={x + INSIDE_LABEL_PAD_X}
|
|
52
|
+
y={centerY}
|
|
53
|
+
dominantBaseline="middle"
|
|
54
|
+
textAnchor="start"
|
|
55
|
+
fill={fill}
|
|
56
|
+
stroke={VX.surface.panel}
|
|
57
|
+
strokeWidth={INSIDE_LABEL_HALO_WIDTH}
|
|
58
|
+
paintOrder="stroke"
|
|
59
|
+
fontFamily={TICK_FONT_FAMILY}
|
|
60
|
+
fontSize={size}
|
|
61
|
+
>
|
|
62
|
+
{formattedValue}
|
|
63
|
+
</text>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
14
66
|
|
|
15
67
|
/** Themed left numeric axis — baked-in theme colors + font size. The tick font tracks the ambient
|
|
16
68
|
* chart tier (`docs/CHARTS-SPEC.md` §8); a caller measuring its own gutter must measure at the
|
|
17
|
-
* SAME size (`
|
|
69
|
+
* SAME size (`chartMetrics().axisFont` into `autoMargin`'s `fontPx`), or the measured label
|
|
18
70
|
* and the painted one stop being the same string's width. */
|
|
19
71
|
export function AxisLeftNumeric({
|
|
20
72
|
scale,
|
|
21
73
|
numTicks = 5,
|
|
22
74
|
tickFormat,
|
|
23
75
|
tickValues,
|
|
24
|
-
|
|
76
|
+
inside = false,
|
|
77
|
+
tickColor = DEFAULT_TICK_COLOR,
|
|
78
|
+
}: TickColor & {
|
|
25
79
|
scale: AxisScale
|
|
26
80
|
numTicks?: number
|
|
27
81
|
tickFormat?: TickFormatter<number>
|
|
28
82
|
/** Exact tick positions (e.g. a compass axis at 0/90/180/270). Overrides `numTicks`. */
|
|
29
83
|
tickValues?: readonly number[]
|
|
84
|
+
/** Labels drawn over the grid, left-aligned on a surface-coloured background chip (above their
|
|
85
|
+
* line, or below it for a tick within one line-height of the plot's top edge) — the caller
|
|
86
|
+
* reserves no gutter for them (`docs/CHARTS-SPEC.md` §8). */
|
|
87
|
+
inside?: boolean
|
|
30
88
|
}) {
|
|
31
|
-
const { axisFont } =
|
|
89
|
+
const { axisFont } = useChartMetrics()
|
|
32
90
|
return (
|
|
33
91
|
<AxisLeft
|
|
34
92
|
scale={scale}
|
|
35
93
|
numTicks={numTicks}
|
|
36
94
|
{...(tickFormat !== undefined && { tickFormat })}
|
|
37
95
|
{...(tickValues !== undefined && { tickValues: [...tickValues] })}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
96
|
+
{...(inside && { tickLength: 0, hideTicks: true, tickComponent: InsideTickLabel })}
|
|
97
|
+
tickLabelProps={
|
|
98
|
+
inside
|
|
99
|
+
? { fill: tickColor, fontSize: axisFont }
|
|
100
|
+
: {
|
|
101
|
+
fill: tickColor,
|
|
102
|
+
fontFamily: TICK_FONT_FAMILY,
|
|
103
|
+
fontSize: axisFont,
|
|
104
|
+
dx: -4,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
44
107
|
stroke={VX.surface.border}
|
|
45
108
|
tickStroke={VX.surface.border}
|
|
46
109
|
/>
|
|
@@ -54,7 +117,8 @@ export function AxisRightNumeric({
|
|
|
54
117
|
numTicks = 5,
|
|
55
118
|
tickFormat,
|
|
56
119
|
tickValues,
|
|
57
|
-
|
|
120
|
+
tickColor = DEFAULT_TICK_COLOR,
|
|
121
|
+
}: TickColor & {
|
|
58
122
|
scale: AxisScale
|
|
59
123
|
/** Left offset inside the Group (typically xMax). Required since AxisRight needs positioning. */
|
|
60
124
|
left: number
|
|
@@ -63,7 +127,7 @@ export function AxisRightNumeric({
|
|
|
63
127
|
/** Exact tick positions (e.g. a compass axis at 0/90/180/270). Overrides `numTicks`. */
|
|
64
128
|
tickValues?: readonly number[]
|
|
65
129
|
}) {
|
|
66
|
-
const { axisFont } =
|
|
130
|
+
const { axisFont } = useChartMetrics()
|
|
67
131
|
return (
|
|
68
132
|
<AxisRight
|
|
69
133
|
left={left}
|
|
@@ -72,7 +136,7 @@ export function AxisRightNumeric({
|
|
|
72
136
|
{...(tickFormat !== undefined && { tickFormat })}
|
|
73
137
|
{...(tickValues !== undefined && { tickValues: [...tickValues] })}
|
|
74
138
|
tickLabelProps={{
|
|
75
|
-
fill:
|
|
139
|
+
fill: tickColor,
|
|
76
140
|
fontFamily: TICK_FONT_FAMILY,
|
|
77
141
|
fontSize: axisFont,
|
|
78
142
|
dx: 4,
|
|
@@ -104,7 +168,7 @@ export function AxisBottomNumeric({
|
|
|
104
168
|
/** Exact tick positions (e.g. a compass axis at 0/90/180/270). Overrides `numTicks`. */
|
|
105
169
|
tickValues?: readonly number[]
|
|
106
170
|
}) {
|
|
107
|
-
const { axisFont } =
|
|
171
|
+
const { axisFont } = useChartMetrics()
|
|
108
172
|
return (
|
|
109
173
|
<AxisBottom
|
|
110
174
|
top={top}
|
|
@@ -134,6 +198,18 @@ export function AxisBottomNumeric({
|
|
|
134
198
|
*/
|
|
135
199
|
const ROTATED_OFFSET = ROTATED_LABEL_OFFSET
|
|
136
200
|
|
|
201
|
+
/** `textAnchor` of one x label: inward at the two terminals when asked, centred otherwise. */
|
|
202
|
+
export function terminalAnchor(input: {
|
|
203
|
+
anchorTerminals: boolean
|
|
204
|
+
index: number
|
|
205
|
+
count: number
|
|
206
|
+
}): 'start' | 'middle' | 'end' {
|
|
207
|
+
const { anchorTerminals, index, count } = input
|
|
208
|
+
if (!anchorTerminals || count < 2) return 'middle'
|
|
209
|
+
if (index === 0) return 'start'
|
|
210
|
+
return index === count - 1 ? 'end' : 'middle'
|
|
211
|
+
}
|
|
212
|
+
|
|
137
213
|
/** Themed bottom date axis — baked-in smartTicks + DD.MM formatting. */
|
|
138
214
|
export function AxisBottomDate({
|
|
139
215
|
scale,
|
|
@@ -141,10 +217,17 @@ export function AxisBottomDate({
|
|
|
141
217
|
tickValues,
|
|
142
218
|
tickFormat = fmtAxisDate,
|
|
143
219
|
rotate,
|
|
220
|
+
anchorTerminals = false,
|
|
221
|
+
wrapWidth,
|
|
144
222
|
}: {
|
|
145
223
|
scale: AxisScale
|
|
146
224
|
top: number
|
|
147
225
|
tickValues: string[]
|
|
226
|
+
/** First label anchored `start`, last `end`, so the terminals stay inside the plot instead of
|
|
227
|
+
* overflowing it. Ignored when rotated. */
|
|
228
|
+
anchorTerminals?: boolean
|
|
229
|
+
/** Wrap each label to this many px (SVG tspans) instead of thinning or rotating it. */
|
|
230
|
+
wrapWidth?: number
|
|
148
231
|
/**
|
|
149
232
|
* Defaults to `fmtAxisDate` (DD.MM). Override for a sub-day window, where DD.MM collapses every
|
|
150
233
|
* tick to the same label.
|
|
@@ -157,7 +240,7 @@ export function AxisBottomDate({
|
|
|
157
240
|
*/
|
|
158
241
|
rotate?: 45 | 90
|
|
159
242
|
}) {
|
|
160
|
-
const { axisFont } =
|
|
243
|
+
const { axisFont } = useChartMetrics()
|
|
161
244
|
const rotated = rotate === undefined ? undefined : ROTATED_OFFSET[rotate]
|
|
162
245
|
return (
|
|
163
246
|
<AxisBottom
|
|
@@ -165,14 +248,22 @@ export function AxisBottomDate({
|
|
|
165
248
|
scale={scale}
|
|
166
249
|
tickValues={tickValues}
|
|
167
250
|
tickFormat={tickFormat}
|
|
168
|
-
tickLabelProps={{
|
|
251
|
+
tickLabelProps={(_value, index, values) => ({
|
|
169
252
|
fill: VX.faint,
|
|
170
253
|
fontFamily: TICK_FONT_FAMILY,
|
|
171
254
|
fontSize: axisFont,
|
|
172
|
-
textAnchor:
|
|
255
|
+
textAnchor:
|
|
256
|
+
rotated !== undefined
|
|
257
|
+
? 'end'
|
|
258
|
+
: terminalAnchor({ anchorTerminals, index, count: values.length }),
|
|
173
259
|
...(rotate !== undefined && { angle: -rotate }),
|
|
174
260
|
...(rotated !== undefined && { dx: rotated.dx, dy: rotated.dy }),
|
|
175
|
-
|
|
261
|
+
// A wrapped label's default baseline anchors its LAST line, which stacks the earlier ones
|
|
262
|
+
// UPWARD into the axis line and ticks (R2C-7) — `autoMargin`'s `bottomLines` reserves room
|
|
263
|
+
// BELOW the axis, so the reservation and the paint have to agree on which line anchors.
|
|
264
|
+
...(wrapWidth !== undefined &&
|
|
265
|
+
rotated === undefined && { width: wrapWidth, verticalAnchor: 'start', dy: '-0.35em' }),
|
|
266
|
+
})}
|
|
176
267
|
stroke={VX.surface.border}
|
|
177
268
|
tickStroke={VX.surface.border}
|
|
178
269
|
/>
|