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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Group } from '@visx/group'
|
|
2
2
|
import { Pie } from '@visx/shape'
|
|
3
|
-
import { memo, useMemo
|
|
4
|
-
import type {
|
|
3
|
+
import { memo, useMemo } from 'react'
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import { useChartSize } from '../hooks/useChartSize'
|
|
6
|
+
import { useDiscreteCursor } from '../hooks/use-discrete-cursor'
|
|
7
|
+
import { useHysteresis } from '../hooks/use-hysteresis'
|
|
5
8
|
import { assertRequiredProps } from '../../common/validate'
|
|
6
9
|
import type { BasaltProps } from '../../common/props'
|
|
7
10
|
import { ChartTooltipFloat, TooltipBody, TooltipRow } from '../primitives/ChartTooltip'
|
|
@@ -9,6 +12,15 @@ import { ChartFrame } from '../primitives/ChartFrame'
|
|
|
9
12
|
import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
|
|
10
13
|
import type { ChartState } from '../primitives/ChartPending'
|
|
11
14
|
import { VX } from '../../tokens'
|
|
15
|
+
import { resolveContainerClass } from '../../tokens/size-classes'
|
|
16
|
+
import { resolveFrameHeight } from '../primitives/chart-frame-layout'
|
|
17
|
+
import {
|
|
18
|
+
foldOther,
|
|
19
|
+
resolveDonutLayout,
|
|
20
|
+
resolveDonutRadius,
|
|
21
|
+
resolveOtherKey,
|
|
22
|
+
sharePercent,
|
|
23
|
+
} from './donut-layout'
|
|
12
24
|
import type { SeriesStyle } from '../series'
|
|
13
25
|
import type { SeriesKey } from '../../register'
|
|
14
26
|
|
|
@@ -64,14 +76,22 @@ export type DonutProps<K extends string = SeriesKey> = BasaltProps & {
|
|
|
64
76
|
state?: ChartState
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
const identityLabel = (key: string): string => key
|
|
80
|
+
|
|
67
81
|
/**
|
|
68
82
|
* Radial slice-share chart with a punched-out center label. Composes `ChartFrame` for a
|
|
69
83
|
* categorical legend derived from the slices (one `SeriesStyle` per slice, `mark: 'bar'`) so the
|
|
70
|
-
* legend can never drift from what's plotted — legend-hidden slices drop out of the ring
|
|
71
|
-
*
|
|
72
|
-
* meaningless for a radial layout. Hover stays local to the pie (dimming siblings on hover) rather
|
|
84
|
+
* legend can never drift from what's plotted — legend-hidden slices drop out of the ring and the
|
|
85
|
+
* tooltip together, while every share is taken of ALL slices so the legend and the tooltip never
|
|
86
|
+
* disagree (`docs/CHARTS-SPEC.md` §5). No crosshair — meaningless for a radial layout. Hover stays local to the pie (dimming siblings on hover) rather
|
|
73
87
|
* than joining the shared cursor: a date-keyed cursor has no counterpart on a donut, and cross-kind
|
|
74
88
|
* category sync (donut ↔ bar, via a generalized key) is a distinct, deliberately deferred feature.
|
|
89
|
+
* Tap-to-pin, keyboard stepping and focus over the slice ring come from `useDiscreteCursor`
|
|
90
|
+
* (`docs/CHARTS-SPEC.md` §4, Tooltip), the discrete counterpart of `HoverOverlay`.
|
|
91
|
+
*
|
|
92
|
+
* Layout law (`docs/CHARTS-SPEC.md` §5, Legend): frame W/H > 1.5 puts the legend beside the ring
|
|
93
|
+
* with value and %, the ring capped at `min(h, 0.55w)`; `micro`/`compact` stack the full legend
|
|
94
|
+
* under the ring, never rolled up. 7 or more slices fold the smallest into a neutral "Other".
|
|
75
95
|
*/
|
|
76
96
|
function DonutInner<K extends string = SeriesKey>(props: DonutProps<K>) {
|
|
77
97
|
// F-ERR-1: name the component and the prop. Without this a missing accessor surfaces
|
|
@@ -82,7 +102,8 @@ function DonutInner<K extends string = SeriesKey>(props: DonutProps<K>) {
|
|
|
82
102
|
data,
|
|
83
103
|
height,
|
|
84
104
|
colorForKey,
|
|
85
|
-
|
|
105
|
+
formatValue,
|
|
106
|
+
seriesLabel = identityLabel,
|
|
86
107
|
ariaLabel,
|
|
87
108
|
isPending,
|
|
88
109
|
state,
|
|
@@ -90,44 +111,118 @@ function DonutInner<K extends string = SeriesKey>(props: DonutProps<K>) {
|
|
|
90
111
|
style,
|
|
91
112
|
} = props
|
|
92
113
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
// The legend side depends on the frame's own measured box, which only exists inside
|
|
115
|
+
// `ChartFrame` — so measure the wrapper (same width) one level up. A stated height is used as
|
|
116
|
+
// is; the measured one moves with the legend's placement, so an unstated height gets hysteresis.
|
|
117
|
+
const { ref, width: frameW, height: measuredH } = useChartSize()
|
|
118
|
+
const containerClass = resolveContainerClass(frameW)
|
|
119
|
+
const frameH = height === undefined ? measuredH : resolveFrameHeight(height, containerClass)
|
|
120
|
+
const side = useHysteresis(
|
|
121
|
+
(wasSide) =>
|
|
122
|
+
resolveDonutLayout({
|
|
123
|
+
frameW,
|
|
124
|
+
frameH,
|
|
125
|
+
containerClass,
|
|
126
|
+
wasSide: height === undefined && wasSide,
|
|
127
|
+
}).side,
|
|
128
|
+
false,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
const slices = useMemo(() => {
|
|
132
|
+
const { kept, other } = foldOther(data)
|
|
133
|
+
const out: Slice[] = kept.map((d) => ({
|
|
134
|
+
key: d.key,
|
|
135
|
+
label: seriesLabel(d.key),
|
|
136
|
+
color: colorForKey(d.key),
|
|
137
|
+
value: d.value,
|
|
138
|
+
}))
|
|
139
|
+
if (other !== null)
|
|
140
|
+
out.push({
|
|
141
|
+
key: resolveOtherKey(kept.map((d) => d.key)),
|
|
142
|
+
label: 'Other',
|
|
143
|
+
color: VX.neutral,
|
|
144
|
+
value: other,
|
|
145
|
+
})
|
|
146
|
+
return out
|
|
147
|
+
}, [data, seriesLabel, colorForKey])
|
|
148
|
+
|
|
149
|
+
// One denominator for the side legend and the tooltip: every slice, hidden or not.
|
|
150
|
+
const total = useMemo(() => slices.reduce((sum, d) => sum + d.value, 0), [slices])
|
|
151
|
+
const series = useMemo<SeriesStyle[]>(
|
|
152
|
+
() =>
|
|
153
|
+
slices.map((d) => ({
|
|
154
|
+
key: d.key,
|
|
155
|
+
label: side
|
|
156
|
+
? `${d.label} · ${formatValue(d.value)} · ${sharePercent(d.value, total)}%`
|
|
157
|
+
: d.label,
|
|
158
|
+
color: d.color,
|
|
159
|
+
mark: 'bar',
|
|
160
|
+
})),
|
|
161
|
+
[slices, side, formatValue, total],
|
|
162
|
+
)
|
|
99
163
|
|
|
100
164
|
return (
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
{...(height !== undefined && { height })}
|
|
104
|
-
{...(ariaLabel !== undefined && { ariaLabel })}
|
|
105
|
-
{...(isPending !== undefined && { isPending })}
|
|
106
|
-
{...(state !== undefined && { state })}
|
|
165
|
+
<div
|
|
166
|
+
ref={ref}
|
|
107
167
|
{...(className !== undefined && { className })}
|
|
108
|
-
{
|
|
168
|
+
style={{ display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: 0, ...style }}
|
|
109
169
|
>
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
<ChartFrame
|
|
171
|
+
series={series}
|
|
172
|
+
// Never roll the legend up: the full list is the donut's only key. Side legends read as a
|
|
173
|
+
// column. `headerSlot: false` (P1-4) keeps it under/beside the ring even inside a
|
|
174
|
+
// `ChartCard` with a header slot free — a Donut's legend IS its key, so it must never
|
|
175
|
+
// shrink to unlabelled header dots.
|
|
176
|
+
legend={{ placement: side ? 'right' : 'bottom', maxRows: series.length, headerSlot: false }}
|
|
177
|
+
{...(height !== undefined && { height })}
|
|
178
|
+
{...(ariaLabel !== undefined && { ariaLabel })}
|
|
179
|
+
{...(isPending !== undefined && { isPending })}
|
|
180
|
+
{...(state !== undefined && { state })}
|
|
181
|
+
>
|
|
182
|
+
{(plot) => (
|
|
183
|
+
<DonutPlot
|
|
184
|
+
slices={slices}
|
|
185
|
+
total={total}
|
|
186
|
+
side={side}
|
|
187
|
+
frameW={frameW}
|
|
188
|
+
plot={plot}
|
|
189
|
+
formatValue={formatValue}
|
|
190
|
+
{...(props.centerLabel !== undefined && { centerLabel: props.centerLabel })}
|
|
191
|
+
{...(props.centerSubLabel !== undefined && { centerSubLabel: props.centerSubLabel })}
|
|
192
|
+
{...(props.centerContent !== undefined && { centerContent: props.centerContent })}
|
|
193
|
+
{...(props.innerRatio !== undefined && { innerRatio: props.innerRatio })}
|
|
194
|
+
{...(props.padAngle !== undefined && { padAngle: props.padAngle })}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
</ChartFrame>
|
|
198
|
+
</div>
|
|
112
199
|
)
|
|
113
200
|
}
|
|
114
201
|
|
|
115
|
-
type
|
|
202
|
+
type Slice = { key: string; label: string; color: string; value: number }
|
|
203
|
+
|
|
204
|
+
type DonutPlotProps = Pick<
|
|
205
|
+
DonutProps,
|
|
206
|
+
'formatValue' | 'centerLabel' | 'centerSubLabel' | 'centerContent' | 'innerRatio' | 'padAngle'
|
|
207
|
+
> & {
|
|
116
208
|
plot: { width: number; height: number; hidden: ReadonlySet<string> }
|
|
209
|
+
slices: Slice[]
|
|
210
|
+
/** Sum of ALL slices (hidden included) — the one denominator for every share. */
|
|
211
|
+
total: number
|
|
212
|
+
side: boolean
|
|
213
|
+
frameW: number
|
|
117
214
|
}
|
|
118
215
|
|
|
119
|
-
/** A hovered slice plus the viewport anchor `ChartTooltipFloat` positions against. */
|
|
120
|
-
type DonutTip<K extends string> = { key: K; value: number; anchor: { x: number; y: number } }
|
|
121
|
-
|
|
122
216
|
/** The measured plot — split from {@link DonutInner} so it only draws once `ChartFrame` has
|
|
123
217
|
* resolved a non-empty plot rect (radius/center depend on the measured size). */
|
|
124
|
-
function DonutPlot
|
|
218
|
+
function DonutPlot(props: DonutPlotProps) {
|
|
125
219
|
const {
|
|
126
|
-
|
|
220
|
+
slices,
|
|
221
|
+
total,
|
|
127
222
|
plot,
|
|
128
|
-
|
|
223
|
+
side,
|
|
224
|
+
frameW,
|
|
129
225
|
formatValue,
|
|
130
|
-
seriesLabel = (k) => k,
|
|
131
226
|
centerLabel,
|
|
132
227
|
centerSubLabel,
|
|
133
228
|
centerContent,
|
|
@@ -136,28 +231,28 @@ function DonutPlot<K extends string = SeriesKey>(props: DonutPlotProps<K>) {
|
|
|
136
231
|
} = props
|
|
137
232
|
const { width, height, hidden } = plot
|
|
138
233
|
|
|
139
|
-
const
|
|
140
|
-
const hoveredKey = tip?.key ?? null
|
|
234
|
+
const visibleData = useMemo(() => slices.filter((d) => !hidden.has(d.key)), [slices, hidden])
|
|
141
235
|
|
|
142
|
-
|
|
236
|
+
// One ring, one shared keyboard host: `useDiscreteCursor` gives arrow-key stepping through the
|
|
237
|
+
// VISIBLE slices (a hidden legend entry is not a reachable target either), plus tap-to-pin on
|
|
238
|
+
// touch. Left/Right and Up/Down are equivalent (no `columns` — a ring has no 2D shape).
|
|
239
|
+
const cursor = useDiscreteCursor<Slice>({
|
|
240
|
+
targets: visibleData,
|
|
241
|
+
getKey: (d) => d.key,
|
|
242
|
+
ariaLabel: 'Donut slices — use arrow keys to select, Escape to dismiss',
|
|
243
|
+
})
|
|
244
|
+
const activeKey = cursor.tip?.target.key ?? null
|
|
143
245
|
|
|
144
|
-
const radius =
|
|
246
|
+
const radius = resolveDonutRadius({ plotW: width, plotH: height, frameW, side })
|
|
145
247
|
const innerRadius = radius * innerRatio
|
|
146
248
|
const centerX = width / 2
|
|
147
249
|
const centerY = height / 2
|
|
148
250
|
|
|
149
|
-
const total = useMemo(() => visibleData.reduce((sum, d) => sum + d.value, 0), [visibleData])
|
|
150
|
-
|
|
151
|
-
const show = (d: DonutDatum<K>, event: PointerEvent<SVGGElement>) => {
|
|
152
|
-
setTip({ key: d.key, value: d.value, anchor: { x: event.clientX, y: event.clientY } })
|
|
153
|
-
}
|
|
154
|
-
const hide = () => setTip(null)
|
|
155
|
-
|
|
156
251
|
return (
|
|
157
252
|
<div style={{ position: 'relative' }}>
|
|
158
|
-
<svg width={width} height={height}>
|
|
253
|
+
<svg width={width} height={height} {...cursor.hostProps}>
|
|
159
254
|
<Group left={centerX} top={centerY}>
|
|
160
|
-
<Pie<
|
|
255
|
+
<Pie<Slice>
|
|
161
256
|
data={visibleData}
|
|
162
257
|
pieValue={(d) => d.value}
|
|
163
258
|
pieSortValues={() => 0}
|
|
@@ -172,26 +267,22 @@ function DonutPlot<K extends string = SeriesKey>(props: DonutPlotProps<K>) {
|
|
|
172
267
|
return (
|
|
173
268
|
<g
|
|
174
269
|
key={key}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}}
|
|
184
|
-
onPointerCancel={() => {
|
|
185
|
-
hide()
|
|
186
|
-
}}
|
|
270
|
+
id={cursor.optionId(arc.data)}
|
|
271
|
+
// An SVG `<g>` has no `<option>` counterpart — `useDiscreteCursor`'s
|
|
272
|
+
// listbox/option pair (see its JSDoc) is a role over an arbitrary shape.
|
|
273
|
+
// oxlint-disable-next-line jsx-a11y/prefer-tag-over-role
|
|
274
|
+
role="option"
|
|
275
|
+
aria-selected={activeKey === key}
|
|
276
|
+
aria-label={`${arc.data.label}: ${formatValue(arc.data.value)}, ${sharePercent(arc.data.value, total)}%`}
|
|
277
|
+
{...cursor.pointerProps(arc.data)}
|
|
187
278
|
style={{ cursor: 'pointer' }}
|
|
188
279
|
>
|
|
189
280
|
<path
|
|
190
281
|
d={pie.path(arc) || ''}
|
|
191
|
-
fill={
|
|
282
|
+
fill={arc.data.color}
|
|
192
283
|
stroke={VX.surface.panel}
|
|
193
284
|
strokeWidth={1.5}
|
|
194
|
-
opacity={
|
|
285
|
+
opacity={activeKey === null || activeKey === key ? 1 : 0.4}
|
|
195
286
|
/>
|
|
196
287
|
</g>
|
|
197
288
|
)
|
|
@@ -244,19 +335,19 @@ function DonutPlot<K extends string = SeriesKey>(props: DonutPlotProps<K>) {
|
|
|
244
335
|
</div>
|
|
245
336
|
)}
|
|
246
337
|
|
|
247
|
-
{tip !== null && (
|
|
248
|
-
<ChartTooltipFloat anchor={tip.anchor}>
|
|
338
|
+
{cursor.tip !== null && (
|
|
339
|
+
<ChartTooltipFloat anchor={cursor.tip.anchor}>
|
|
249
340
|
<TooltipBody>
|
|
250
341
|
<TooltipRow
|
|
251
|
-
color={
|
|
252
|
-
label={
|
|
253
|
-
value={formatValue(tip.value)}
|
|
342
|
+
color={cursor.tip.target.color}
|
|
343
|
+
label={cursor.tip.target.label}
|
|
344
|
+
value={formatValue(cursor.tip.target.value)}
|
|
254
345
|
shape="bar"
|
|
255
346
|
/>
|
|
256
347
|
<TooltipRow
|
|
257
348
|
color={VX.grid}
|
|
258
349
|
label="Share"
|
|
259
|
-
value={`${
|
|
350
|
+
value={`${sharePercent(cursor.tip.target.value, total)}%`}
|
|
260
351
|
shape="bar"
|
|
261
352
|
/>
|
|
262
353
|
</TooltipBody>
|
|
@@ -336,12 +336,16 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
336
336
|
[bottomDomain, bottomH, useNiceBottom],
|
|
337
337
|
)
|
|
338
338
|
|
|
339
|
+
// ONE `<svg>` wraps both panes below, so it doubles as the tap-outside boundary
|
|
340
|
+
// (`docs/CHARTS-SPEC.md` §4, Tooltip) for the single cursor they share.
|
|
341
|
+
const svgRef = useRef<SVGSVGElement>(null)
|
|
339
342
|
const cursor = useChartCursor<T>({
|
|
340
343
|
data,
|
|
341
344
|
chartId,
|
|
342
345
|
getKey: getX,
|
|
343
346
|
xScale,
|
|
344
347
|
marginLeft: margin.left,
|
|
348
|
+
boundaryRef: svgRef,
|
|
345
349
|
...(cursorResolution !== undefined && { resolution: cursorResolution }),
|
|
346
350
|
})
|
|
347
351
|
|
|
@@ -477,7 +481,6 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
477
481
|
[visibleSeries, lineValid, xScale, topYScale, getX],
|
|
478
482
|
)
|
|
479
483
|
|
|
480
|
-
const svgRef = useRef<SVGSVGElement>(null)
|
|
481
484
|
const point = cursor.point
|
|
482
485
|
const sx = point !== null ? (xScale(getX(point)) ?? 0) : 0
|
|
483
486
|
const syncedBar = point !== null ? getBar(point) : null
|
|
@@ -485,10 +488,12 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
485
488
|
// Same seam as `CartesianChart`: this chart renders its tooltip as the cursor SOURCE (always) or,
|
|
486
489
|
// opted in via `onFollow`, as a FOLLOWER. A follower has no pointer under it, so it is always
|
|
487
490
|
// positioned via the anchored (crosshair) path — `cursor.anchor` stays null on a chart nobody is
|
|
488
|
-
// hovering, so following it here would render nothing.
|
|
491
|
+
// hovering, so following it here would render nothing. `DualPanel` has no `follow` prop to
|
|
492
|
+
// override the default, so touch (spec §5) anchors unconditionally, same as an unset `follow`
|
|
493
|
+
// elsewhere.
|
|
489
494
|
const isFollowerRender = !cursor.isSource && onFollow === true
|
|
490
495
|
const showTooltip = point !== null && (cursor.isSource || isFollowerRender)
|
|
491
|
-
const anchorX = isFollowerRender && point !== null ? sx : null
|
|
496
|
+
const anchorX = (isFollowerRender || cursor.isTouch) && point !== null ? sx : null
|
|
492
497
|
const svgRect = anchorX === null ? undefined : svgRef.current?.getBoundingClientRect()
|
|
493
498
|
const tooltipAnchor =
|
|
494
499
|
svgRect === undefined || anchorX === null
|
|
@@ -497,7 +502,13 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
497
502
|
|
|
498
503
|
return (
|
|
499
504
|
<>
|
|
500
|
-
<svg
|
|
505
|
+
<svg
|
|
506
|
+
ref={svgRef}
|
|
507
|
+
width={plot.width}
|
|
508
|
+
height={plot.height}
|
|
509
|
+
// `touch-action` on the plot `<svg>`: Chrome ignores it on the overlay `<rect>` (`R2C-2`).
|
|
510
|
+
style={{ touchAction: 'pan-y' }}
|
|
511
|
+
>
|
|
501
512
|
{/* Top pane: line series + fill-between + zones + ref lines. */}
|
|
502
513
|
<Group left={margin.left} top={margin.top}>
|
|
503
514
|
<GridRows scale={topYScale} width={xMax} stroke={VX.grid} numTicks={4} />
|
|
@@ -585,6 +596,8 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
585
596
|
width={xMax}
|
|
586
597
|
height={topH + PANE_GAP}
|
|
587
598
|
onMove={cursor.onPointerMove}
|
|
599
|
+
onDown={cursor.onPointerDown}
|
|
600
|
+
onUp={cursor.onPointerUp}
|
|
588
601
|
onLeave={cursor.onPointerLeave}
|
|
589
602
|
onKeyDown={cursor.onKeyDown}
|
|
590
603
|
onBlur={cursor.onBlur}
|
|
@@ -648,6 +661,8 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
648
661
|
width={xMax}
|
|
649
662
|
height={bottomH + margin.bottom}
|
|
650
663
|
onMove={cursor.onPointerMove}
|
|
664
|
+
onDown={cursor.onPointerDown}
|
|
665
|
+
onUp={cursor.onPointerUp}
|
|
651
666
|
onLeave={cursor.onPointerLeave}
|
|
652
667
|
/>
|
|
653
668
|
</Group>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Group } from '@visx/group'
|
|
2
|
-
import type {
|
|
3
|
-
import { memo, useMemo
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { memo, useMemo } from 'react'
|
|
4
4
|
import { assertRequiredProps } from '../../common/validate'
|
|
5
5
|
import type { BasaltProps } from '../../common/props'
|
|
6
6
|
import {
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
} from '../primitives/ChartTooltip'
|
|
12
12
|
import { ChartFrame } from '../primitives/ChartFrame'
|
|
13
13
|
import type { ResponsiveChartHeight } from '../primitives/ChartFrame'
|
|
14
|
-
import {
|
|
14
|
+
import { useChartMetrics } from '../primitives/chart-tier'
|
|
15
|
+
import { useDiscreteCursor } from '../hooks/use-discrete-cursor'
|
|
15
16
|
import type { ChartState } from '../primitives/ChartPending'
|
|
16
17
|
import { maxTextWidth } from '../utils/measure-text'
|
|
17
18
|
import { thinLabels, xLabelPxFor } from '../utils/ticks'
|
|
@@ -20,8 +21,9 @@ import { VX, alpha } from '../../tokens'
|
|
|
20
21
|
/** A single resolved heatmap cell — the unit the tooltip and hover operate on. */
|
|
21
22
|
type HeatmapCell = { row: string; col: string; value: number }
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
24
|
+
/** One grid position, data or not — `useDiscreteCursor`'s target unit. `value` is `undefined` for
|
|
25
|
+
* an (row, col) pair with no data, which stays a keyboard stop but never shows a tooltip. */
|
|
26
|
+
type CellTarget = { row: string; col: string; value: number | undefined }
|
|
25
27
|
|
|
26
28
|
export type HeatmapProps<T> = BasaltProps & {
|
|
27
29
|
data: T[]
|
|
@@ -115,6 +117,13 @@ const LEGEND_LABEL_H = 16
|
|
|
115
117
|
* right gutter reserves half the widest column label, so no two painted labels overlap and the
|
|
116
118
|
* last one cannot clip.
|
|
117
119
|
*
|
|
120
|
+
* Tap-to-pin, keyboard stepping and focus over the grid come from `useDiscreteCursor`
|
|
121
|
+
* (`docs/CHARTS-SPEC.md` §4, Tooltip), the discrete counterpart of `HoverOverlay`. Its targets are
|
|
122
|
+
* the FULL row-major grid (data cells and empty ones alike) so Up/Down can jump one row via a fixed
|
|
123
|
+
* `columns` stride; only a cell WITH data renders a tooltip or takes pointer input, matching today's
|
|
124
|
+
* hover behavior — an empty cell is still a keyboard stop (so the grid reads as one rectangle), it
|
|
125
|
+
* just carries no readout.
|
|
126
|
+
*
|
|
118
127
|
* Composes `ChartFrame` purely for measuring (`height`/`aspectRatio`/`fill`, the same three
|
|
119
128
|
* sizing modes every other kind exposes) — `legend={false}` and an empty `series` opt out of
|
|
120
129
|
* `ChartFrame`'s own derived legend, since Heatmap already ships its own gradient strip.
|
|
@@ -177,9 +186,7 @@ function HeatmapPlot<T>(props: HeatmapPlotProps<T>) {
|
|
|
177
186
|
// The row/column labels and the gradient legend's endpoints read like axis ticks, so they take
|
|
178
187
|
// the tier's tick font for the same reason `Axes.tsx` does — a 360px heatmap that painted its
|
|
179
188
|
// categories at the desktop size would be the one chart still ignoring §8.
|
|
180
|
-
const { axisFont } =
|
|
181
|
-
|
|
182
|
-
const [tip, setTip] = useState<HeatmapTip | null>(null)
|
|
189
|
+
const { axisFont } = useChartMetrics()
|
|
183
190
|
|
|
184
191
|
const rows = useMemo(() => rowsProp ?? firstSeen(data, getRow), [rowsProp, data, getRow])
|
|
185
192
|
const cols = useMemo(() => colsProp ?? firstSeen(data, getCol), [colsProp, data, getCol])
|
|
@@ -196,6 +203,36 @@ function HeatmapPlot<T>(props: HeatmapPlotProps<T>) {
|
|
|
196
203
|
return { lookup: map, max: m }
|
|
197
204
|
}, [data, getRow, getCol, getValue])
|
|
198
205
|
|
|
206
|
+
// The FULL row-major grid (gaps included) so `columns: cols.length` gives `useDiscreteCursor` a
|
|
207
|
+
// real row stride for Up/Down — a targets list built only from cells WITH data would shift that
|
|
208
|
+
// stride the moment a row has a gap.
|
|
209
|
+
const cellTargets = useMemo<CellTarget[]>(() => {
|
|
210
|
+
const out: CellTarget[] = []
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
for (const col of cols) out.push({ row, col, value: lookup.get(cellKey(row, col)) })
|
|
213
|
+
}
|
|
214
|
+
return out
|
|
215
|
+
}, [rows, cols, lookup])
|
|
216
|
+
const cursor = useDiscreteCursor<CellTarget>({
|
|
217
|
+
targets: cellTargets,
|
|
218
|
+
getKey: (c) => cellKey(c.row, c.col),
|
|
219
|
+
columns: cols.length,
|
|
220
|
+
ariaLabel: 'Heatmap cells — use arrow keys to navigate, Escape to dismiss',
|
|
221
|
+
})
|
|
222
|
+
// Resolved once here (not inline in the JSX below) so `value` narrows to `number` — a keyboard
|
|
223
|
+
// stop on an empty cell has a `tip` but no readout to show.
|
|
224
|
+
const activeTip =
|
|
225
|
+
cursor.tip !== null && cursor.tip.target.value !== undefined
|
|
226
|
+
? {
|
|
227
|
+
anchor: cursor.tip.anchor,
|
|
228
|
+
cell: {
|
|
229
|
+
row: cursor.tip.target.row,
|
|
230
|
+
col: cursor.tip.target.col,
|
|
231
|
+
value: cursor.tip.target.value,
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
: null
|
|
235
|
+
|
|
199
236
|
const rowLabels = useMemo(() => rows.map(rowLabel), [rows, rowLabel])
|
|
200
237
|
const colLabels = useMemo(() => cols.map(colLabel), [cols, colLabel])
|
|
201
238
|
|
|
@@ -223,23 +260,27 @@ function HeatmapPlot<T>(props: HeatmapPlotProps<T>) {
|
|
|
223
260
|
[rowLabels, cellH, axisFont],
|
|
224
261
|
)
|
|
225
262
|
|
|
226
|
-
const show = (row: string, col: string, value: number, event: PointerEvent<SVGRectElement>) => {
|
|
227
|
-
setTip({ row, col, value, anchor: { x: event.clientX, y: event.clientY } })
|
|
228
|
-
}
|
|
229
|
-
const hide = () => setTip(null)
|
|
230
|
-
|
|
231
263
|
return (
|
|
232
264
|
<div style={{ position: 'relative' }}>
|
|
233
|
-
<svg width={width} height={height}>
|
|
265
|
+
<svg width={width} height={height} {...cursor.hostProps}>
|
|
234
266
|
<Group left={PAD_LEFT} top={PAD_TOP}>
|
|
235
267
|
{rows.flatMap((row, ri) =>
|
|
236
268
|
cols.map((col, ci) => {
|
|
237
269
|
const value = lookup.get(cellKey(row, col))
|
|
238
270
|
const has = value !== undefined
|
|
239
271
|
const cellFill = has ? alpha(color, intensity(value, max)) : alpha(VX.neutral, 0.04)
|
|
272
|
+
const target: CellTarget = { row, col, value }
|
|
273
|
+
const isActive = cursor.tip?.target.row === row && cursor.tip?.target.col === col
|
|
240
274
|
return (
|
|
241
275
|
<rect
|
|
242
276
|
key={cellKey(row, col)}
|
|
277
|
+
id={cursor.optionId(target)}
|
|
278
|
+
// An SVG `<rect>` has no `<option>` counterpart — `useDiscreteCursor`'s
|
|
279
|
+
// listbox/option pair (see its JSDoc) is a role over an arbitrary shape.
|
|
280
|
+
// oxlint-disable-next-line jsx-a11y/prefer-tag-over-role
|
|
281
|
+
role="option"
|
|
282
|
+
aria-selected={isActive}
|
|
283
|
+
aria-label={`${rowLabels[ri]}: ${colLabels[ci]}: ${value !== undefined ? formatValue(value) : 'no data'}`}
|
|
243
284
|
x={ci * cellW + cellGap / 2}
|
|
244
285
|
y={ri * cellH + cellGap / 2}
|
|
245
286
|
width={Math.max(0, cellW - cellGap)}
|
|
@@ -247,9 +288,7 @@ function HeatmapPlot<T>(props: HeatmapPlotProps<T>) {
|
|
|
247
288
|
rx={cellRadius}
|
|
248
289
|
fill={cellFill}
|
|
249
290
|
style={{ cursor: has ? 'pointer' : 'default' }}
|
|
250
|
-
|
|
251
|
-
onPointerLeave={hide}
|
|
252
|
-
onPointerCancel={hide}
|
|
291
|
+
{...(has ? cursor.pointerProps(target) : {})}
|
|
253
292
|
/>
|
|
254
293
|
)
|
|
255
294
|
}),
|
|
@@ -328,17 +367,17 @@ function HeatmapPlot<T>(props: HeatmapPlotProps<T>) {
|
|
|
328
367
|
)}
|
|
329
368
|
</svg>
|
|
330
369
|
|
|
331
|
-
{
|
|
332
|
-
<ChartTooltipFloat anchor={
|
|
333
|
-
<TooltipHeader date={rowLabel(
|
|
370
|
+
{activeTip !== null && (
|
|
371
|
+
<ChartTooltipFloat anchor={activeTip.anchor}>
|
|
372
|
+
<TooltipHeader date={rowLabel(activeTip.cell.row)} label={colLabel(activeTip.cell.col)} />
|
|
334
373
|
<TooltipBody>
|
|
335
374
|
<TooltipRow
|
|
336
375
|
color={alpha(color, 0.9)}
|
|
337
376
|
shape="bar"
|
|
338
377
|
label="Value"
|
|
339
|
-
value={formatValue(
|
|
378
|
+
value={formatValue(activeTip.cell.value)}
|
|
340
379
|
/>
|
|
341
|
-
{renderTooltip?.(
|
|
380
|
+
{renderTooltip?.(activeTip.cell)}
|
|
342
381
|
</TooltipBody>
|
|
343
382
|
</ChartTooltipFloat>
|
|
344
383
|
)}
|
|
@@ -362,7 +362,13 @@ function MirroredBarsPlot<T>(props: MirroredBarsPlotProps<T>) {
|
|
|
362
362
|
|
|
363
363
|
return (
|
|
364
364
|
<>
|
|
365
|
-
<svg
|
|
365
|
+
<svg
|
|
366
|
+
ref={band.svgRef}
|
|
367
|
+
width={plot.width}
|
|
368
|
+
height={plot.height}
|
|
369
|
+
// `touch-action` on the plot `<svg>`: Chrome ignores it on the overlay `<rect>` (`R2C-2`).
|
|
370
|
+
style={{ touchAction: 'pan-y' }}
|
|
371
|
+
>
|
|
366
372
|
<defs>
|
|
367
373
|
<HatchPattern
|
|
368
374
|
id={hatchId}
|
|
@@ -383,19 +389,21 @@ function MirroredBarsPlot<T>(props: MirroredBarsPlotProps<T>) {
|
|
|
383
389
|
|
|
384
390
|
{/* One axis per pane, each in its own scale's units — a single shared axis would be
|
|
385
391
|
wrong for at least one of them whenever the two domains differ, which is always. */}
|
|
386
|
-
{upVisible && (
|
|
392
|
+
{upVisible && band.yPlacement !== 'none' && (
|
|
387
393
|
<AxisLeftNumeric
|
|
388
394
|
scale={upAxisScale}
|
|
389
395
|
numTicks={upTicks}
|
|
390
396
|
tickFormat={(v) => up.format(Number(v))}
|
|
397
|
+
inside={band.yPlacement === 'inside'}
|
|
391
398
|
/>
|
|
392
399
|
)}
|
|
393
|
-
{downVisible && (
|
|
400
|
+
{downVisible && band.yPlacement !== 'none' && (
|
|
394
401
|
<Group top={baseline}>
|
|
395
402
|
<AxisLeftNumeric
|
|
396
403
|
scale={downScale}
|
|
397
404
|
numTicks={downTicks}
|
|
398
405
|
tickFormat={(v) => down.format(Number(v))}
|
|
406
|
+
inside={band.yPlacement === 'inside'}
|
|
399
407
|
/>
|
|
400
408
|
</Group>
|
|
401
409
|
)}
|
|
@@ -416,12 +424,16 @@ function MirroredBarsPlot<T>(props: MirroredBarsPlotProps<T>) {
|
|
|
416
424
|
scale={scale}
|
|
417
425
|
tickValues={band.tickValues}
|
|
418
426
|
tickFormat={(v) => formatX(String(v))}
|
|
427
|
+
anchorTerminals={band.xAnchorTerminals}
|
|
428
|
+
{...(band.xWrapWidth !== undefined && { wrapWidth: band.xWrapWidth })}
|
|
419
429
|
/>
|
|
420
430
|
|
|
421
431
|
<HoverOverlay
|
|
422
432
|
width={plotWidth}
|
|
423
433
|
height={barBand}
|
|
424
434
|
onMove={cursor.onPointerMove}
|
|
435
|
+
onDown={cursor.onPointerDown}
|
|
436
|
+
onUp={cursor.onPointerUp}
|
|
425
437
|
onLeave={cursor.onPointerLeave}
|
|
426
438
|
onKeyDown={cursor.onKeyDown}
|
|
427
439
|
onBlur={cursor.onBlur}
|