basalt-ui 1.14.0 → 1.16.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 +279 -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/agent/templates/CLAUDE-block.md.tpl +9 -6
- 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 +45 -54
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +235 -392
- package/dist/charts/kinds/Bars.test.d.ts +2 -0
- package/dist/charts/kinds/Bars.test.d.ts.map +1 -0
- 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 +13 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +185 -92
- package/dist/charts/kinds/DualPanel.test.d.ts +2 -0
- package/dist/charts/kinds/DualPanel.test.d.ts.map +1 -0
- 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 +122 -235
- package/dist/charts/kinds/MultiLine.test.d.ts +2 -0
- package/dist/charts/kinds/MultiLine.test.d.ts.map +1 -0
- 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 +104 -184
- package/dist/charts/kinds/ZonedLine.test.d.ts +2 -0
- package/dist/charts/kinds/ZonedLine.test.d.ts.map +1 -0
- 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 +161 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -0
- package/dist/charts/primitives/CartesianChart.js +359 -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 +15 -1
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +25 -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 +54 -0
- package/dist/charts/primitives/XZoneRects.d.ts.map +1 -0
- package/dist/charts/primitives/XZoneRects.js +65 -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 +28 -3
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/series.js +5 -2
- 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/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 +35 -14
- package/src/charts/kinds/Bars.test.tsx +98 -0
- package/src/charts/kinds/Bars.tsx +350 -467
- package/src/charts/kinds/Donut.tsx +31 -23
- package/src/charts/kinds/DualPanel.test.tsx +131 -0
- package/src/charts/kinds/DualPanel.tsx +301 -135
- package/src/charts/kinds/Heatmap.tsx +72 -47
- package/src/charts/kinds/MultiLine.test.tsx +37 -0
- package/src/charts/kinds/MultiLine.tsx +171 -290
- package/src/charts/kinds/StackedArea.tsx +116 -175
- package/src/charts/kinds/ZonedLine.test.tsx +37 -0
- package/src/charts/kinds/ZonedLine.tsx +149 -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.tsx +160 -0
- package/src/charts/primitives/CartesianChart.tsx +598 -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 +78 -0
- package/src/charts/primitives/ChartLegend.tsx +52 -2
- package/src/charts/primitives/ChartTooltip.tsx +67 -37
- package/src/charts/primitives/HoverOverlay.tsx +39 -3
- package/src/charts/primitives/XZoneRects.test.tsx +123 -0
- package/src/charts/primitives/XZoneRects.tsx +132 -0
- package/src/charts/series.test.ts +112 -0
- package/src/charts/series.ts +33 -5
- 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/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
package/AGENTS.md
CHANGED
|
@@ -9,7 +9,7 @@ Toolchain: Bun runtime, oxlint + oxfmt, conventional commits (empty scope), `mas
|
|
|
9
9
|
| Subpath | Layer | Purpose |
|
|
10
10
|
| --------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
11
|
| `basalt-ui` | mantine-coupled | BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, SettingsSection/SettingsRow/DangerZone) |
|
|
12
|
-
| `basalt-ui/charts` | headless | visx chart primitives, sparklines, hooks, and token re-exports (Mantine-free)
|
|
12
|
+
| `basalt-ui/charts` | headless | CartesianChart + visx chart primitives, kinds, sparklines, hooks, and token re-exports (Mantine-free) |
|
|
13
13
|
| `basalt-ui/tokens` | headless | VX token refs, buildPaletteCss, defineSeries, seriesTokens, groupTokens, alpha (Mantine-free) |
|
|
14
14
|
| `basalt-ui/theme-lab` | mantine-coupled | ThemeLabControls, applyOverrides, COLOR_GROUPS — a low-level inspector for the non-derived structural tokens; identity/color tuning lives in DeriveControls |
|
|
15
15
|
| `basalt-ui/vite` | mantine-coupled | basaltViteConfig(opts) — Vite preset for basalt-ui consumer apps; basaltAppPlugin(opts) — PWA head, manifest, and icon metadata derived from the token palette |
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# [1.16.0](https://github.com/jkrumm/basalt-ui/compare/v1.15.0...v1.16.0) (2026-08-18)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* close the chart-API gaps the first full consumer migration found ([518a086](https://github.com/jkrumm/basalt-ui/commit/518a086e632c7c010584071f5780d7aa3bbe3bc2))
|
|
7
|
+
|
|
8
|
+
# [1.15.0](https://github.com/jkrumm/basalt-ui/compare/v1.14.0...v1.15.0) (2026-08-18)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add x-range chart bands, a dual-axis margin, and legend notes ([4883c19](https://github.com/jkrumm/basalt-ui/commit/4883c19d06a43d6a908efcf9fe8098610c86627a))
|
|
14
|
+
* make one cartesian primitive the whole chart contract ([4f8a507](https://github.com/jkrumm/basalt-ui/commit/4f8a507b2ca01923c1ee9bdf8a911ac0e5b76710))
|
|
15
|
+
|
|
1
16
|
# [1.14.0](https://github.com/jkrumm/basalt-ui/compare/v1.13.0...v1.14.0) (2026-08-10)
|
|
2
17
|
|
|
3
18
|
|
package/README.md
CHANGED
|
@@ -222,7 +222,7 @@ deleted rather than silently surviving into the next real skew.
|
|
|
222
222
|
| Subpath | Mantine? | Purpose |
|
|
223
223
|
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
224
224
|
| `.` | coupled | `BasaltProvider`, `createBasaltTheme` / `baseTheme` / `cssVariablesResolver`, `BasaltShell` + sidebar / mobile-nav / breadcrumbs / page-header, `NavCountBadge`, `SidebarAccount` + the provider-agnostic account contract (`BasaltAccountProps`/`State`/`Actions`), `ThreadWorkspace` + thread-chat components, shell types — the root entry still re-exports the thread-chat components; `./agent-chat` takes the same components standalone, without `BasaltProvider`, the shell, the dashboard composites, or `./connectivity` |
|
|
225
|
-
| `./charts` | **free** | visx
|
|
225
|
+
| `./charts` | **free** | `CartesianChart` + visx primitives, kinds, sparklines, hooks, token re-exports |
|
|
226
226
|
| `./tokens` | **free** | `VX` token refs, `buildPaletteCss`, `defineSeries`, `seriesTokens`, `groupTokens`, `alpha`, `ColorPair` / `SeriesMap` types |
|
|
227
227
|
| `./theme-lab` | coupled | `ThemeLabControls`, `applyOverrides`, `loadOverrides`, `COLOR_GROUPS` for live theme inspection |
|
|
228
228
|
| `./vite` | — | `basaltViteConfig(opts)` — Vite preset for basalt-ui consumer apps; `basaltAppPlugin(opts)` — PWA head, manifest, and icon metadata derived from the token palette |
|
|
@@ -331,10 +331,46 @@ bun add @visx/axis @visx/curve @visx/event @visx/grid @visx/group @visx/responsi
|
|
|
331
331
|
dependency-free: it needs the nine `@visx/*` packages, all pinned exact at `4.0.0`. They ship as
|
|
332
332
|
optional peers, not bundled dependencies, so a `./tokens`-only consumer never installs them.
|
|
333
333
|
|
|
334
|
+
`CartesianChart` is the entry point: it owns the measured margins, both y scales and their
|
|
335
|
+
domains, the axes, grid, the page-shared cursor, the crosshair and the derived tooltip, so a chart
|
|
336
|
+
supplies its `series` and draws only marks. Legends toggle series, margins size themselves from
|
|
337
|
+
the tick labels actually painted, and charts share a cursor with no provider.
|
|
338
|
+
|
|
334
339
|
```tsx
|
|
335
|
-
import {
|
|
340
|
+
import { CartesianChart, LinePath, VX } from 'basalt-ui/charts'
|
|
341
|
+
;<CartesianChart
|
|
342
|
+
data={rows}
|
|
343
|
+
chartId="sessions"
|
|
344
|
+
getX={(d) => d.date}
|
|
345
|
+
series={[
|
|
346
|
+
{
|
|
347
|
+
key: 'sessions',
|
|
348
|
+
label: 'Sessions',
|
|
349
|
+
color: VX.accent,
|
|
350
|
+
mark: 'line',
|
|
351
|
+
getValue: (d) => d.sessions,
|
|
352
|
+
},
|
|
353
|
+
]}
|
|
354
|
+
y={{ format: (v) => v.toFixed(0) }}
|
|
355
|
+
height={260}
|
|
356
|
+
>
|
|
357
|
+
{({ visible, xScale, yScale }) =>
|
|
358
|
+
visible.map((s) => (
|
|
359
|
+
<LinePath
|
|
360
|
+
key={s.key}
|
|
361
|
+
data={rows}
|
|
362
|
+
x={(d) => xScale(d.date) ?? 0}
|
|
363
|
+
y={(d) => yScale(s.getValue(d) ?? 0)}
|
|
364
|
+
stroke={s.color}
|
|
365
|
+
/>
|
|
366
|
+
))
|
|
367
|
+
}
|
|
368
|
+
</CartesianChart>
|
|
336
369
|
```
|
|
337
370
|
|
|
371
|
+
Shipped kinds (`Bars`, `MultiLine`, `StackedArea`, `ZonedLine`, `Donut`, `DualPanel`, `Heatmap`)
|
|
372
|
+
are that same composition, pre-wired.
|
|
373
|
+
|
|
338
374
|
### `./query` — TanStack Query adapter
|
|
339
375
|
|
|
340
376
|
```bash
|
|
@@ -11,8 +11,11 @@ paths:
|
|
|
11
11
|
|
|
12
12
|
basalt-ui owns the chart doctrine. Charts are built from low-level [visx](https://airbnb.io/visx)
|
|
13
13
|
primitives so we can build exactly the chart we want; the trade-off is that every chart duplicates
|
|
14
|
-
structure unless we compose shared building blocks.
|
|
15
|
-
contract, not optional polish
|
|
14
|
+
structure unless we compose shared building blocks. **`CartesianChart` plus a small kind set are
|
|
15
|
+
the contract, not optional polish** — composing `CartesianChart` (single-plot) or `ChartFrame`
|
|
16
|
+
directly (multi-pane/radial/matrix) is mandatory for every chart, mechanically enforced by
|
|
17
|
+
`basalt/hand-rolled-plot` (see "Mechanical enforcement" below). `docs/CHARTS-SPEC.md` is the ground
|
|
18
|
+
truth this doc reflects.
|
|
16
19
|
|
|
17
20
|
## The boundary (lint-enforced)
|
|
18
21
|
|
|
@@ -25,7 +28,7 @@ downstream apps too:
|
|
|
25
28
|
`AreaClosed`, `scaleLinear`, `curveMonotoneX`, …) — keep the dependency declared in one place, and
|
|
26
29
|
keep your own bespoke chart file under a `charts/` directory so it stays on the right side of the
|
|
27
30
|
boundary.
|
|
28
|
-
- **`@visx/tooltip` is banned everywhere** (`basalt/visx-tooltip`). Use `
|
|
31
|
+
- **`@visx/tooltip` is banned everywhere** (`basalt/visx-tooltip`). Use `ChartTooltipFloat` +
|
|
29
32
|
`TooltipHeader`/`TooltipRow`/`TooltipBody` from `basalt-ui/charts`.
|
|
30
33
|
|
|
31
34
|
`basalt-ui/charts` is itself Mantine-free (`basalt/token-layer-boundary`, enforced only inside
|
|
@@ -44,62 +47,168 @@ either — both remedies point at a Mantine layout primitive (`Flex`/`Grid`/`Gro
|
|
|
44
47
|
Mantine-free boundary above already forbids there, so the finding would be unactionable. Reach for
|
|
45
48
|
`ChartCenter` (below, under "Pending state") when a chart file needs to center something.
|
|
46
49
|
|
|
50
|
+
## The contract is mandatory (lint-enforced)
|
|
51
|
+
|
|
52
|
+
Composing `CartesianChart` for a single-plot chart, or `ChartFrame` directly for a multi-pane/
|
|
53
|
+
radial/matrix shape, is not optional — two `basalt` oxlint plugin rules
|
|
54
|
+
(`packages/basalt-ui/configs/oxlint-plugin.js`) make it a lint failure to bypass:
|
|
55
|
+
|
|
56
|
+
- **`basalt/hand-rolled-plot`** — rendering a chart-assembly primitive (`AxisLeftNumeric`,
|
|
57
|
+
`AxisRightNumeric`, `AxisBottomDate`, `HoverOverlay`, `Crosshair`) in a file that does not
|
|
58
|
+
compose `CartesianChart` fails the build. Escape: a `theme-allow` comment on the first such
|
|
59
|
+
site — that is how a genuinely non-single-plot shape (multi-pane, radial, matrix) declares
|
|
60
|
+
itself; `DualPanel` carries the repo's one such comment. The file that DEFINES `CartesianChart`
|
|
61
|
+
is exempt definitionally (detected by declaration, not by path), since a rule saying "compose X"
|
|
62
|
+
cannot fire inside X.
|
|
63
|
+
- **`basalt/chart-legend-literal`** — passing a hand-written array literal to `ChartLegend`'s
|
|
64
|
+
`items` fails the build; the legend must be derived from the same `series` array the chart draws
|
|
65
|
+
(`deriveLegend`, or just let `ChartFrame`/`CartesianChart` do it), so it cannot go stale and keep
|
|
66
|
+
naming a series the plot no longer draws.
|
|
67
|
+
|
|
68
|
+
Both ship at `warn` in the consumer preset (`configs/oxlint.json`) for one minor and `error`
|
|
69
|
+
repo-local, per the "Shipping a stricter guard — the grace minor" doctrine in
|
|
70
|
+
`packages/basalt-ui/CLAUDE.md`. They promote to `error` in the next minor.
|
|
71
|
+
|
|
72
|
+
## Maps are not charts
|
|
73
|
+
|
|
74
|
+
Neither `@visx/geo` nor any map library is among the 9 exact-pinned `@visx/*` peers, and basalt
|
|
75
|
+
ships no map kind.
|
|
76
|
+
|
|
77
|
+
- **Maps are consumer territory.** A map is a MapLibre/Leaflet/deck.gl component in the consumer's
|
|
78
|
+
own tree, not a visx chart — build it wherever the rest of that consumer's non-chart UI lives.
|
|
79
|
+
- **The `basalt/visx-boundary` rule doesn't apply.** It constrains `@visx/*` imports only. A map
|
|
80
|
+
library is not `@visx/*`, so a map component needs no `charts/` path segment and no exemption.
|
|
81
|
+
- **Want `@visx/geo` anyway?** Install it yourself — basalt doesn't pin it — and keep it under a
|
|
82
|
+
`charts/` segment like every other visx import, same as any bespoke chart.
|
|
83
|
+
- **Style map chrome with `--vx-*` tokens** (`VX.*`/`alpha()`) so it matches the rest of the app.
|
|
84
|
+
- **The guard trap:** `check-theme`'s `inline-spacing` kind is a per-line regex over raw source, not
|
|
85
|
+
an AST pass — it flags any `padding:`/`gap:`/`margin:` followed by a number in _any_ object
|
|
86
|
+
literal, in any file. A map's pixel geometry (`fitBounds({ padding: 48 })`, marker offsets) trips
|
|
87
|
+
it even though it isn't spacing, and hoisting the value to a module-scope const doesn't help — the
|
|
88
|
+
regex sees the line, not the binding. Use a `theme-allow` line comment; that's the sanctioned
|
|
89
|
+
answer, not a bug to file.
|
|
90
|
+
|
|
47
91
|
## Every chart has
|
|
48
92
|
|
|
49
|
-
1.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
— and never paint; one consumer shipped eight authored tooltip rows no one had ever seen. The
|
|
57
|
-
portal also un-breaks `position: fixed` under a transformed ancestor.
|
|
58
|
-
4. **AxisLeftNumeric** / **AxisRightNumeric** + **AxisBottomDate** — never raw `<AxisLeft>`/`<AxisBottom>`/`<AxisRight>`
|
|
59
|
-
(they miss theme tokens + smart ticks). Enforced by `basalt-ui check-theme` (`raw-visx-axis` guard
|
|
60
|
-
fails the build on a raw axis in a `/charts/` file; escape via `theme-allow`), not just convention.
|
|
61
|
-
`AxisBottomDate` takes an optional `tickFormat` (`TickFormatter<string>`, defaults to
|
|
62
|
-
`fmtAxisDate` — `DD.MM`) — the only supported exit for a sub-day window, where the default
|
|
63
|
-
collapses every tick to the same label and a raw `<AxisBottom>` isn't an option.
|
|
64
|
-
5. **HoverOverlay** for pointer capture (mouse + touch + pen — `touch-action: pan-y` lets vertical
|
|
65
|
-
page scroll pass through while a horizontal drag scrubs the chart), **HoverContext** for
|
|
66
|
-
cross-chart crosshair sync, **useChartTooltip** for tip state, **useHoverSync** for the shared
|
|
67
|
-
cursor. Wrap a group of date-aligned charts in **ChartHoverSync** (from `basalt-ui/charts`) to
|
|
68
|
-
activate cross-chart sync — hovering one chart casts a ghost crosshair on all siblings; without
|
|
69
|
-
it `useHoverSync` runs per-chart only and logs a dev warning:
|
|
93
|
+
1. **`CartesianChart`** — the primitive every single-plot cartesian chart composes, and composes
|
|
94
|
+
NOTHING else from this list by hand. `ZonedLine`, `Bars`, `StackedArea`, and `MultiLine` all
|
|
95
|
+
compose it; a bespoke single-plot chart should too. It owns measured margins, both y scales +
|
|
96
|
+
domains, the x scale + tick thinning, grid, zones, axes, the shared cursor, the crosshair + its
|
|
97
|
+
per-series dots, the hover/keyboard overlay, and the derived tooltip. The caller supplies
|
|
98
|
+
`series` (the single source of truth — see #3) and a `children` render prop that draws ONLY
|
|
99
|
+
marks:
|
|
70
100
|
|
|
71
101
|
```tsx
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
102
|
+
<CartesianChart
|
|
103
|
+
data={data} chartId="sessions" getX={(d) => d.date} series={SERIES}
|
|
104
|
+
y={{ domain: 'auto', format: fmtInt }}
|
|
105
|
+
height={260}
|
|
106
|
+
>
|
|
107
|
+
{({ xScale, yScale, visible }) => visible.map((s) => <LinePath key={s.key} … />)}
|
|
108
|
+
</CartesianChart>
|
|
76
109
|
```
|
|
77
110
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
111
|
+
Reaching past it for an axis, a tooltip, or a margin means the chart has drifted from every
|
|
112
|
+
other chart. `DualPanel`, `Heatmap`, and `Donut` are the deliberate exception, by shape: they
|
|
113
|
+
compose `ChartFrame` + `useChartCursor` + `autoMargin` + `ChartTooltipFloat` directly instead —
|
|
114
|
+
same machinery, different assembly, because their contract isn't a single plot rect with one or
|
|
115
|
+
two numeric y axes.
|
|
116
|
+
|
|
117
|
+
2. **ChartCard** wrapper — never a raw `<Card>`. Gives title + info-tooltip + extra slot, consistent
|
|
118
|
+
margin.
|
|
119
|
+
3. **`series` is the single source of truth.** Legend entries and tooltip rows are DERIVED from it
|
|
120
|
+
(`deriveLegend` / `deriveTooltipRows`) — a chart cannot show a row or a legend key it doesn't
|
|
121
|
+
draw. Never hand-author a legend or a tooltip row in parallel. Corollary for mark renderers: draw
|
|
122
|
+
`ctx.visible` (the `PlotContext` field), never the `series` prop directly — `visible` is `series`
|
|
123
|
+
minus whatever the legend has toggled off, so drawing `series` would repaint a mark the reader
|
|
124
|
+
just hid.
|
|
125
|
+
4. **`ChartTooltipFloat`** + `TooltipHeader` + `TooltipRow` + `TooltipBody` — never `@visx/tooltip`
|
|
126
|
+
directly. `CartesianChart` wires this internally for a single-plot chart; a non-cartesian shape
|
|
127
|
+
composes it directly (see #1). Portals to `document.body` (SSR-guarded: renders nothing when
|
|
128
|
+
`document` is undefined), so it can be authored anywhere in the tree, including inside an
|
|
129
|
+
`<svg>`. A plain `<div>` there mounts in the SVG namespace — accepting every prop,
|
|
130
|
+
throwing nothing, typechecking, passing lint — and never paint; one consumer shipped eight
|
|
131
|
+
authored tooltip rows no one had ever seen. The portal also un-breaks `position: fixed` under a
|
|
132
|
+
transformed ancestor.
|
|
133
|
+
5. **AxisLeftNumeric** / **AxisRightNumeric** + **AxisBottomDate** — never raw
|
|
134
|
+
`<AxisLeft>`/`<AxisBottom>`/`<AxisRight>` (they miss theme tokens + smart ticks). Enforced by
|
|
135
|
+
`basalt-ui check-theme` (`raw-visx-axis` guard fails the build on a raw axis in a `/charts/`
|
|
136
|
+
file; escape via `theme-allow`), not just convention. `AxisBottomDate` takes an optional
|
|
137
|
+
`tickFormat` (`TickFormatter<string>`, defaults to `fmtAxisDate` — `DD.MM`) — the only supported
|
|
138
|
+
exit for a sub-day window, where the default collapses every tick to the same label and a raw
|
|
139
|
+
`<AxisBottom>` isn't an option. `CartesianChart`'s own `formatX` prop is the higher-level way to
|
|
140
|
+
set this without touching the axis component at all.
|
|
141
|
+
6. **The cursor is shared by default.** No provider needed — `useChartCursor` reads a module-level
|
|
142
|
+
external store (`useSyncExternalStore`), so every `CartesianChart`/`ChartFrame`-composed chart on
|
|
143
|
+
the page shares one cursor out of the box. `ChartCursorScope` **isolates** a subtree onto a
|
|
144
|
+
private store instead: it opts a subtree OUT of sharing, never into it. Reach for it only when a
|
|
145
|
+
group must not follow the rest of the page.
|
|
84
146
|
|
|
85
147
|
```tsx
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
xScale,
|
|
91
|
-
marginLeft,
|
|
92
|
-
// ~288 daily buckets folded into ~97 columns at this width — map an unfolded sibling's key
|
|
93
|
-
// onto the folded bucket that swallowed it.
|
|
94
|
-
resolveKey: (key) => pointByKey.get(foldKey(key)) ?? null,
|
|
95
|
-
})
|
|
148
|
+
<ChartCursorScope>
|
|
149
|
+
<ChartCard>…</ChartCard>
|
|
150
|
+
<ChartCard>…</ChartCard>
|
|
151
|
+
</ChartCursorScope>
|
|
96
152
|
```
|
|
97
153
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
154
|
+
**Resolution is domain-aware, not string-equal**, which is what makes sharing safe as a default.
|
|
155
|
+
Exact-string matching would desync any chart that folds or downsamples its own domain: it stops
|
|
156
|
+
owning most of the keys its siblings broadcast, so the shared crosshair lands on some hovers and
|
|
157
|
+
not others, with no rule a reader can infer — worse than no shared cursor at all.
|
|
158
|
+
`useChartCursor` resolves a broadcast key against a chart's own points by, in order: exact
|
|
159
|
+
match → nearest parsed-numeric/date match within the chart's own step → `null`. A chart that folds its calendar
|
|
160
|
+
into weekly buckets still tracks a hover from a sibling plotting daily points, with no extra
|
|
161
|
+
wiring — `resolveKey` no longer exists because there is nothing left for it to patch.
|
|
162
|
+
|
|
163
|
+
7. **Keyboard-operable by construction.** The hover overlay `CartesianChart` renders is focusable;
|
|
164
|
+
`←`/`→` scrub the shared cursor one point at a time, `Escape` clears it. The tooltip is
|
|
165
|
+
`aria-live="polite"`. This comes for free from composing `CartesianChart`/`ChartFrame` — nothing
|
|
166
|
+
to wire per chart.
|
|
167
|
+
8. **Theme-aware colors** via `VX.*` tokens + `alpha()`. **Never** a raw hex literal in a chart file.
|
|
168
|
+
**Never** `localStorage.getItem('theme')` — the scheme resolves via CSS vars (see Dark/light
|
|
169
|
+
below).
|
|
170
|
+
|
|
171
|
+
**Exemption:** sparklines (`LineSparkline`, `BarSparkline` — tiny inline charts with no
|
|
172
|
+
legend/tooltip) don't have to compose ChartCard/`CartesianChart`'s legend/tooltip — but still use
|
|
173
|
+
`VX.*` tokens.
|
|
174
|
+
|
|
175
|
+
## Legend interaction
|
|
176
|
+
|
|
177
|
+
At ≥2 entries, the derived legend is a toggle by default: clicking an entry hides that series from
|
|
178
|
+
the plot, the tooltip, AND the auto y-domain, together — a stacked chart's axis actually shrinks
|
|
179
|
+
when a band is hidden, it doesn't just leave a dead gap. `legend={{ toggle: false }}` opts a chart
|
|
180
|
+
out. A single-entry legend never toggles (hiding the only series a chart draws is never useful).
|
|
181
|
+
This is exactly why marks must draw `ctx.visible` and never `series` (see "Every chart has" #3) —
|
|
182
|
+
`visible` is what actually reflects a hidden key; the raw `series` prop never changes.
|
|
183
|
+
|
|
184
|
+
## Tooltip config
|
|
185
|
+
|
|
186
|
+
`CartesianChart`'s `tooltip` prop (`CartesianTooltipConfig<T>`, or `false` to disable the tooltip
|
|
187
|
+
and its crosshair dots entirely):
|
|
188
|
+
|
|
189
|
+
- `label?: (d: T) => { text; color } | null` — a right-aligned badge in the header (a status label,
|
|
190
|
+
a zone name).
|
|
191
|
+
- `prependRows?: (d: T, ctx: { visible; hidden }) => ReactNode` — rows rendered BEFORE the derived
|
|
192
|
+
per-series rows (a total, a context line). `ctx` is the same `visible`/`hidden` the plot itself
|
|
193
|
+
draws from, so a hand-authored row tracks legend toggling instead of desyncing from it.
|
|
194
|
+
- `extraRows?: (d: T, ctx: { visible; hidden }) => ReactNode` — rows appended after the derived
|
|
195
|
+
rows. Same `ctx` as `prependRows`.
|
|
196
|
+
- `follow?: boolean` — default `true`, the tooltip tracks the pointer. `follow: false` anchors it to
|
|
197
|
+
the crosshair at the plot's top edge instead, which reads better across a column of charts sharing
|
|
198
|
+
one cursor (every tooltip lines up on the same x). Anchoring costs one `getBoundingClientRect` per
|
|
199
|
+
hovered frame, which is why following is the default.
|
|
200
|
+
|
|
201
|
+
Per-series, `SeriesStyle.tooltip: false` means "draw the mark and legend it, but never give it a
|
|
202
|
+
tooltip row" — it lives on the series, not on the kind. `ChartSeries.formatValue` is
|
|
203
|
+
`(v: number, d: T) => string` — a row can cite the hovered datum, not just the plotted number (e.g.
|
|
204
|
+
`97.5 kg (92.5 × 3)`). `Bars` has its own per-key version of the opt-out: `BarsBar.tooltip: false` /
|
|
205
|
+
`BarsLine.tooltip: false` draws and legends a bar series without ever listing it as a tooltip row.
|
|
206
|
+
|
|
207
|
+
A **stacked** chart needs `cursorValue` too: `CartesianChart`'s crosshair dot defaults to
|
|
208
|
+
`series.getValue(point)`, which is correct for an unstacked line but puts a stacked band's dot at
|
|
209
|
+
its own raw value — somewhere inside the fill, not on the edge the reader is tracking. Pass
|
|
210
|
+
`cursorValue={(point, series, visible) => …}` to place it at the cumulative band top instead;
|
|
211
|
+
`StackedArea` is the reference implementation (`kinds/StackedArea.tsx`).
|
|
103
212
|
|
|
104
213
|
## Pending state (`isPending`)
|
|
105
214
|
|
|
@@ -116,15 +225,18 @@ import { Bars } from 'basalt-ui/charts'
|
|
|
116
225
|
isPending={query.isPending}
|
|
117
226
|
chartId="load"
|
|
118
227
|
getX={(d) => d.date}
|
|
119
|
-
|
|
228
|
+
getValue={(d, key) => d[key]}
|
|
229
|
+
positiveBars={[{ key: 'load', label: 'Load', color: VX.accent }]}
|
|
120
230
|
/>
|
|
121
231
|
```
|
|
122
232
|
|
|
123
|
-
All seven kinds accept `isPending` and forward it to `ChartFrame`
|
|
124
|
-
it
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
233
|
+
All seven kinds accept `isPending` and forward it to `ChartFrame` — `ZonedLine`/`Bars`/
|
|
234
|
+
`StackedArea`/`MultiLine` forward it through `CartesianChart`, which forwards it on to `ChartFrame`
|
|
235
|
+
in turn; `DualPanel`/`Heatmap`/`Donut` compose `ChartFrame` directly and forward it there. Every
|
|
236
|
+
path lands on the same `ChartFrame`, so there's one pending renderer, not seven. `ChartFrame` with
|
|
237
|
+
`isPending` renders `ChartPending` over the plot rect in place of `children`, suppresses the legend
|
|
238
|
+
entirely (a legend naming a "not measured" series with nothing to point at is its own small lie),
|
|
239
|
+
and sets `aria-busy="true"` on the outer container.
|
|
128
240
|
|
|
129
241
|
`ChartPending` reserves the plot's exact footprint and draws **nothing** that could be mistaken for
|
|
130
242
|
a measurement — no axes, no gridlines, no hatching, no marks, no animation (the motion doctrine
|
|
@@ -138,31 +250,88 @@ directly in a bespoke chart file the same way; it's deliberately minimal (`width
|
|
|
138
250
|
|
|
139
251
|
## Kinds — the recurring shapes
|
|
140
252
|
|
|
141
|
-
basalt-ui ships these kinds (declarative props, generic over your point type via `getX
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- **`
|
|
145
|
-
|
|
146
|
-
- **`
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
|
|
253
|
+
basalt-ui ships these kinds (declarative props, generic over your point type via `getX` and
|
|
254
|
+
per-series `getValue` accessors on `ChartSeries<T>`):
|
|
255
|
+
|
|
256
|
+
- **`ZonedLine`** — a single-series line with zone bands, thresholds, x-zones, and reference lines.
|
|
257
|
+
Composes `CartesianChart`.
|
|
258
|
+
- **`Bars`** — 1+ stacked positive/negative bar series, optional line overlays, zones, ref lines,
|
|
259
|
+
dual-axis config. Composes `CartesianChart`.
|
|
260
|
+
- **`StackedArea`** — opaque stacked bands, auto cumulative-top y-domain. Composes `CartesianChart`.
|
|
261
|
+
- **`MultiLine`** — N series on a shared y-axis (or two, via `y2`): legend-hover dimming, per-series
|
|
262
|
+
synced dots, dashed companion (MA) lines, per-point markers (PR stars / status dots), zones +
|
|
263
|
+
refLines, fixed or auto domain (covers z-score/σ via a fixed symmetric domain + zero refLine).
|
|
264
|
+
Composes `CartesianChart`.
|
|
150
265
|
- **`DualPanel`** — top line-pane + bottom signed-histogram pane sharing one x-scale and one cursor;
|
|
151
|
-
optional fill-between two top lines, zones, refLines
|
|
266
|
+
optional fill-between two top lines, zones, refLines, per-point markers on the top pane
|
|
267
|
+
(`ChartSeries.getMarker`). Composes `ChartFrame` directly (two panes, not one plot rect). The
|
|
268
|
+
bottom pane's tooltip row is `formatBar` (separate from `formatBottom`'s tick labels); its domain
|
|
269
|
+
is configurable via `bottomYDomain`/`bottomMaxAbsFloor`.
|
|
152
270
|
- **`Heatmap`** — category×category intensity grid (`color-mix` alpha), per-cell tooltip, optional
|
|
153
|
-
gradient legend strip.
|
|
271
|
+
gradient legend strip. Composes `ChartFrame` directly; self-measures, no separate responsive
|
|
272
|
+
wrapper.
|
|
273
|
+
- **`Donut`** — proportional donut with an optional center-content overlay. Composes `ChartFrame`
|
|
274
|
+
directly (radial, not cartesian).
|
|
275
|
+
|
|
276
|
+
`ZonedLine` and `MultiLine` also accept `xZones?: XZoneSpec[]` — the vertical counterpart to a
|
|
277
|
+
kind's horizontal zone bands, for marking a time window rather than a value range. Each
|
|
278
|
+
`{ from?, to?, fill, align? }` bound is a `getX` **domain key** (the label string the kind's
|
|
279
|
+
`scalePoint<string>` runs over), not a date or timestamp. An omitted bound is the plot edge; a key
|
|
280
|
+
absent from the scale's domain skips that band entirely rather than clamping to an edge. `align`
|
|
281
|
+
defaults `'center'` (a present bound resolves to the point's own center, today's behaviour);
|
|
282
|
+
`align: 'edge'` widens by half a step at each present bound instead — a two-key band covers both
|
|
283
|
+
terminal slots in full, and `from === to` renders one step wide instead of being skipped as
|
|
284
|
+
degenerate. An edge-aligned bound is clamped into the plot range at the first/last sample.
|
|
154
285
|
|
|
155
286
|
How to add a chart:
|
|
156
287
|
|
|
157
|
-
1. **Fits an existing kind?** Use it. Pass `data`, `
|
|
158
|
-
|
|
288
|
+
1. **Fits an existing kind?** Use it. Pass `data`, `chartId`, `getX`, `series` (or
|
|
289
|
+
`positiveBars`/`getValue` for `Bars`), an `AxisConfig` per axis (`y`/`y2`), and the declarative
|
|
290
|
+
zones/thresholds/refLines arrays.
|
|
159
291
|
2. **Second instance of a new recurring shape?** Extract a kind and migrate both call sites
|
|
160
292
|
(Rule of Three: don't extract on the first, don't wait past the third). Bespoke escape hatches
|
|
161
|
-
(`
|
|
162
|
-
3. **Genuinely unique (e.g. a dual-panel MACD
|
|
163
|
-
|
|
293
|
+
(`tooltip.extraRows`, etc.) are fine but must not grow into god-object configs.
|
|
294
|
+
3. **Genuinely unique (e.g. a dual-panel MACD, or a dual-axis line pair no kind's config surface
|
|
295
|
+
covers)?** Stay bespoke — compose `CartesianChart` (single-plot) or `ChartFrame` (multi-pane/
|
|
296
|
+
non-cartesian) directly, drawing only marks in the `children` render prop. Keep it in the page's
|
|
297
|
+
chart file, not in a shared kind. See `apps/playground/src/demo/ChartsPage.tsx`'s
|
|
298
|
+
`SessionsRevenueChart` (a dual-axis line pair via `CartesianChart` + `y`/`y2`) and
|
|
299
|
+
`ChannelVolumeChart` (a role-grouped, `maxRows`-capped legend with a threshold-as-legend-entry
|
|
300
|
+
trick) for two real bespoke compositions.
|
|
301
|
+
|
|
302
|
+
**Anti-pattern:** a single `<Chart type="..." config={...} />` that switches by kind. Prefer N small
|
|
303
|
+
kinds.
|
|
304
|
+
|
|
305
|
+
## Axis config (`AxisConfig<T>`)
|
|
306
|
+
|
|
307
|
+
One object per axis on `CartesianChart` and every kind that composes it — this collapses the
|
|
308
|
+
`yDomain` / `yAutoMaxFloor` / `yAutoMinCeil` / `yAutoPad` / `numTicksY` /
|
|
309
|
+
`formatYTick` prop soup:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
y={{
|
|
313
|
+
domain: 'auto', // 'auto' (default) | [min, max] | (data, visible) => [min, max]
|
|
314
|
+
autoMaxFloor: 100, // when 'auto': upper bound is at least this
|
|
315
|
+
autoMinCeil: 0, // when 'auto': lower bound is at most this (Infinity = pad from raw min)
|
|
316
|
+
autoPad: 1.1, // when 'auto': padding multiplier away from the data
|
|
317
|
+
ticks: 5,
|
|
318
|
+
format: (v) => `${v}%`,
|
|
319
|
+
grid: true, // horizontal grid rules; default on for `y`, off for `y2`
|
|
320
|
+
nice: false, // default false — opt in to d3's scale.nice() domain rounding
|
|
321
|
+
}}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`nice` defaults `false` deliberately: flipping the default would move the domain of every
|
|
325
|
+
already-migrated chart, so it's opt-in per axis.
|
|
164
326
|
|
|
165
|
-
**
|
|
327
|
+
**Passing `y2` is what makes a chart dual-axis** — it draws the right axis and widens the right
|
|
328
|
+
margin by measurement; nothing else flips it on. A series opts into that axis with `axis: 'right'`
|
|
329
|
+
on its `ChartSeries` entry (`SeriesStyle.axis`, default `'left'`).
|
|
330
|
+
|
|
331
|
+
`domain` as a function is the seam a stacked chart uses: it receives `(data, visible)`, so a
|
|
332
|
+
domain summed from the VISIBLE series shrinks correctly when the legend toggles a band off
|
|
333
|
+
(`StackedArea`'s `yConfig` is the reference — summing `props.series` there instead would leave a
|
|
334
|
+
permanent gap above the stack once a band is hidden).
|
|
166
335
|
|
|
167
336
|
## Series color
|
|
168
337
|
|
|
@@ -183,6 +352,15 @@ shifts shade across schemes — that's why each entry is a `{ light, dark }` pai
|
|
|
183
352
|
**Adding a new color** means adding a pair to your series map and rebuilding the palette CSS — never
|
|
184
353
|
inline a hex in a chart.
|
|
185
354
|
|
|
355
|
+
A series that's invisible in the plot (flat at the domain floor, filtered to zero) can still carry
|
|
356
|
+
an explanation: `SeriesStyle.note` (and so `LegendEntry.note`, via `deriveLegend`) renders a short
|
|
357
|
+
muted qualifier after the legend label — e.g. _"Low cloud — 0% all night"_. Keep it to a clause.
|
|
358
|
+
|
|
359
|
+
`SeriesStyle.strokeOpacity` dims the plotted stroke (e.g. a faint moving-average companion) — the
|
|
360
|
+
legend swatch honors it too, parity with `fillOpacity`. The tooltip-row swatch and the crosshair
|
|
361
|
+
dot deliberately do NOT honor it: a sub-1 opacity on a 12px value-readout chip reads as a rendering
|
|
362
|
+
bug, not as data.
|
|
363
|
+
|
|
186
364
|
## Dark/light mode
|
|
187
365
|
|
|
188
366
|
Theme reactivity is **pure CSS**: the `--vx-*` variables are redeclared under the light/dark color
|
|
@@ -198,51 +376,44 @@ clutter). Keep stacked-area bands opaque — fading them leaks lower bands.
|
|
|
198
376
|
|
|
199
377
|
## Responsive sizing
|
|
200
378
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`
|
|
379
|
+
There is exactly one responsive path now — `ChartFrame` (composed either directly, or via
|
|
380
|
+
`CartesianChart`, which composes it internally). `Heatmap` self-measures
|
|
381
|
+
the same way every other kind does, instead of taking `width`/`height` from a separate wrapper.
|
|
382
|
+
Every kind and `CartesianChart` itself resolve their size the same three ways, tried in this order:
|
|
205
383
|
|
|
206
384
|
```tsx
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<ResponsiveChart height={320}>
|
|
211
|
-
{({ width, height }) => (
|
|
212
|
-
<Bars width={width} height={height} data={data} chartId="load" getX={…} getY={…} />
|
|
213
|
-
)}
|
|
214
|
-
</ResponsiveChart>
|
|
215
|
-
|
|
216
|
-
// aspectRatio variant: height = width / ratio
|
|
217
|
-
<ResponsiveChart aspectRatio={16 / 9}>
|
|
218
|
-
{({ width, height }) => <MyChart width={width} height={height} />}
|
|
219
|
-
</ResponsiveChart>
|
|
385
|
+
<Bars height={260} … /> // fixed height (most common)
|
|
386
|
+
<Bars aspectRatio={16 / 9} … /> // height = round(measured width / ratio)
|
|
387
|
+
<Bars fill … /> // fills the parent flex/grid cell's measured height
|
|
220
388
|
```
|
|
221
389
|
|
|
222
|
-
|
|
390
|
+
Reach for `useChartSize` directly only when composing `ChartFrame`/`CartesianChart` yourself in a
|
|
391
|
+
bespoke chart and you need the measured `{ width, height }` for something beyond what those
|
|
392
|
+
primitives already do with it — `ChartFrame` already calls it internally for you in the common case.
|
|
393
|
+
`UseChartSizeResult`: `{ ref, width, height }`; attach `ref` to the container you want measured.
|
|
223
394
|
|
|
224
|
-
|
|
225
|
-
import { useChartSize } from 'basalt-ui/charts'
|
|
226
|
-
|
|
227
|
-
const { ref, width, height } = useChartSize()
|
|
228
|
-
// attach ref to your container div; width/height update via ResizeObserver
|
|
229
|
-
```
|
|
395
|
+
## Margin
|
|
230
396
|
|
|
231
|
-
|
|
232
|
-
`
|
|
397
|
+
Margins are **measured**, not hand-picked. `CartesianChart` runs every configured axis's tick
|
|
398
|
+
labels through `autoMargin`/`measureText` (`basalt-ui/charts`) before laying out the plot, so
|
|
399
|
+
`VX.margin` is a **floor**, not the value — a wide tick label (a long right-axis dollar format, a
|
|
400
|
+
rotated x label) widens its own gutter automatically instead of clipping or needing a hand-tuned
|
|
401
|
+
margin. Passing `y2` to get a dual-axis chart no longer needs a manual margin nudge at all:
|
|
233
402
|
|
|
234
|
-
|
|
403
|
+
```tsx
|
|
404
|
+
// now: nothing — passing y2 is what widens the right margin, measured from what's actually painted
|
|
405
|
+
<CartesianChart data={data} chartId="x" getX={…} series={SERIES} y={{…}} y2={{…}} />
|
|
406
|
+
```
|
|
235
407
|
|
|
236
|
-
|
|
237
|
-
|
|
408
|
+
The escape hatch is `margin={{ left: n }}` (`Partial<ChartMargin>`) on `CartesianChart` — a per-side
|
|
409
|
+
override applied LAST, after measurement, so it always wins.
|
|
238
410
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
indicator) — drop the manual slice and let `maxRows` own it.
|
|
411
|
+
`chartMargin(opts?)` (`basalt-ui/tokens`, also re-exported from `basalt-ui/charts`) still exists,
|
|
412
|
+
but only matters for a bespoke chart that composes `ChartFrame` directly (not `CartesianChart`) and
|
|
413
|
+
needs a right-axis-sized margin without running its own measurement pass — `DualPanel` and `Heatmap`
|
|
414
|
+
don't use it today (neither renders a measured dual numeric axis), so treat it as a fallback, not the
|
|
415
|
+
default path. It returns `VX.margin` widened for `{ rightAxis: true }`, or any side overridden
|
|
416
|
+
directly; returns a new object per call, so memoize it at the call site if you do reach for it.
|
|
246
417
|
|
|
247
418
|
## Rule of thumb
|
|
248
419
|
|
|
@@ -114,8 +114,10 @@ Route components import chart primitives directly from `basalt-ui/charts` — ne
|
|
|
114
114
|
as dead code against the floor.
|
|
115
115
|
- **DatePickerInput**: from `@mantine/dates` (if you separately install it). In v9 it uses string
|
|
116
116
|
values (`YYYY-MM-DD`) for `value`/`onChange` — no `Date` conversion.
|
|
117
|
-
- **Responsive chart sizing**:
|
|
118
|
-
|
|
117
|
+
- **Responsive chart sizing**: charts measure themselves — `CartesianChart`/`ChartFrame` do it,
|
|
118
|
+
including reserving the legend band, so pass `height` / `aspectRatio` / `fill` and nothing else
|
|
119
|
+
(see basalt-charts.md). `useChartSize` from `basalt-ui/charts` is for a non-chart box that needs
|
|
120
|
+
measuring. Never `useElementSize` from `@mantine/hooks` in a chart file (Mantine is
|
|
119
121
|
banned inside `charts/**`), and never raw `@visx/responsive` outside `charts/**`.
|
|
120
122
|
|
|
121
123
|
## Strict surfaces & layout primitives
|