basalt-ui 1.13.0 → 1.15.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 +15 -0
- package/README.md +38 -2
- package/agent/rules/basalt-charts.md +260 -108
- package/agent/rules/basalt-mantine.md +4 -2
- package/agent/skills/basalt-charts/SKILL.md +114 -64
- package/agent/skills/basalt-design/SKILL.md +4 -3
- package/configs/oxlint-plugin.js +152 -0
- package/configs/oxlint-plugin.test.ts +107 -0
- package/configs/oxlint.json +2 -0
- package/dist/charts/cursor/resolve.d.ts +33 -0
- package/dist/charts/cursor/resolve.d.ts.map +1 -0
- package/dist/charts/cursor/resolve.js +61 -0
- package/dist/charts/cursor/resolve.test.d.ts +2 -0
- package/dist/charts/cursor/resolve.test.d.ts.map +1 -0
- package/dist/charts/cursor/scope.d.ts +17 -0
- package/dist/charts/cursor/scope.d.ts.map +1 -0
- package/dist/charts/cursor/scope.js +20 -0
- package/dist/charts/cursor/store.d.ts +31 -0
- package/dist/charts/cursor/store.d.ts.map +1 -0
- package/dist/charts/cursor/store.js +25 -0
- package/dist/charts/cursor/store.test.d.ts +2 -0
- package/dist/charts/cursor/store.test.d.ts.map +1 -0
- package/dist/charts/hooks/useChartCursor.d.ts +39 -0
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -0
- package/dist/charts/hooks/useChartCursor.js +104 -0
- package/dist/charts/hooks/useChartSize.d.ts +8 -4
- package/dist/charts/hooks/useChartSize.d.ts.map +1 -1
- package/dist/charts/index.d.ts +16 -13
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +38 -19
- package/dist/charts/kinds/Bars.d.ts +25 -47
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +230 -390
- package/dist/charts/kinds/Donut.d.ts +5 -4
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.js +17 -14
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +136 -90
- package/dist/charts/kinds/Heatmap.d.ts +15 -11
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.js +38 -22
- package/dist/charts/kinds/MultiLine.d.ts +27 -35
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +114 -237
- package/dist/charts/kinds/StackedArea.d.ts +21 -10
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +74 -132
- package/dist/charts/kinds/ZonedLine.d.ts +24 -40
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +103 -184
- package/dist/charts/layout/auto-margin.d.ts +60 -0
- package/dist/charts/layout/auto-margin.d.ts.map +1 -0
- package/dist/charts/layout/auto-margin.js +37 -0
- package/dist/charts/layout/auto-margin.test.d.ts +2 -0
- package/dist/charts/layout/auto-margin.test.d.ts.map +1 -0
- package/dist/charts/primitives/CartesianChart.d.ts +143 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -0
- package/dist/charts/primitives/CartesianChart.js +349 -0
- package/dist/charts/primitives/CartesianChart.test.d.ts +2 -0
- package/dist/charts/primitives/CartesianChart.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartFrame.d.ts +16 -5
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +17 -2
- package/dist/charts/primitives/ChartLegend.d.ts +12 -1
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +21 -3
- package/dist/charts/primitives/ChartLegend.test.d.ts +2 -0
- package/dist/charts/primitives/ChartLegend.test.d.ts.map +1 -0
- package/dist/charts/primitives/ChartTooltip.d.ts +16 -16
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +45 -19
- package/dist/charts/primitives/HoverOverlay.d.ts +19 -4
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +22 -1
- package/dist/charts/primitives/XZoneRects.d.ts +40 -0
- package/dist/charts/primitives/XZoneRects.d.ts.map +1 -0
- package/dist/charts/primitives/XZoneRects.js +28 -0
- package/dist/charts/primitives/XZoneRects.test.d.ts +2 -0
- package/dist/charts/primitives/XZoneRects.test.d.ts.map +1 -0
- package/dist/charts/series.d.ts +16 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/series.js +3 -1
- package/dist/charts/series.test.d.ts +2 -0
- package/dist/charts/series.test.d.ts.map +1 -0
- package/dist/charts/utils/measure-text.d.ts +25 -0
- package/dist/charts/utils/measure-text.d.ts.map +1 -0
- package/dist/charts/utils/measure-text.js +64 -0
- package/dist/charts/utils/measure-text.test.d.ts +2 -0
- package/dist/charts/utils/measure-text.test.d.ts.map +1 -0
- package/dist/content/fence-block.d.ts.map +1 -1
- package/dist/shell/app-sidebar.d.ts +19 -1
- package/dist/shell/app-sidebar.d.ts.map +1 -1
- package/dist/shell/app-sidebar.js +30 -26
- package/dist/shell/app-sidebar.module.css +11 -0
- package/dist/shell/app-sidebar.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.test.d.ts +2 -0
- package/dist/shell/app-sidebar.test.d.ts.map +1 -0
- package/dist/shell/index.d.ts +10 -1
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +2 -0
- package/dist/shell/index.test.d.ts +2 -0
- package/dist/shell/index.test.d.ts.map +1 -0
- package/dist/surfaces.d.ts +1 -1
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +1 -1
- package/dist/tokens/chart-margin.test.d.ts +2 -0
- package/dist/tokens/chart-margin.test.d.ts.map +1 -0
- package/dist/tokens/index.d.ts +29 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +10 -0
- package/dist/tokens/palette.d.ts +4 -0
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +3 -0
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/charts/cursor/resolve.test.ts +61 -0
- package/src/charts/cursor/resolve.ts +102 -0
- package/src/charts/cursor/scope.tsx +32 -0
- package/src/charts/cursor/store.test.ts +46 -0
- package/src/charts/cursor/store.ts +53 -0
- package/src/charts/hooks/useChartCursor.ts +163 -0
- package/src/charts/hooks/useChartSize.ts +8 -4
- package/src/charts/index.ts +34 -14
- package/src/charts/kinds/Bars.tsx +327 -460
- package/src/charts/kinds/Donut.tsx +31 -23
- package/src/charts/kinds/DualPanel.tsx +228 -134
- package/src/charts/kinds/Heatmap.tsx +72 -47
- package/src/charts/kinds/MultiLine.tsx +158 -294
- package/src/charts/kinds/StackedArea.tsx +116 -175
- package/src/charts/kinds/ZonedLine.tsx +148 -255
- package/src/charts/layout/auto-margin.test.ts +42 -0
- package/src/charts/layout/auto-margin.ts +110 -0
- package/src/charts/primitives/CartesianChart.test.ts +99 -0
- package/src/charts/primitives/CartesianChart.tsx +566 -0
- package/src/charts/primitives/ChartFrame.test.tsx +33 -0
- package/src/charts/primitives/ChartFrame.tsx +40 -4
- package/src/charts/primitives/ChartLegend.test.tsx +48 -0
- package/src/charts/primitives/ChartLegend.tsx +45 -2
- package/src/charts/primitives/ChartTooltip.tsx +67 -37
- package/src/charts/primitives/HoverOverlay.tsx +39 -3
- package/src/charts/primitives/XZoneRects.test.tsx +63 -0
- package/src/charts/primitives/XZoneRects.tsx +67 -0
- package/src/charts/series.test.ts +61 -0
- package/src/charts/series.ts +18 -1
- package/src/charts/utils/measure-text.test.ts +36 -0
- package/src/charts/utils/measure-text.ts +109 -0
- package/src/content/fence-block.tsx +2 -3
- package/src/shell/app-sidebar.module.css +11 -0
- package/src/shell/app-sidebar.module.css.d.ts +1 -0
- package/src/shell/app-sidebar.test.tsx +98 -0
- package/src/shell/app-sidebar.tsx +22 -0
- package/src/shell/index.test.tsx +35 -0
- package/src/shell/index.tsx +11 -0
- package/src/surfaces.ts +2 -1
- package/src/theme/spacing.test.ts +2 -0
- package/src/tokens/chart-margin.test.ts +39 -0
- package/src/tokens/density.test.ts +1 -0
- package/src/tokens/index.ts +36 -0
- package/src/tokens/palette.ts +3 -0
- package/dist/charts/hooks/useChartTooltip.d.ts +0 -13
- package/dist/charts/hooks/useChartTooltip.d.ts.map +0 -1
- package/dist/charts/hooks/useChartTooltip.js +0 -26
- package/dist/charts/hooks/useHoverSync.d.ts +0 -46
- package/dist/charts/hooks/useHoverSync.d.ts.map +0 -1
- package/dist/charts/hooks/useHoverSync.js +0 -75
- package/dist/charts/hover-context.d.ts +0 -21
- package/dist/charts/hover-context.d.ts.map +0 -1
- package/dist/charts/hover-context.js +0 -12
- package/dist/charts/hover-sync.d.ts +0 -14
- package/dist/charts/hover-sync.d.ts.map +0 -1
- package/dist/charts/hover-sync.js +0 -17
- package/dist/charts/primitives/ResponsiveChart.d.ts +0 -45
- package/dist/charts/primitives/ResponsiveChart.d.ts.map +0 -1
- package/dist/charts/primitives/ResponsiveChart.js +0 -24
- package/src/charts/hooks/useChartTooltip.ts +0 -31
- package/src/charts/hooks/useHoverSync.ts +0 -152
- package/src/charts/hover-context.ts +0 -28
- package/src/charts/hover-sync.tsx +0 -37
- package/src/charts/primitives/ResponsiveChart.tsx +0 -64
|
@@ -1,46 +1,61 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: basalt-charts
|
|
3
|
-
description: Add or extend a chart in a basalt-ui app — pick a kind vs go bespoke (Rule of Three), register a consumer series with defineSeries/groupTokens (since VX.series is app-side, not shipped), compose
|
|
3
|
+
description: Add or extend a chart in a basalt-ui app — pick a kind vs go bespoke (Rule of Three), register a consumer series with defineSeries/groupTokens (since VX.series is app-side, not shipped), compose CartesianChart (or ChartFrame directly for a non-single-plot shape), and respect the @visx-only-in-charts + Mantine-free boundary. Use when adding a new chart, a new metric/series color, or extracting a repeated chart shape into a kind.
|
|
4
4
|
when_to_use: User wants to add a chart, restyle an existing one structurally, introduce a new metric/series and its color, extract a recurring chart shape into a reusable kind, or asks where chart series colors live / how to register them. The how-to companion to /basalt-design (which owns the aesthetic law).
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
`/basalt-charts` is the how-to for the chart system. `/basalt-design` owns the aesthetic law
|
|
8
8
|
(palette, restraint, the theme-lab loop) and the DESIGN.md precedence — read it for "what color /
|
|
9
9
|
how should this look". This skill answers "how do I wire it" with basalt-ui's `./charts` and
|
|
10
|
-
`./tokens` surfaces.
|
|
10
|
+
`./tokens` surfaces. `docs/CHARTS-SPEC.md` is the ground truth for the chart-primitive contract
|
|
11
|
+
everything below assumes — composing `CartesianChart` (single-plot) or `ChartFrame` directly
|
|
12
|
+
(multi-pane/radial/matrix) is mandatory, mechanically enforced (see "Respect the boundary" below).
|
|
11
13
|
|
|
12
14
|
## The decision: kind vs bespoke (Rule of Three)
|
|
13
15
|
|
|
14
16
|
Before writing chart code, decide which of three paths you are on:
|
|
15
17
|
|
|
16
|
-
1. **Reuse a shipped kind.** basalt-ui ships `ZonedLine`, `Bars`, `StackedArea`, `
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **`
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
1. **Reuse a shipped kind.** basalt-ui ships `ZonedLine`, `Bars`, `StackedArea`, `MultiLine`,
|
|
19
|
+
`DualPanel`, `Heatmap`, `Donut`. If the shape matches, use it — props are declarative (`data`,
|
|
20
|
+
`chartId`, `getX`, `series`/`positiveBars`, one `AxisConfig` per axis, zones/thresholds/refLines
|
|
21
|
+
as plain arrays). Done.
|
|
22
|
+
- **`ZonedLine`** — a single-series line with zone bands, thresholds, x-zones, refLines.
|
|
23
|
+
- **`Bars`** — 1+ stacked positive/negative bar series, optional line overlays, dual-axis config.
|
|
24
|
+
- **`StackedArea`** — opaque stacked bands, auto cumulative-top y-domain.
|
|
25
|
+
- **`MultiLine`** — N series on a shared y-axis (or two, via `y2`): legend-hover dimming,
|
|
26
|
+
per-series synced dots, dashed companion (MA) lines, per-point markers (PR stars / status
|
|
27
|
+
dots), zones + refLines, fixed or auto domain (also covers z-score/σ charts via a fixed
|
|
28
|
+
symmetric domain + zero refLine). Replaces bespoke multi-line / relative-progression /
|
|
29
|
+
training-load / strength-composite / fitness-trends.
|
|
25
30
|
- **`DualPanel`** — top line-pane + bottom signed-histogram pane sharing ONE x-scale and ONE
|
|
26
31
|
cursor; optional fill-between two top lines, zones, refLines. Replaces bespoke divergence /
|
|
27
32
|
momentum.
|
|
28
33
|
- **`Heatmap`** — category×category intensity grid (`color-mix` alpha), per-cell tooltip,
|
|
29
34
|
optional gradient legend strip. Replaces bespoke time-of-day.
|
|
35
|
+
- **`Donut`** — proportional donut, optional `centerContent` overlay.
|
|
30
36
|
2. **Extract a new kind (second instance of a pattern).** When you are about to build the _second_
|
|
31
37
|
chart of a shape that has no shipped kind, extract a kind. Don't extract on the first; don't
|
|
32
38
|
wait past the third. A new kind lives in the consumer's chart kinds dir (or, if generic enough
|
|
33
39
|
to promote, gets contributed to basalt-ui `src/charts/kinds/`). Migrate both call sites onto it.
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
A single-plot kind composes `CartesianChart`, drawing only marks; a multi-pane or non-cartesian
|
|
41
|
+
kind composes `ChartFrame` directly (see "Compose the primitives" below).
|
|
42
|
+
3. **Stay bespoke (genuinely unique).** A one-off shape (e.g. a dual-panel MACD, or two series on
|
|
43
|
+
genuinely different scales that don't fit any kind's config surface) composes `CartesianChart`
|
|
44
|
+
or `ChartFrame` directly in the page's chart file — not in `kinds/`.
|
|
36
45
|
|
|
37
46
|
> Anti-pattern: a single `<Chart type="..." config={...} />` god-component that switches by kind.
|
|
38
47
|
> That is the Recharts trap. Prefer N small kinds. **If a chart doesn't fit the primitives, add a
|
|
39
48
|
> kind — don't loosen the primitives.**
|
|
40
49
|
|
|
41
|
-
A good kind's props: `data`, `
|
|
42
|
-
thresholds / refLines arrays,
|
|
43
|
-
|
|
50
|
+
A good kind's props: `data`, `chartId`, generic `getX` accessor, `height`/`aspectRatio`/`fill`,
|
|
51
|
+
zones / thresholds / refLines arrays, one `AxisConfig` object per axis, `series: ChartSeries<T>[]`
|
|
52
|
+
as the single source of truth for color/label/legend/tooltip. Bespoke escape hatches
|
|
53
|
+
(`tooltip.extraRows`, `tooltip.prependRows`) are fine; they must not grow into a god-object config.
|
|
54
|
+
|
|
55
|
+
`ZonedLine` and `MultiLine` also take `xZones?: XZoneSpec[]` — vertical time-window bands, the
|
|
56
|
+
counterpart to the horizontal zones above. `{ from?, to?, fill }` bounds are `getX` **domain keys**
|
|
57
|
+
(the label string, not a date/timestamp); an omitted bound is the plot edge, a key missing from the
|
|
58
|
+
domain skips the band rather than clamping to one.
|
|
44
59
|
|
|
45
60
|
## Register a consumer series (VX.series is app-side)
|
|
46
61
|
|
|
@@ -84,73 +99,95 @@ export const PALETTE_CSS = buildPaletteCss({
|
|
|
84
99
|
- Inject `PALETTE_CSS` once (BasaltProvider injects the framework palette; the consumer appends its
|
|
85
100
|
series CSS — or passes `injectPalette={false}` and head-injects both).
|
|
86
101
|
- Then charts read `SERIES.hrv`, never a hex. This is what keeps `basalt-ui check-theme` green.
|
|
102
|
+
- A series with nothing visible to point at (flat at the domain floor, all-zero) can still carry a
|
|
103
|
+
qualifier: pass `note` on the series' `SeriesStyle` (flows to `LegendEntry.note` via
|
|
104
|
+
`deriveLegend`) and `ChartLegend` renders it, muted, after the label.
|
|
87
105
|
|
|
88
106
|
Theme tuning of these series happens in the theme lab — pass them as `groups` to
|
|
89
107
|
`ThemeLabControls` (see `/basalt-design`).
|
|
90
108
|
|
|
91
109
|
## Compose the primitives (never hand-roll)
|
|
92
110
|
|
|
93
|
-
Every chart — kind or bespoke — composes
|
|
94
|
-
|
|
111
|
+
Every chart — kind or bespoke — composes `CartesianChart` for a single plot rect, or `ChartFrame`
|
|
112
|
+
directly for a multi-pane / non-cartesian shape (`DualPanel`'s two panes, `Heatmap`'s grid,
|
|
113
|
+
`Donut`'s ring). Either way the contract is the same: the primitive owns margin, scales, grid,
|
|
114
|
+
axes, legend, the shared cursor, and the tooltip; you supply `series` and draw ONLY marks. This is
|
|
115
|
+
mandatory, not a convention — `basalt/hand-rolled-plot` fails the build on a bypass (see "Respect
|
|
116
|
+
the boundary" below); it is easier to compose it than to work around it.
|
|
117
|
+
|
|
118
|
+
**The common case — a single-plot chart:**
|
|
95
119
|
|
|
96
120
|
```tsx
|
|
97
|
-
import {
|
|
98
|
-
|
|
99
|
-
ChartLegend,
|
|
100
|
-
AxisBottomDate,
|
|
101
|
-
AxisLeftNumeric,
|
|
102
|
-
HoverOverlay,
|
|
103
|
-
AreaGradient,
|
|
104
|
-
areaFillUrl,
|
|
105
|
-
ChartTooltip,
|
|
106
|
-
TooltipHeader,
|
|
107
|
-
TooltipRow,
|
|
108
|
-
useChartTooltip,
|
|
109
|
-
useHoverSync,
|
|
110
|
-
VX,
|
|
111
|
-
alpha,
|
|
112
|
-
} from 'basalt-ui/charts' // ./charts re-exports the token layer too
|
|
121
|
+
import { CartesianChart, LinePath, curveMonotoneX, VX } from 'basalt-ui/charts'
|
|
122
|
+
import type { ChartSeries } from 'basalt-ui/charts'
|
|
113
123
|
import { SERIES } from '../lib/series'
|
|
114
124
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
125
|
+
type HrvPoint = { date: string; hrv: number }
|
|
126
|
+
|
|
127
|
+
const HRV_SERIES: ChartSeries<HrvPoint>[] = [
|
|
128
|
+
{ key: 'hrv', label: 'HRV', color: SERIES.hrv, mark: 'line', getValue: (d) => d.hrv },
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
function HrvChart({ data }: { data: HrvPoint[] }) {
|
|
118
132
|
return (
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
<CartesianChart
|
|
134
|
+
data={data}
|
|
135
|
+
chartId="hrv"
|
|
136
|
+
getX={(d) => d.date}
|
|
137
|
+
series={HRV_SERIES}
|
|
138
|
+
y={{ domain: 'auto', format: (v) => `${Math.round(v)} ms` }}
|
|
139
|
+
height={240}
|
|
140
|
+
>
|
|
141
|
+
{({ xScale, yScale, visible }) =>
|
|
142
|
+
visible.map((s) => (
|
|
143
|
+
<LinePath<HrvPoint>
|
|
144
|
+
key={s.key}
|
|
145
|
+
data={data}
|
|
146
|
+
x={(d) => xScale(d.date) ?? 0}
|
|
147
|
+
y={(d) => yScale(s.getValue(d) ?? 0)}
|
|
148
|
+
stroke={s.color}
|
|
149
|
+
strokeWidth={VX.lineWidth}
|
|
150
|
+
curve={curveMonotoneX}
|
|
151
|
+
/>
|
|
152
|
+
))
|
|
153
|
+
}
|
|
154
|
+
</CartesianChart>
|
|
133
155
|
)
|
|
134
156
|
}
|
|
135
157
|
```
|
|
136
158
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
159
|
+
`CartesianChart` draws the grid, both axes, the shared cursor + crosshair + dots, and the derived
|
|
160
|
+
tooltip for you — `HrvChart` above draws nothing but the line: no hover wiring, no manual
|
|
161
|
+
`<AxisLeft>`/`<AxisBottom>`, no tooltip assembly.
|
|
162
|
+
|
|
163
|
+
**Dual-axis is `y2`, not a flag.** Passing `y2` is what turns on the right axis (margin widens by
|
|
164
|
+
measurement); a series opts in with `axis: 'right'` on its `ChartSeries` entry. See
|
|
165
|
+
`apps/playground/src/demo/ChartsPage.tsx`'s `SessionsRevenueChart` for a full worked example (two
|
|
166
|
+
series on genuinely different scales — session counts vs. `$k` revenue — composed directly from
|
|
167
|
+
`CartesianChart` because no shipped kind's config surface covers a dual-axis line pair).
|
|
168
|
+
|
|
169
|
+
Never substitute: hand-rolled legend markup, a raw `<AxisLeft>`/`<AxisBottom>`/`<AxisRight>`, a
|
|
170
|
+
direct `@visx/tooltip` import, or an `rgba()` fill. Sparklines (`charts/sparklines/`) are the one
|
|
171
|
+
exemption from the `CartesianChart`/legend/tooltip composition — but they still use `VX.*` tokens.
|
|
172
|
+
|
|
173
|
+
## The cursor is shared by default
|
|
141
174
|
|
|
142
|
-
|
|
175
|
+
No provider needed. `useChartCursor` reads a module-level external store, so every chart on the
|
|
176
|
+
page — kind or bespoke, `CartesianChart` or `ChartFrame` — shares one cursor out of the box, and
|
|
177
|
+
resolution is **domain-aware** (nearest parsed date/number within a chart's own step, not exact
|
|
178
|
+
string match), so a chart that folds its calendar into weekly buckets still tracks a hover from an
|
|
179
|
+
unfolded daily sibling.
|
|
143
180
|
|
|
144
|
-
Wrap a
|
|
145
|
-
|
|
146
|
-
|
|
181
|
+
Wrap a subtree in `ChartCursorScope` to **isolate** it onto a private cursor instead — the inverse
|
|
182
|
+
of the old opt-in pattern, for e.g. two independent dashboards rendered side by side over the same
|
|
183
|
+
calendar that must NOT sync with each other:
|
|
147
184
|
|
|
148
185
|
```tsx
|
|
149
|
-
import {
|
|
150
|
-
;<
|
|
186
|
+
import { ChartCursorScope } from 'basalt-ui/charts'
|
|
187
|
+
;<ChartCursorScope>
|
|
151
188
|
<ChartCard>…</ChartCard>
|
|
152
189
|
<ChartCard>…</ChartCard>
|
|
153
|
-
</
|
|
190
|
+
</ChartCursorScope>
|
|
154
191
|
```
|
|
155
192
|
|
|
156
193
|
## Respect the boundary (oxlint-enforced)
|
|
@@ -171,15 +208,28 @@ import { ChartHoverSync } from 'basalt-ui/charts'
|
|
|
171
208
|
- **No raw visx axes.** `basalt-ui check-theme`'s `raw-visx-axis` guard fails the build on a raw
|
|
172
209
|
`<AxisLeft>`/`<AxisBottom>`/`<AxisRight>` inside any `/charts/` file — use the tokenized
|
|
173
210
|
`AxisLeftNumeric` / `AxisRightNumeric` / `AxisBottomDate` (escape via `theme-allow`).
|
|
211
|
+
- **No hand-rolled plot assembly.** `basalt/hand-rolled-plot` fails the build on a chart-assembly
|
|
212
|
+
primitive (`AxisLeftNumeric`, `AxisRightNumeric`, `AxisBottomDate`, `HoverOverlay`, `Crosshair`)
|
|
213
|
+
rendered in a file that doesn't compose `CartesianChart`. Escape: a `theme-allow` comment on the
|
|
214
|
+
first such site — how a genuinely non-single-plot shape (`DualPanel`, `Heatmap`, `Donut`) declares
|
|
215
|
+
itself. The file that defines `CartesianChart` is exempt definitionally.
|
|
216
|
+
- **No hand-authored legends.** `basalt/chart-legend-literal` fails the build on a hand-written
|
|
217
|
+
array literal passed to `ChartLegend`'s `items` — derive it from the same `series` the chart draws
|
|
218
|
+
(`deriveLegend`, or let `ChartFrame`/`CartesianChart` do it) so it can't name a series the plot no
|
|
219
|
+
longer draws.
|
|
220
|
+
- Both ship at `warn` in the consumer preset (`configs/oxlint.json`) for one minor and `error`
|
|
221
|
+
repo-local (the "grace minor" doctrine in `packages/basalt-ui/CLAUDE.md`) — promoting to `error`
|
|
222
|
+
in the next minor.
|
|
174
223
|
|
|
175
224
|
## Checklist for a new chart
|
|
176
225
|
|
|
177
226
|
- Picked the right path: reused a kind / extracted on the second instance / stayed bespoke for a
|
|
178
227
|
true one-off.
|
|
228
|
+
- Composed `CartesianChart` (single plot) or `ChartFrame` directly (multi-pane / non-cartesian) —
|
|
229
|
+
never hand-assembled margin/scales/axes/cursor/tooltip.
|
|
179
230
|
- Any new metric color is a `{ light, dark }` pair in the consumer series file, exposed via
|
|
180
231
|
`seriesTokens`/`groupTokens`, and wired into `PALETTE_CSS` via `buildPaletteCss`.
|
|
181
232
|
- Charts read `SERIES.*` / `VX.*` — `basalt-ui check-theme` green, zero raw hex.
|
|
182
|
-
-
|
|
183
|
-
no direct `@visx/tooltip`.
|
|
233
|
+
- Marks draw `ctx.visible`, never the `series` prop, so a legend toggle actually removes them.
|
|
184
234
|
- `@visx/*` stays inside chart files; chart code imports no `@mantine/*`.
|
|
185
235
|
- Tuned both schemes via the theme lab where the hue needed it (defer to `/basalt-design`).
|
|
@@ -233,9 +233,10 @@ Every chart composes the shipped primitives — never hand-rolled equivalents:
|
|
|
233
233
|
`<AxisLeft>`/`<AxisBottom>` (they miss theme tokens + smart ticks). Now lint-enforced:
|
|
234
234
|
`basalt-ui check-theme`'s `raw-visx-axis` guard fails the build on a raw axis inside a `/charts/`
|
|
235
235
|
file (escape via `theme-allow`).
|
|
236
|
-
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
- **The cursor is shared by default** — `CartesianChart` wires the hover overlay, crosshair, dots
|
|
237
|
+
and tooltip itself, and every chart on the page tracks one cursor through a module-level store
|
|
238
|
+
with no provider. Reach for `useChartCursor` only in a hand-composed non-single-plot shape, and
|
|
239
|
+
for **`ChartCursorScope`** only to opt a subtree OUT of sharing.
|
|
239
240
|
- **`AreaGradient`** / `areaFillUrl` for the soft single-hue fill.
|
|
240
241
|
|
|
241
242
|
Shipped kinds beyond `ZonedLine` / `Bars` / `StackedArea` / `Donut`: **`MultiLine`** (N series on a
|
package/configs/oxlint-plugin.js
CHANGED
|
@@ -803,6 +803,156 @@ const aiSdkMajor = {
|
|
|
803
803
|
},
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
+
// ── Rule 12 — hand-rolled-plot ───────────────────────────────────────────────────────────────────
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* The primitives `CartesianChart` assembles. Rendering one of these directly means the file is
|
|
810
|
+
* re-implementing the plot assembly — margins, scales, axes, cursor, crosshair, tooltip — that the
|
|
811
|
+
* primitive owns for every other chart, which is exactly how charts drift apart from each other.
|
|
812
|
+
*/
|
|
813
|
+
const PLOT_ASSEMBLY_TAGS = new Set([
|
|
814
|
+
'AxisLeftNumeric',
|
|
815
|
+
'AxisRightNumeric',
|
|
816
|
+
'AxisBottomDate',
|
|
817
|
+
'HoverOverlay',
|
|
818
|
+
'Crosshair',
|
|
819
|
+
])
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Composing this means the file is USING the chart system rather than re-implementing it.
|
|
823
|
+
* `ChartFrame` deliberately does NOT count: composing the frame directly and then hand-assembling
|
|
824
|
+
* axes and a cursor on top of it IS the drift this rule exists to catch. A genuinely non-single-
|
|
825
|
+
* plot shape does exactly that — and declares it with a `theme-allow` comment, so the exception is
|
|
826
|
+
* a decision someone wrote down rather than a default anyone can fall into.
|
|
827
|
+
*/
|
|
828
|
+
const PLOT_OWNER_TAGS = new Set(['CartesianChart'])
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Import sources that make a JSX tag one of BASALT's chart primitives rather than a component the
|
|
832
|
+
* consumer happens to have named `Crosshair`. Matching bare tag names would false-positive on any
|
|
833
|
+
* app with its own overlay component — and this rule ships to consumers, so that matters.
|
|
834
|
+
*/
|
|
835
|
+
const CHART_IMPORT_SOURCE = /(^|\/)basalt-ui\/charts|(^|\/)charts(\/|$)|(^|\.\.\/)primitives\//
|
|
836
|
+
|
|
837
|
+
/** Local names imported from a basalt charts entry — the only tags either chart rule considers. */
|
|
838
|
+
function collectChartImports(node, into) {
|
|
839
|
+
if (!CHART_IMPORT_SOURCE.test(node.source?.value ?? '')) return
|
|
840
|
+
for (const spec of node.specifiers ?? []) {
|
|
841
|
+
if (spec.local?.name !== undefined) into.add(spec.local.name)
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const HAND_ROLLED_PLOT_MESSAGE =
|
|
846
|
+
'Chart assembly primitive rendered without composing CartesianChart — that primitive already ' +
|
|
847
|
+
'owns the measured margins, both y scales, the axes, grid, shared cursor, crosshair and ' +
|
|
848
|
+
'tooltip. Hand-assembling them is how two charts stop matching. Compose CartesianChart and ' +
|
|
849
|
+
'render only marks. A genuinely non-single-plot shape (multi-pane, radial, matrix) composes ' +
|
|
850
|
+
'ChartFrame instead — add a theme-allow comment to declare that. (basalt/hand-rolled-plot)'
|
|
851
|
+
|
|
852
|
+
const handRolledPlot = {
|
|
853
|
+
meta: {
|
|
854
|
+
type: 'suggestion',
|
|
855
|
+
docs: {
|
|
856
|
+
description:
|
|
857
|
+
'Disallow assembling a cartesian plot by hand instead of composing CartesianChart.',
|
|
858
|
+
},
|
|
859
|
+
schema: [],
|
|
860
|
+
},
|
|
861
|
+
create(context) {
|
|
862
|
+
// File-scoped, not node-scoped: the verdict depends on whether a plot OWNER appears anywhere
|
|
863
|
+
// in the file, which is only known once the whole file has been walked.
|
|
864
|
+
const assemblyNodes = []
|
|
865
|
+
const chartImports = new Set()
|
|
866
|
+
let ownsPlot = false
|
|
867
|
+
let definesOwner = false
|
|
868
|
+
|
|
869
|
+
// A rule that says "compose CartesianChart" cannot fire inside the module that DEFINES
|
|
870
|
+
// CartesianChart — that file is the assembly. Detected by declaration rather than by path so
|
|
871
|
+
// it stays honest: this is definitional, not basalt exempting itself for convenience (the
|
|
872
|
+
// self-exemption habit is what let the 1.4.0 regression reach consumers — see docs/STATUS.md).
|
|
873
|
+
const notesOwnerDefinition = (name) => {
|
|
874
|
+
if (name === 'CartesianChart') definesOwner = true
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return {
|
|
878
|
+
ImportDeclaration(node) {
|
|
879
|
+
collectChartImports(node, chartImports)
|
|
880
|
+
},
|
|
881
|
+
FunctionDeclaration(node) {
|
|
882
|
+
notesOwnerDefinition(node.id?.name)
|
|
883
|
+
},
|
|
884
|
+
VariableDeclarator(node) {
|
|
885
|
+
notesOwnerDefinition(node.id?.name)
|
|
886
|
+
},
|
|
887
|
+
JSXOpeningElement(node) {
|
|
888
|
+
const tagName = node.name?.name
|
|
889
|
+
if (PLOT_OWNER_TAGS.has(tagName)) ownsPlot = true
|
|
890
|
+
else if (PLOT_ASSEMBLY_TAGS.has(tagName)) assemblyNodes.push(node)
|
|
891
|
+
},
|
|
892
|
+
'Program:exit'() {
|
|
893
|
+
if (ownsPlot || definesOwner) return
|
|
894
|
+
// Only the FIRST site is reported, and the whole check is FILE-scoped rather than
|
|
895
|
+
// component-scoped. Both are deliberate. File scope is forced by the shape of the correct
|
|
896
|
+
// pattern: `<CartesianChart>{() => <AxisLeftNumeric …/>}</CartesianChart>` puts the axis
|
|
897
|
+
// inside a render-prop arrow, a different function from the component that owns the chart
|
|
898
|
+
// — component scope would flag the canonical usage. One report because declaring an
|
|
899
|
+
// exception is a decision about the FILE ("this shape isn't a single cartesian plot"), so
|
|
900
|
+
// one `theme-allow` settles it instead of one comment per axis and overlay.
|
|
901
|
+
const [first] = assemblyNodes.filter((node) => chartImports.has(node.name?.name))
|
|
902
|
+
if (first === undefined || hasThemeAllow(context, first)) return
|
|
903
|
+
context.report({ node: first, message: HAND_ROLLED_PLOT_MESSAGE })
|
|
904
|
+
},
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// ── Rule 13 — chart-legend-literal ───────────────────────────────────────────────────────────────
|
|
910
|
+
|
|
911
|
+
const CHART_LEGEND_LITERAL_MESSAGE =
|
|
912
|
+
'Hand-authored ChartLegend items — the legend must be DERIVED from the same `series` array the ' +
|
|
913
|
+
'chart draws (deriveLegend, or just let ChartFrame/CartesianChart do it). A hand-written list ' +
|
|
914
|
+
'is a second source of truth that silently goes stale: it keeps naming a series after the plot ' +
|
|
915
|
+
'stops drawing it. (basalt/chart-legend-literal)'
|
|
916
|
+
|
|
917
|
+
const chartLegendLiteral = {
|
|
918
|
+
meta: {
|
|
919
|
+
type: 'suggestion',
|
|
920
|
+
docs: { description: 'Require ChartLegend items to be derived from the series descriptor.' },
|
|
921
|
+
schema: [],
|
|
922
|
+
},
|
|
923
|
+
create(context) {
|
|
924
|
+
const chartImports = new Set()
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
ImportDeclaration(node) {
|
|
928
|
+
collectChartImports(node, chartImports)
|
|
929
|
+
},
|
|
930
|
+
JSXOpeningElement(node) {
|
|
931
|
+
if (node.name?.name !== 'ChartLegend' || !chartImports.has('ChartLegend')) return
|
|
932
|
+
const items = node.attributes?.find(
|
|
933
|
+
(attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'items',
|
|
934
|
+
)
|
|
935
|
+
const expression = items?.value?.expression
|
|
936
|
+
if (expression?.type !== 'ArrayExpression') return
|
|
937
|
+
// Only a list that is ENTIRELY object literals is a hand-authored legend. An array that
|
|
938
|
+
// spreads or calls something (`[...deriveLegend(series), note]`) is derived-and-extended,
|
|
939
|
+
// which is legitimate — flagging it would push people to disable the rule.
|
|
940
|
+
//
|
|
941
|
+
// Known and accepted gap: hoisting the literal to a `const` one line up
|
|
942
|
+
// (`const items = [...]; <ChartLegend items={items} />`) reads as an Identifier here and
|
|
943
|
+
// is not flagged. Following that would mean local flow analysis for a bypass nobody
|
|
944
|
+
// reaches by accident; the rule targets the shape people actually write.
|
|
945
|
+
const allObjectLiterals =
|
|
946
|
+
expression.elements.length > 0 &&
|
|
947
|
+
expression.elements.every((el) => el?.type === 'ObjectExpression')
|
|
948
|
+
if (!allObjectLiterals) return
|
|
949
|
+
if (hasThemeAllow(context, node)) return
|
|
950
|
+
context.report({ node, message: CHART_LEGEND_LITERAL_MESSAGE })
|
|
951
|
+
},
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
}
|
|
955
|
+
|
|
806
956
|
// ── Plugin export ───────────────────────────────────────────────────────────────────────────────
|
|
807
957
|
|
|
808
958
|
export default {
|
|
@@ -812,6 +962,8 @@ export default {
|
|
|
812
962
|
'raw-size-literal': rawSizeLiteral,
|
|
813
963
|
'card-inset': cardInset,
|
|
814
964
|
'chart-in-raw-surface': chartInRawSurface,
|
|
965
|
+
'hand-rolled-plot': handRolledPlot,
|
|
966
|
+
'chart-legend-literal': chartLegendLiteral,
|
|
815
967
|
'raw-scroll-container': rawScrollContainer,
|
|
816
968
|
'visx-boundary': visxBoundary,
|
|
817
969
|
'visx-tooltip': visxTooltip,
|
|
@@ -28,6 +28,8 @@ beforeEach(() => {
|
|
|
28
28
|
'basalt/no-raw-font-size': 'error',
|
|
29
29
|
'basalt/card-inset': 'error',
|
|
30
30
|
'basalt/chart-in-raw-surface': 'error',
|
|
31
|
+
'basalt/hand-rolled-plot': 'error',
|
|
32
|
+
'basalt/chart-legend-literal': 'error',
|
|
31
33
|
'basalt/visx-boundary': 'error',
|
|
32
34
|
'basalt/visx-tooltip': 'error',
|
|
33
35
|
'basalt/token-layer-boundary': 'error',
|
|
@@ -476,3 +478,108 @@ describe('basalt agent rules promotion', () => {
|
|
|
476
478
|
},
|
|
477
479
|
)
|
|
478
480
|
})
|
|
481
|
+
|
|
482
|
+
// ── hand-rolled-plot ─────────────────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
const CHARTS_IMPORT = `import { AxisLeftNumeric, CartesianChart, ChartLegend, Crosshair, HoverOverlay } from 'basalt-ui/charts'\n`
|
|
485
|
+
|
|
486
|
+
describe('basalt/hand-rolled-plot', () => {
|
|
487
|
+
it('flags an axis primitive in a file that never composes CartesianChart', () => {
|
|
488
|
+
const { code, rules } = run(
|
|
489
|
+
`${CHARTS_IMPORT}export const C = () => <svg><AxisLeftNumeric scale={s} /></svg>\n`,
|
|
490
|
+
)
|
|
491
|
+
expect(code).toBe(1)
|
|
492
|
+
expect(rules).toContain('hand-rolled-plot')
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('does NOT flag the same primitive when CartesianChart is composed', () => {
|
|
496
|
+
const { code, rules } = run(
|
|
497
|
+
`${CHARTS_IMPORT}export const C = () => <CartesianChart>{() => <AxisLeftNumeric scale={s} />}</CartesianChart>\n`,
|
|
498
|
+
)
|
|
499
|
+
expect(code).toBe(0)
|
|
500
|
+
expect(rules).not.toContain('hand-rolled-plot')
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('reports ONCE per file — declaring the exception is a file-level decision', () => {
|
|
504
|
+
const { code, rules } = run(
|
|
505
|
+
`${CHARTS_IMPORT}export const C = () => (
|
|
506
|
+
<svg>
|
|
507
|
+
{/* theme-allow: multi-pane shape, not a single cartesian plot */}
|
|
508
|
+
<AxisLeftNumeric scale={a} />
|
|
509
|
+
<HoverOverlay width={1} height={1} />
|
|
510
|
+
<Crosshair x={0} top={0} bottom={1} />
|
|
511
|
+
</svg>
|
|
512
|
+
)\n`,
|
|
513
|
+
)
|
|
514
|
+
expect(code).toBe(0)
|
|
515
|
+
expect(rules).not.toContain('hand-rolled-plot')
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
it('does NOT flag a same-named component that is NOT a basalt chart primitive', () => {
|
|
519
|
+
const { code, rules } = run(
|
|
520
|
+
`import { Crosshair } from './my-own-widgets'
|
|
521
|
+
export const C = () => <svg><Crosshair x={0} /></svg>\n`,
|
|
522
|
+
)
|
|
523
|
+
expect(code).toBe(0)
|
|
524
|
+
expect(rules).not.toContain('hand-rolled-plot')
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('does NOT flag the module that DEFINES CartesianChart (function form)', () => {
|
|
528
|
+
const { code, rules } = run(
|
|
529
|
+
`import { AxisBottomDate } from '../primitives/Axes'
|
|
530
|
+
export function CartesianChart() {
|
|
531
|
+
return <svg><AxisBottomDate scale={s} top={0} tickValues={[]} /></svg>
|
|
532
|
+
}\n`,
|
|
533
|
+
)
|
|
534
|
+
expect(code).toBe(0)
|
|
535
|
+
expect(rules).not.toContain('hand-rolled-plot')
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('does NOT flag the module that DEFINES CartesianChart (const form)', () => {
|
|
539
|
+
const { code, rules } = run(
|
|
540
|
+
`import { AxisBottomDate } from '../primitives/Axes'
|
|
541
|
+
export const CartesianChart = () => <svg><AxisBottomDate scale={s} top={0} tickValues={[]} /></svg>\n`,
|
|
542
|
+
)
|
|
543
|
+
expect(code).toBe(0)
|
|
544
|
+
expect(rules).not.toContain('hand-rolled-plot')
|
|
545
|
+
})
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
// ── chart-legend-literal ─────────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
describe('basalt/chart-legend-literal', () => {
|
|
551
|
+
it('flags a hand-authored items array', () => {
|
|
552
|
+
const { code, rules } = run(
|
|
553
|
+
`${CHARTS_IMPORT}export const C = () => <ChartLegend items={[{ key: 'a', label: 'A', color: 'red' }]} />\n`,
|
|
554
|
+
)
|
|
555
|
+
expect(code).toBe(1)
|
|
556
|
+
expect(rules).toContain('chart-legend-literal')
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('does NOT flag items derived from the series descriptor', () => {
|
|
560
|
+
const { code, rules } = run(
|
|
561
|
+
`${CHARTS_IMPORT}export const C = () => <ChartLegend items={deriveLegend(series)} />\n`,
|
|
562
|
+
)
|
|
563
|
+
expect(code).toBe(0)
|
|
564
|
+
expect(rules).not.toContain('chart-legend-literal')
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('does NOT flag a derived-and-extended list — that is legitimate composition', () => {
|
|
568
|
+
const { code, rules } = run(
|
|
569
|
+
`${CHARTS_IMPORT}export const C = () => <ChartLegend items={[...deriveLegend(series), note]} />\n`,
|
|
570
|
+
)
|
|
571
|
+
expect(code).toBe(0)
|
|
572
|
+
expect(rules).not.toContain('chart-legend-literal')
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
it('honors a theme-allow escape', () => {
|
|
576
|
+
const { code, rules } = run(
|
|
577
|
+
`${CHARTS_IMPORT}export const C = () => (
|
|
578
|
+
// theme-allow: static reference legend, no series behind it
|
|
579
|
+
<ChartLegend items={[{ key: 'a', label: 'A', color: 'red' }]} />
|
|
580
|
+
)\n`,
|
|
581
|
+
)
|
|
582
|
+
expect(code).toBe(0)
|
|
583
|
+
expect(rules).not.toContain('chart-legend-literal')
|
|
584
|
+
})
|
|
585
|
+
})
|
package/configs/oxlint.json
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"basalt/raw-size-literal": "warn",
|
|
21
21
|
"basalt/card-inset": "error",
|
|
22
22
|
"basalt/chart-in-raw-surface": "error",
|
|
23
|
+
"basalt/hand-rolled-plot": "warn",
|
|
24
|
+
"basalt/chart-legend-literal": "warn",
|
|
23
25
|
"basalt/raw-scroll-container": "error",
|
|
24
26
|
"basalt/visx-boundary": "error",
|
|
25
27
|
"basalt/visx-tooltip": "error",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-aware resolution of a BROADCAST cursor key against a chart's own points.
|
|
3
|
+
*
|
|
4
|
+
* The layer this replaced matched keys byte-for-byte, so two charts shared a cursor only if they emitted identical
|
|
5
|
+
* strings. A chart that folded its domain to fit a narrow viewport stopped owning most of the keys
|
|
6
|
+
* its siblings broadcast, and the shared crosshair then landed on roughly one hover in three with
|
|
7
|
+
* no rule a reader could infer — patched per chart by a manual `resolveKey`. Resolving on the
|
|
8
|
+
* PARSED domain instead makes that automatic (`docs/CHARTS-SPEC.md` §3).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* A key as a comparable number: numeric-looking strings as numbers, everything else as a date.
|
|
12
|
+
* Neither (a plain category like "Direct") → null, which degrades to exact-match only — correct,
|
|
13
|
+
* since "nearest category" is meaningless.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseKey(key: string): number | null;
|
|
16
|
+
export type DomainIndex<T> = {
|
|
17
|
+
byKey: Map<string, T>;
|
|
18
|
+
/** Parsed positions, ascending. Empty when the domain isn't parseable. */
|
|
19
|
+
values: number[];
|
|
20
|
+
/** Points parallel to `values`. */
|
|
21
|
+
points: T[];
|
|
22
|
+
/** Median spacing — the tolerance within which a foreign key still resolves. */
|
|
23
|
+
step: number;
|
|
24
|
+
};
|
|
25
|
+
/** Build the lookup structure once per `data` identity. */
|
|
26
|
+
export declare function buildDomainIndex<T>(data: readonly T[], getKey: (d: T) => string): DomainIndex<T>;
|
|
27
|
+
/**
|
|
28
|
+
* The own point a broadcast `key` maps to: exact match first, else the nearest parsed position
|
|
29
|
+
* within one domain step. Out of range → null, so charts over unrelated domains never paint a
|
|
30
|
+
* cursor for each other — which is what makes "shared by default" safe.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveCursorPoint<T>(index: DomainIndex<T>, key: string): T | null;
|
|
33
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/charts/cursor/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOnD;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;IACrB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,mCAAmC;IACnC,MAAM,EAAE,CAAC,EAAE,CAAA;IACX,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CA4BhG;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CA+BlF"}
|