basalt-ui 1.17.0 → 1.18.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/CHANGELOG.md +7 -0
- package/agent/rules/basalt-charts.md +12 -0
- package/agent/skills/basalt-design/SKILL.md +2 -2
- package/dist/charts/kinds/DualPanel.d.ts +7 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +10 -3
- package/dist/charts/primitives/CartesianChart.d.ts +17 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +5 -2
- package/dist/charts/primitives/ChartTooltip.d.ts +6 -1
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +2 -1
- package/dist/charts/series.d.ts +5 -0
- package/dist/charts/series.d.ts.map +1 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/kinds/DualPanel.test.tsx +81 -1
- package/src/charts/kinds/DualPanel.tsx +26 -4
- package/src/charts/primitives/CartesianChart.test.tsx +173 -1
- package/src/charts/primitives/CartesianChart.tsx +29 -5
- package/src/charts/primitives/ChartTooltip.test.tsx +22 -1
- package/src/charts/primitives/ChartTooltip.tsx +7 -1
- package/src/charts/series.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.18.0](https://github.com/jkrumm/basalt-ui/compare/v1.17.0...v1.18.0) (2026-08-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* render tooltips on cursor followers, and gate managed-doc drift ([6b46613](https://github.com/jkrumm/basalt-ui/commit/6b466136e1b1dbd88934d339420179768b3e9f8b)), closes [#52](https://github.com/jkrumm/basalt-ui/issues/52)
|
|
7
|
+
|
|
1
8
|
# [1.17.0](https://github.com/jkrumm/basalt-ui/compare/v1.16.0...v1.17.0) (2026-08-18)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -145,6 +145,18 @@ ships no map kind.
|
|
|
145
145
|
onto one domain value, silently dropping one from the plot (see #6 below for why that's the
|
|
146
146
|
cursor's problem too). `Heatmap` is deliberately excluded: its existing `colLabel`/`rowLabel`
|
|
147
147
|
already are that seam, and a second prop over one concern would fork them.
|
|
148
|
+
**The x axis is CATEGORICAL, and this is not inferable from the API.** `CartesianChart` builds
|
|
149
|
+
its x scale as `scalePoint<string>`, so N points are N evenly spaced positions whatever the
|
|
150
|
+
values behind the keys — there is no linear or time x scale. Correct and invisible for a domain
|
|
151
|
+
that is already a regular grid (five-minute buckets ARE evenly spaced); wrong for an event-shaped
|
|
152
|
+
series (speed-test runs, deploys, sessions). Two silent consequences: on a page sharing one
|
|
153
|
+
cursor, event points draw at equal spacing, so the crosshair marks the right point at a different
|
|
154
|
+
screen x than a regularly-sampled sibling — the correlation stays legible in the numbers and not
|
|
155
|
+
in the geometry; and because the domain value must be unique, two events at the same instant
|
|
156
|
+
collapse onto one position and one stops being drawn. `getX` returning a date string reads like a
|
|
157
|
+
time axis and is not one. Tracked as issue #52; a chart whose x is a measured quantity currently
|
|
158
|
+
needs a `basalt/hand-rolled-plot` exemption.
|
|
159
|
+
|
|
148
160
|
6. **The cursor is shared by default.** No provider needed — `useChartCursor` reads a module-level
|
|
149
161
|
external store (`useSyncExternalStore`), so every `CartesianChart`/`ChartFrame`-composed chart on
|
|
150
162
|
the page shares one cursor out of the box. `ChartCursorScope` **isolates** a subtree onto a
|
|
@@ -227,7 +227,7 @@ Every chart composes the shipped primitives — never hand-rolled equivalents:
|
|
|
227
227
|
|
|
228
228
|
- **`ChartCard`** wrapper (title + info-tooltip + extra slot, consistent margin).
|
|
229
229
|
- **`ChartLegend`** (`line | bar | split | splitLine` shapes) — never hand-rolled legend markup.
|
|
230
|
-
- **`
|
|
230
|
+
- **`ChartTooltipFloat`** + `TooltipHeader` / `TooltipRow` / `TooltipBody` — never import `@visx/tooltip`
|
|
231
231
|
directly (oxlint bans it in chart files).
|
|
232
232
|
- **`AxisBottomDate`** / **`AxisLeftNumeric`** / **`AxisRightNumeric`** — never raw
|
|
233
233
|
`<AxisLeft>`/`<AxisBottom>` (they miss theme tokens + smart ticks). Now lint-enforced:
|
|
@@ -262,7 +262,7 @@ resolve/render with no `@mantine/*` installed — CI-tested, repo-local only, no
|
|
|
262
262
|
- Every color comes from a token (`VX.*` / palette) — `basalt-ui check-theme` green, zero raw hex.
|
|
263
263
|
- One hue per metric; categorical multi-color only where the data is genuinely categorical.
|
|
264
264
|
- Neutral line/axis/grid; soft `AreaGradient` on plain metric lines, opaque stacked-area bands.
|
|
265
|
-
- Tooltip = `ChartCard` / `
|
|
265
|
+
- Tooltip = `ChartCard` / `ChartTooltipFloat` primitives, one swatch per row — no hand-rolled markup.
|
|
266
266
|
- Looks right in BOTH schemes (toggled and checked: glow on dark, contrast on light).
|
|
267
267
|
- New colors landed as `{ light, dark }` pairs in the palette, not inline.
|
|
268
268
|
- Nothing reads `localStorage.getItem('theme')`.
|
|
@@ -69,6 +69,13 @@ export type DualPanelProps<T> = {
|
|
|
69
69
|
* alongside the hovered datum.
|
|
70
70
|
*/
|
|
71
71
|
formatHeader?: (key: string, d: T) => string;
|
|
72
|
+
/**
|
|
73
|
+
* Render this chart's tooltip when it is a cursor FOLLOWER, not only when it is the SOURCE —
|
|
74
|
+
* same seam and same anchored-positioning behaviour as `CartesianTooltipConfig.onFollow`. Default
|
|
75
|
+
* false (today's behaviour). Only the SOURCE's tooltip is `aria-live`; a follower's is visual
|
|
76
|
+
* only.
|
|
77
|
+
*/
|
|
78
|
+
onFollow?: boolean;
|
|
72
79
|
/** Legend config forwarded to `ChartFrame`; `false` disables the legend (sparkline escape).
|
|
73
80
|
* Default `{ placement: 'bottom' }`. */
|
|
74
81
|
legend?: ChartLegendConfig | false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DualPanel.d.ts","sourceRoot":"","sources":["../../../src/charts/kinds/DualPanel.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAIzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAe,MAAM,WAAW,CAAA;AAK5E,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACtB;gEAC4D;IAC5D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACjC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC;8DAC0D;IAC1D,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACxB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACtC,2EAA2E;IAC3E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACxD,wGAAwG;IACxG,WAAW,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;IAClE,oDAAoD;IACpD,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAA;IACrB,8DAA8D;IAC9D,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IAChC,+FAA+F;IAC/F,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IACnC;;yFAEqF;IACrF,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACvC;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACzC;iGAC6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,kEAAkE;IAClE,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/D;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IAC5C;4CACwC;IACxC,MAAM,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAA;IAClC,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAID;;;;;;;;;;;GAWG;AACH,iBAAS,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,+BAmClD;
|
|
1
|
+
{"version":3,"file":"DualPanel.d.ts","sourceRoot":"","sources":["../../../src/charts/kinds/DualPanel.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAIzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAe,MAAM,WAAW,CAAA;AAK5E,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACtB;gEAC4D;IAC5D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACjC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC;8DAC0D;IAC1D,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACxB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACtC,2EAA2E;IAC3E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACxD,wGAAwG;IACxG,WAAW,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;IAClE,oDAAoD;IACpD,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAA;IACrB,8DAA8D;IAC9D,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IAChC,+FAA+F;IAC/F,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IACnC;;yFAEqF;IACrF,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACvC;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACzC;iGAC6F;IAC7F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,kEAAkE;IAClE,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/D;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;4CACwC;IACxC,MAAM,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAA;IAClC,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAID;;;;;;;;;;;GAWG;AACH,iBAAS,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,+BAmClD;AAggBD;;;GAGG;AACH,eAAO,MAAM,SAAS,EAA2B,OAAO,cAAc,CAAA"}
|
|
@@ -5,7 +5,7 @@ import { Group } from "@visx/group";
|
|
|
5
5
|
import { scaleLinear, scalePoint } from "@visx/scale";
|
|
6
6
|
import { Bar, LinePath } from "@visx/shape";
|
|
7
7
|
import { Threshold } from "@visx/threshold";
|
|
8
|
-
import { memo, useMemo } from "react";
|
|
8
|
+
import { memo, useMemo, useRef } from "react";
|
|
9
9
|
import { AxisBottomDate, AxisLeftNumeric } from "../primitives/Axes.js";
|
|
10
10
|
import {
|
|
11
11
|
ChartTooltipFloat,
|
|
@@ -75,6 +75,7 @@ function DualPanelPlot(props) {
|
|
|
75
75
|
bottomMaxAbsFloor,
|
|
76
76
|
tooltipLabel,
|
|
77
77
|
formatHeader,
|
|
78
|
+
onFollow,
|
|
78
79
|
plot
|
|
79
80
|
} = props;
|
|
80
81
|
const { hidden } = plot;
|
|
@@ -259,11 +260,17 @@ function DualPanelPlot(props) {
|
|
|
259
260
|
}),
|
|
260
261
|
[visibleSeries, lineValid, xScale, topYScale, getX]
|
|
261
262
|
);
|
|
263
|
+
const svgRef = useRef(null);
|
|
262
264
|
const point = cursor.point;
|
|
263
265
|
const sx = point !== null ? xScale(getX(point)) ?? 0 : 0;
|
|
264
266
|
const syncedBar = point !== null ? getBar(point) : null;
|
|
267
|
+
const isFollowerRender = !cursor.isSource && onFollow === true;
|
|
268
|
+
const showTooltip = point !== null && (cursor.isSource || isFollowerRender);
|
|
269
|
+
const anchorX = isFollowerRender && point !== null ? sx : null;
|
|
270
|
+
const svgRect = anchorX === null ? void 0 : svgRef.current?.getBoundingClientRect();
|
|
271
|
+
const tooltipAnchor = svgRect === void 0 || anchorX === null ? cursor.anchor : { x: svgRect.left + margin.left + anchorX, y: svgRect.top + margin.top };
|
|
265
272
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
266
|
-
/* @__PURE__ */ jsxs("svg", { width: plot.width, height: plot.height, children: [
|
|
273
|
+
/* @__PURE__ */ jsxs("svg", { ref: svgRef, width: plot.width, height: plot.height, children: [
|
|
267
274
|
/* @__PURE__ */ jsxs(Group, { left: margin.left, top: margin.top, children: [
|
|
268
275
|
/* @__PURE__ */ jsx(GridRows, { scale: topYScale, width: xMax, stroke: VX.grid, numTicks: 4 }),
|
|
269
276
|
/* @__PURE__ */ jsx(ZoneRects, { zones: topZones, width: xMax, leftScale: topYScale }),
|
|
@@ -392,7 +399,7 @@ function DualPanelPlot(props) {
|
|
|
392
399
|
] }),
|
|
393
400
|
point !== null && /* @__PURE__ */ jsx(Group, { left: margin.left, top: margin.top, children: /* @__PURE__ */ jsx(Crosshair, { x: sx, top: 0, bottom: innerH }) })
|
|
394
401
|
] }),
|
|
395
|
-
|
|
402
|
+
showTooltip && /* @__PURE__ */ jsxs(ChartTooltipFloat, { anchor: tooltipAnchor, ariaLive: cursor.isSource, children: [
|
|
396
403
|
/* @__PURE__ */ jsx(
|
|
397
404
|
TooltipHeader,
|
|
398
405
|
{
|
|
@@ -77,6 +77,23 @@ export type CartesianTooltipConfig<T> = {
|
|
|
77
77
|
prependRows?: (d: T, ctx: CartesianTooltipRowContext<T>) => ReactNode;
|
|
78
78
|
/** Rows appended after the derived per-series rows. Same `ctx` as {@link prependRows}. */
|
|
79
79
|
extraRows?: (d: T, ctx: CartesianTooltipRowContext<T>) => ReactNode;
|
|
80
|
+
/**
|
|
81
|
+
* Render this chart's tooltip when it is a cursor FOLLOWER (the crosshair came from a sibling
|
|
82
|
+
* chart sharing the cursor), not only when this chart is the SOURCE. Default false — today's
|
|
83
|
+
* behaviour, nothing moves. A follower has no pointer under it, so its tooltip is always ANCHORED
|
|
84
|
+
* to the crosshair — the same positioning `follow: false` gives the source — regardless of what
|
|
85
|
+
* `follow` is set to on this chart: there is no other valid position for a chart nobody is
|
|
86
|
+
* hovering. `follow` keeps governing the SOURCE's own tooltip exactly as before; the two options
|
|
87
|
+
* only interact for a follower, where `onFollow` always wins on positioning. Only the SOURCE's
|
|
88
|
+
* tooltip is `aria-live` — followers announce nothing, so a shared-cursor page doesn't fire N
|
|
89
|
+
* live-region updates per pointer move.
|
|
90
|
+
*
|
|
91
|
+
* Cost, since it scales with the page rather than the chart: anchoring reads one
|
|
92
|
+
* `getBoundingClientRect` per hovered frame (the same cost `follow: false` documents), so N
|
|
93
|
+
* opted-in followers pay N of them per frame. That is the reason this is opt-in per chart rather
|
|
94
|
+
* than a page-level switch — turn it on for the charts a reader actually correlates.
|
|
95
|
+
*/
|
|
96
|
+
onFollow?: boolean;
|
|
80
97
|
};
|
|
81
98
|
/** Everything a mark renderer needs. Handed to `CartesianChart`'s child on every render. */
|
|
82
99
|
export type PlotContext<T> = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CartesianChart.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/CartesianChart.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAErD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAIzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAU/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,CAAA;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAA;AAKvD;uFACuF;AACvF,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAC1B;;;;;OAKG;IACH,MAAM,CAAC,EACH,CAAC,MAAM,EAAE,MAAM,CAAC,GAChB,MAAM,GACN,CAAC,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,SAAS,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAClF;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;sDACkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;IAC9B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,CAAA;AAED;;eAEe;AACf,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,OAAO,EAAE,SAAS,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IAClC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,sBAAsB,CAAC,CAAC,IAAI;IACtC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACxD;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IAC5C;;iGAE6F;IAC7F,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,0BAA0B,CAAC,CAAC,CAAC,KAAK,SAAS,CAAA;IACrE,0FAA0F;IAC1F,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,0BAA0B,CAAC,CAAC,CAAC,KAAK,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"CartesianChart.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/CartesianChart.tsx"],"names":[],"mappings":"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"}
|
|
@@ -228,7 +228,10 @@ function CartesianPlot({
|
|
|
228
228
|
[visible, leftFormat, rightFormat]
|
|
229
229
|
);
|
|
230
230
|
const rows = tooltipEnabled && point !== null ? deriveTooltipRows(tooltipSeries, point, leftFormat) : [];
|
|
231
|
-
const
|
|
231
|
+
const isFollowerRender = !cursor.isSource && tooltipCfg?.onFollow === true;
|
|
232
|
+
const showTooltip = tooltipEnabled && point !== null && (cursor.isSource || isFollowerRender);
|
|
233
|
+
const useAnchoredPosition = tooltipCfg?.follow === false || isFollowerRender;
|
|
234
|
+
const anchorX = useAnchoredPosition && point !== null && cursorX !== null ? cursorX : null;
|
|
232
235
|
const svgRect = anchorX === null ? void 0 : svgRef.current?.getBoundingClientRect();
|
|
233
236
|
const tooltipAnchor = svgRect === void 0 || anchorX === null ? cursor.anchor : { x: svgRect.left + margin.left + anchorX, y: svgRect.top + margin.top };
|
|
234
237
|
const ctx = {
|
|
@@ -330,7 +333,7 @@ function CartesianPlot({
|
|
|
330
333
|
}
|
|
331
334
|
)
|
|
332
335
|
] }) }),
|
|
333
|
-
|
|
336
|
+
showTooltip && /* @__PURE__ */ jsxs(ChartTooltipFloat, { anchor: tooltipAnchor, ariaLive: cursor.isSource, children: [
|
|
334
337
|
/* @__PURE__ */ jsx(
|
|
335
338
|
TooltipHeader,
|
|
336
339
|
{
|
|
@@ -32,11 +32,16 @@ export declare function TooltipBody({ children }: {
|
|
|
32
32
|
* The box is measured after mount via `useLayoutEffect`, so the first paint of a given tooltip is
|
|
33
33
|
* hidden rather than misplaced — a tooltip that flashes at the wrong corner reads as a bug.
|
|
34
34
|
*/
|
|
35
|
-
export declare function ChartTooltipFloat({ anchor, children, }: {
|
|
35
|
+
export declare function ChartTooltipFloat({ anchor, ariaLive, children, }: {
|
|
36
36
|
anchor: {
|
|
37
37
|
x: number;
|
|
38
38
|
y: number;
|
|
39
39
|
} | null;
|
|
40
|
+
/** Whether this tooltip announces itself to screen readers on every position change. Default
|
|
41
|
+
* true. Pass false for a cursor FOLLOWER's tooltip — N followers on a shared-cursor page all
|
|
42
|
+
* announcing on every pointer move would make the page unusable with a screen reader; only the
|
|
43
|
+
* cursor SOURCE should be `aria-live`. */
|
|
44
|
+
ariaLive?: boolean;
|
|
40
45
|
children: ReactNode;
|
|
41
46
|
}): import("react").ReactPortal | null;
|
|
42
47
|
//# sourceMappingURL=ChartTooltip.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAuBrD,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,MAAuB,GACxB,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;sGAGkG;IAClG,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,+BAoCA;AAED,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,UAAU,EACV,KAAK,EACL,WAAW,EACX,MAAM,GACP,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,+BA0CA;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEhE;AAOD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACvC,QAAQ,EAAE,SAAS,CAAA;CACpB,sCA4CA"}
|
|
1
|
+
{"version":3,"file":"ChartTooltip.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAuBrD,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAU,EACV,MAAuB,GACxB,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;sGAGkG;IAClG,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC,+BAoCA;AAED,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,UAAU,EACV,KAAK,EACL,WAAW,EACX,MAAM,GACP,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAA;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,+BA0CA;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEhE;AAOD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,QAAe,EACf,QAAQ,GACT,EAAE;IACD,MAAM,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACvC;;;8CAG0C;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;CACpB,sCA4CA"}
|
|
@@ -121,6 +121,7 @@ const TOOLTIP_GAP = 12;
|
|
|
121
121
|
const VIEWPORT_MARGIN = 8;
|
|
122
122
|
function ChartTooltipFloat({
|
|
123
123
|
anchor,
|
|
124
|
+
ariaLive = true,
|
|
124
125
|
children
|
|
125
126
|
}) {
|
|
126
127
|
const ref = useRef(null);
|
|
@@ -151,7 +152,7 @@ function ChartTooltipFloat({
|
|
|
151
152
|
{
|
|
152
153
|
ref,
|
|
153
154
|
role: "tooltip",
|
|
154
|
-
"aria-live": "polite",
|
|
155
|
+
...ariaLive && { "aria-live": "polite" },
|
|
155
156
|
style: { ...TOOLTIP_STYLES, left, top, visibility: box === null ? "hidden" : "visible" },
|
|
156
157
|
children
|
|
157
158
|
}
|
package/dist/charts/series.d.ts
CHANGED
|
@@ -58,6 +58,11 @@ export type SeriesStyle = {
|
|
|
58
58
|
* that lies is the thing the derived-legend rule exists to prevent). The TOOLTIP ROW swatch and
|
|
59
59
|
* the CROSSHAIR DOT deliberately do NOT honor it: those are 12px value-readout chips where a
|
|
60
60
|
* sub-1 opacity reads as a rendering bug, not as data.
|
|
61
|
+
*
|
|
62
|
+
* Applies to STROKES only, so it silently no-ops on a `mark: 'bar'`/`'area'` series — a bar
|
|
63
|
+
* swatch has no stroke to dim. Dim those with `fillOpacity` instead. This bites the
|
|
64
|
+
* zone/reference-legend idiom in particular (`mark: 'bar'`, `getValue: () => null`), where
|
|
65
|
+
* reaching for `strokeOpacity` looks right and does nothing.
|
|
61
66
|
*/
|
|
62
67
|
strokeOpacity?: number;
|
|
63
68
|
/** Which y-axis this series is measured against. Default 'left'. `'right'` is what makes a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"series.d.ts","sourceRoot":"","sources":["../../src/charts/series.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAE3D;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAA;AAE5C,iEAAiE;AACjE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;AAEhD,0CAA0C;AAC1C,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE3C,uCAAuC;AACvC,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAA;AAE3D,6DAA6D;AAC7D,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAA;AAEjE;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,uGAAuG;AACvG,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,UAAU,CAAA;IAChB,uBAAuB;IACvB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB
|
|
1
|
+
{"version":3,"file":"series.d.ts","sourceRoot":"","sources":["../../src/charts/series.ts"],"names":[],"mappings":"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"}
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v1.
|
|
1
|
+
# basalt-ui v1.18.0
|
|
2
2
|
# Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
|
|
3
3
|
#
|
|
4
4
|
# basalt-ui is an opinionated framework for Mantine-based React apps.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
|
|
20
20
|
|
|
21
21
|
Package: basalt-ui
|
|
22
|
-
Version: 1.
|
|
22
|
+
Version: 1.18.0
|
|
23
23
|
|
|
24
24
|
## basalt-ui
|
|
25
25
|
Import: import { ... } from 'basalt-ui'
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `Bars.test.tsx`) — DualPanel has two `HoverOverlay`s sharing one cursor, but only the TOP one is
|
|
7
7
|
* keyboard-focusable, so `getByRole('slider')` is unambiguous.
|
|
8
8
|
*/
|
|
9
|
-
import { fireEvent, render, screen } from '@testing-library/react'
|
|
9
|
+
import { fireEvent, render, screen, within } from '@testing-library/react'
|
|
10
10
|
import { describe, expect, test } from 'bun:test'
|
|
11
11
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
12
12
|
import { ChartCursorScope } from '../cursor/scope'
|
|
@@ -439,3 +439,83 @@ describe('DualPanel — cursorResolution threads through to sibling resolution',
|
|
|
439
439
|
expect(foldedSlider.getAttribute('aria-valuetext')).toBe('08.08')
|
|
440
440
|
})
|
|
441
441
|
})
|
|
442
|
+
|
|
443
|
+
describe('DualPanel — onFollow (follower behaviour parity with CartesianChart)', () => {
|
|
444
|
+
type SourceRow = { date: string; shown: number }
|
|
445
|
+
const sourceRows: SourceRow[] = [{ date: '2026-08-01', shown: 10 }]
|
|
446
|
+
const sourceSeries: ChartSeries<SourceRow>[] = [
|
|
447
|
+
{ key: 'shown', label: 'Shown', color: '#111', mark: 'line', getValue: (d) => d.shown },
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
function renderPair(onFollow?: boolean, followerRows: Row[] = rows) {
|
|
451
|
+
render(
|
|
452
|
+
<ChartCursorScope>
|
|
453
|
+
<CartesianChart<SourceRow>
|
|
454
|
+
data={sourceRows}
|
|
455
|
+
chartId="dp-of-source"
|
|
456
|
+
getX={(d) => d.date}
|
|
457
|
+
series={sourceSeries}
|
|
458
|
+
ariaLabel="Source"
|
|
459
|
+
legend={false}
|
|
460
|
+
>
|
|
461
|
+
{() => null}
|
|
462
|
+
</CartesianChart>
|
|
463
|
+
<DualPanel<Row>
|
|
464
|
+
data={followerRows}
|
|
465
|
+
chartId="dp-of-follower"
|
|
466
|
+
getX={(d) => d.date}
|
|
467
|
+
series={series}
|
|
468
|
+
getBar={(d) => d.bar}
|
|
469
|
+
barLabel="Bar"
|
|
470
|
+
barColorPositive="#0a0"
|
|
471
|
+
barColorNegative="#a00"
|
|
472
|
+
formatTop={(v) => String(v)}
|
|
473
|
+
formatBottom={(v) => v.toFixed(2)}
|
|
474
|
+
ariaLabel="Follower"
|
|
475
|
+
legend={false}
|
|
476
|
+
{...(onFollow !== undefined && { onFollow })}
|
|
477
|
+
/>
|
|
478
|
+
</ChartCursorScope>,
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
test('onFollow: true renders the DualPanel tooltip while a sibling owns the cursor', async () => {
|
|
483
|
+
renderPair(true)
|
|
484
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
485
|
+
|
|
486
|
+
expect(await screen.findByText('V')).toBeTruthy()
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
test('without onFollow (default false), DualPanel renders no tooltip as a follower', async () => {
|
|
490
|
+
renderPair()
|
|
491
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
492
|
+
|
|
493
|
+
// The SOURCE still renders its own tooltip — only the DualPanel follower stays silent.
|
|
494
|
+
expect(await screen.findByText('Shown')).toBeTruthy()
|
|
495
|
+
expect(screen.queryByText('V')).toBeNull()
|
|
496
|
+
expect(screen.queryAllByRole('tooltip')).toHaveLength(1)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
test('the follower tooltip is not aria-live; the source is', async () => {
|
|
500
|
+
renderPair(true)
|
|
501
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
502
|
+
await screen.findByText('V')
|
|
503
|
+
|
|
504
|
+
const tooltips = screen.getAllByRole('tooltip')
|
|
505
|
+
expect(tooltips).toHaveLength(2)
|
|
506
|
+
const sourceTooltip = tooltips.find((t) => within(t).queryByText('Shown') !== null)
|
|
507
|
+
const followerTooltip = tooltips.find((t) => within(t).queryByText('V') !== null)
|
|
508
|
+
expect(sourceTooltip?.getAttribute('aria-live')).toBe('polite')
|
|
509
|
+
expect(followerTooltip?.hasAttribute('aria-live')).toBe(false)
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
test('a follower whose domain never resolves the broadcast key renders no tooltip shell', async () => {
|
|
513
|
+
const foreignRows: Row[] = [{ date: '2027-01-15', v: 99, bar: 0.5 }]
|
|
514
|
+
renderPair(true, foreignRows)
|
|
515
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
516
|
+
|
|
517
|
+
expect(await screen.findByText('Shown')).toBeTruthy()
|
|
518
|
+
expect(screen.queryByText('V')).toBeNull()
|
|
519
|
+
expect(screen.queryAllByRole('tooltip')).toHaveLength(1)
|
|
520
|
+
})
|
|
521
|
+
})
|
|
@@ -4,7 +4,7 @@ import { Group } from '@visx/group'
|
|
|
4
4
|
import { scaleLinear, scalePoint } from '@visx/scale'
|
|
5
5
|
import { Bar, LinePath } from '@visx/shape'
|
|
6
6
|
import { Threshold } from '@visx/threshold'
|
|
7
|
-
import { memo, useMemo } from 'react'
|
|
7
|
+
import { memo, useMemo, useRef } from 'react'
|
|
8
8
|
import type { ReactNode } from 'react'
|
|
9
9
|
import { AxisBottomDate, AxisLeftNumeric } from '../primitives/Axes'
|
|
10
10
|
import {
|
|
@@ -84,6 +84,13 @@ export type DualPanelProps<T> = {
|
|
|
84
84
|
* alongside the hovered datum.
|
|
85
85
|
*/
|
|
86
86
|
formatHeader?: (key: string, d: T) => string
|
|
87
|
+
/**
|
|
88
|
+
* Render this chart's tooltip when it is a cursor FOLLOWER, not only when it is the SOURCE —
|
|
89
|
+
* same seam and same anchored-positioning behaviour as `CartesianTooltipConfig.onFollow`. Default
|
|
90
|
+
* false (today's behaviour). Only the SOURCE's tooltip is `aria-live`; a follower's is visual
|
|
91
|
+
* only.
|
|
92
|
+
*/
|
|
93
|
+
onFollow?: boolean
|
|
87
94
|
/** Legend config forwarded to `ChartFrame`; `false` disables the legend (sparkline escape).
|
|
88
95
|
* Default `{ placement: 'bottom' }`. */
|
|
89
96
|
legend?: ChartLegendConfig | false
|
|
@@ -175,6 +182,7 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
175
182
|
bottomMaxAbsFloor,
|
|
176
183
|
tooltipLabel,
|
|
177
184
|
formatHeader,
|
|
185
|
+
onFollow,
|
|
178
186
|
plot,
|
|
179
187
|
} = props
|
|
180
188
|
|
|
@@ -424,13 +432,27 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
424
432
|
[visibleSeries, lineValid, xScale, topYScale, getX],
|
|
425
433
|
)
|
|
426
434
|
|
|
435
|
+
const svgRef = useRef<SVGSVGElement>(null)
|
|
427
436
|
const point = cursor.point
|
|
428
437
|
const sx = point !== null ? (xScale(getX(point)) ?? 0) : 0
|
|
429
438
|
const syncedBar = point !== null ? getBar(point) : null
|
|
430
439
|
|
|
440
|
+
// Same seam as `CartesianChart`: this chart renders its tooltip as the cursor SOURCE (always) or,
|
|
441
|
+
// opted in via `onFollow`, as a FOLLOWER. A follower has no pointer under it, so it is always
|
|
442
|
+
// positioned via the anchored (crosshair) path — `cursor.anchor` stays null on a chart nobody is
|
|
443
|
+
// hovering, so following it here would render nothing.
|
|
444
|
+
const isFollowerRender = !cursor.isSource && onFollow === true
|
|
445
|
+
const showTooltip = point !== null && (cursor.isSource || isFollowerRender)
|
|
446
|
+
const anchorX = isFollowerRender && point !== null ? sx : null
|
|
447
|
+
const svgRect = anchorX === null ? undefined : svgRef.current?.getBoundingClientRect()
|
|
448
|
+
const tooltipAnchor =
|
|
449
|
+
svgRect === undefined || anchorX === null
|
|
450
|
+
? cursor.anchor
|
|
451
|
+
: { x: svgRect.left + margin.left + anchorX, y: svgRect.top + margin.top }
|
|
452
|
+
|
|
431
453
|
return (
|
|
432
454
|
<>
|
|
433
|
-
<svg width={plot.width} height={plot.height}>
|
|
455
|
+
<svg ref={svgRef} width={plot.width} height={plot.height}>
|
|
434
456
|
{/* Top pane: line series + fill-between + zones + ref lines. */}
|
|
435
457
|
<Group left={margin.left} top={margin.top}>
|
|
436
458
|
<GridRows scale={topYScale} width={xMax} stroke={VX.grid} numTicks={4} />
|
|
@@ -593,8 +615,8 @@ function DualPanelPlot<T>(props: DualPanelPlotProps<T>) {
|
|
|
593
615
|
)}
|
|
594
616
|
</svg>
|
|
595
617
|
|
|
596
|
-
{
|
|
597
|
-
<ChartTooltipFloat anchor={cursor.
|
|
618
|
+
{showTooltip && (
|
|
619
|
+
<ChartTooltipFloat anchor={tooltipAnchor} ariaLive={cursor.isSource}>
|
|
598
620
|
<TooltipHeader
|
|
599
621
|
date={getX(point)}
|
|
600
622
|
{...(formatHeader !== undefined && {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react'
|
|
1
|
+
import { fireEvent, render, screen, within } from '@testing-library/react'
|
|
2
2
|
import { describe, expect, test } from 'bun:test'
|
|
3
3
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
4
4
|
import { ChartCursorScope } from '../cursor/scope'
|
|
@@ -267,3 +267,175 @@ describe('tooltip extraRows — ctx.visible/ctx.hidden track legend toggling', (
|
|
|
267
267
|
expect((await screen.findByTestId('extra-ctx')).textContent).toContain('visible:a|hidden:b')
|
|
268
268
|
})
|
|
269
269
|
})
|
|
270
|
+
|
|
271
|
+
describe('tooltip.onFollow — the follower renders when a SIBLING owns the cursor', () => {
|
|
272
|
+
function renderPair(onFollow?: boolean) {
|
|
273
|
+
render(
|
|
274
|
+
<ChartCursorScope>
|
|
275
|
+
<CartesianChart<Row>
|
|
276
|
+
data={rows}
|
|
277
|
+
chartId="of-source"
|
|
278
|
+
getX={(d) => d.date}
|
|
279
|
+
series={[seriesFor('a')]}
|
|
280
|
+
ariaLabel="Source"
|
|
281
|
+
legend={false}
|
|
282
|
+
>
|
|
283
|
+
{() => null}
|
|
284
|
+
</CartesianChart>
|
|
285
|
+
<CartesianChart<Row>
|
|
286
|
+
data={rows}
|
|
287
|
+
chartId="of-follower"
|
|
288
|
+
getX={(d) => d.date}
|
|
289
|
+
series={[seriesFor('b')]}
|
|
290
|
+
ariaLabel="Follower"
|
|
291
|
+
legend={false}
|
|
292
|
+
tooltip={{ follow: false, ...(onFollow !== undefined && { onFollow }) }}
|
|
293
|
+
>
|
|
294
|
+
{() => null}
|
|
295
|
+
</CartesianChart>
|
|
296
|
+
</ChartCursorScope>,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
test('onFollow: true renders the follower tooltip rows while the source owns the cursor', async () => {
|
|
301
|
+
renderPair(true)
|
|
302
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
303
|
+
|
|
304
|
+
expect(await screen.findByText('b')).toBeTruthy()
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('without onFollow (default false), the follower renders no tooltip at all', async () => {
|
|
308
|
+
renderPair()
|
|
309
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
310
|
+
|
|
311
|
+
// The SOURCE still renders its own tooltip ('a') — only the follower ('b') stays silent.
|
|
312
|
+
expect(await screen.findByText('a')).toBeTruthy()
|
|
313
|
+
expect(screen.queryByText('b')).toBeNull()
|
|
314
|
+
expect(screen.queryAllByRole('tooltip')).toHaveLength(1)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// `follow` is deliberately left at its default (true, pointer-tracking) here — unlike every
|
|
318
|
+
// other case in this file, which hardcodes `follow: false` on the follower. That hardcoding
|
|
319
|
+
// means `useAnchoredPosition`'s FIRST clause (`tooltipCfg?.follow === false`) always already
|
|
320
|
+
// satisfies the anchored branch, so those tests can't tell `|| isFollowerRender` apart from a
|
|
321
|
+
// no-op. With `follow` left at its pointer-tracking default, anchoring can only come from
|
|
322
|
+
// `isFollowerRender` — and a follower's own `cursor.anchor` (set only by pointer/keyboard
|
|
323
|
+
// events ON THIS CHART, never by a sibling's) stays null for the whole test, since only the
|
|
324
|
+
// SOURCE is ever interacted with. So: `isFollowerRender` present → anchored position resolves
|
|
325
|
+
// from the crosshair and the tooltip mounts; `isFollowerRender` absent → the pointer-tracking
|
|
326
|
+
// branch falls through to the follower's own (never-set) `cursor.anchor`, which is null, and
|
|
327
|
+
// `ChartTooltipFloat` renders nothing at all for a null anchor. Presence/absence of the
|
|
328
|
+
// follower tooltip is what's observable here — happy-dom zeroes every `getBoundingClientRect`,
|
|
329
|
+
// so the anchor's actual pixel coordinates can't discriminate the two branches.
|
|
330
|
+
test('onFollow: true with follow left at its pointer-tracking default still anchors to the crosshair', async () => {
|
|
331
|
+
render(
|
|
332
|
+
<ChartCursorScope>
|
|
333
|
+
<CartesianChart<Row>
|
|
334
|
+
data={rows}
|
|
335
|
+
chartId="of-anchor-source"
|
|
336
|
+
getX={(d) => d.date}
|
|
337
|
+
series={[seriesFor('a')]}
|
|
338
|
+
ariaLabel="Source"
|
|
339
|
+
legend={false}
|
|
340
|
+
>
|
|
341
|
+
{() => null}
|
|
342
|
+
</CartesianChart>
|
|
343
|
+
<CartesianChart<Row>
|
|
344
|
+
data={rows}
|
|
345
|
+
chartId="of-anchor-follower"
|
|
346
|
+
getX={(d) => d.date}
|
|
347
|
+
series={[seriesFor('b')]}
|
|
348
|
+
ariaLabel="Follower"
|
|
349
|
+
legend={false}
|
|
350
|
+
tooltip={{ onFollow: true }}
|
|
351
|
+
>
|
|
352
|
+
{() => null}
|
|
353
|
+
</CartesianChart>
|
|
354
|
+
</ChartCursorScope>,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
358
|
+
|
|
359
|
+
expect(await screen.findByText('b')).toBeTruthy()
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
describe('tooltip.onFollow — aria-live stays SOURCE-only', () => {
|
|
364
|
+
test('the source tooltip is aria-live; the follower tooltip is not', async () => {
|
|
365
|
+
render(
|
|
366
|
+
<ChartCursorScope>
|
|
367
|
+
<CartesianChart<Row>
|
|
368
|
+
data={rows}
|
|
369
|
+
chartId="al-source"
|
|
370
|
+
getX={(d) => d.date}
|
|
371
|
+
series={[seriesFor('a')]}
|
|
372
|
+
ariaLabel="Source"
|
|
373
|
+
legend={false}
|
|
374
|
+
>
|
|
375
|
+
{() => null}
|
|
376
|
+
</CartesianChart>
|
|
377
|
+
<CartesianChart<Row>
|
|
378
|
+
data={rows}
|
|
379
|
+
chartId="al-follower"
|
|
380
|
+
getX={(d) => d.date}
|
|
381
|
+
series={[seriesFor('b')]}
|
|
382
|
+
ariaLabel="Follower"
|
|
383
|
+
legend={false}
|
|
384
|
+
tooltip={{ follow: false, onFollow: true }}
|
|
385
|
+
>
|
|
386
|
+
{() => null}
|
|
387
|
+
</CartesianChart>
|
|
388
|
+
</ChartCursorScope>,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
392
|
+
await screen.findByText('b')
|
|
393
|
+
|
|
394
|
+
const tooltips = screen.getAllByRole('tooltip')
|
|
395
|
+
expect(tooltips).toHaveLength(2)
|
|
396
|
+
const sourceTooltip = tooltips.find((t) => within(t).queryByText('a') !== null)
|
|
397
|
+
const followerTooltip = tooltips.find((t) => within(t).queryByText('b') !== null)
|
|
398
|
+
expect(sourceTooltip?.getAttribute('aria-live')).toBe('polite')
|
|
399
|
+
expect(followerTooltip?.hasAttribute('aria-live')).toBe(false)
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
describe('tooltip.onFollow — a follower whose domain never resolves the broadcast key renders nothing', () => {
|
|
404
|
+
test('an unrelated calendar stays silent, not an empty tooltip shell', async () => {
|
|
405
|
+
const foreignRows: Row[] = [{ date: '2027-01-15', a: 99, b: 99 }]
|
|
406
|
+
|
|
407
|
+
render(
|
|
408
|
+
<ChartCursorScope>
|
|
409
|
+
<CartesianChart<Row>
|
|
410
|
+
data={rows}
|
|
411
|
+
chartId="unres-source"
|
|
412
|
+
getX={(d) => d.date}
|
|
413
|
+
series={[seriesFor('a')]}
|
|
414
|
+
ariaLabel="Source"
|
|
415
|
+
legend={false}
|
|
416
|
+
>
|
|
417
|
+
{() => null}
|
|
418
|
+
</CartesianChart>
|
|
419
|
+
<CartesianChart<Row>
|
|
420
|
+
data={foreignRows}
|
|
421
|
+
chartId="unres-follower"
|
|
422
|
+
getX={(d) => d.date}
|
|
423
|
+
series={[seriesFor('b')]}
|
|
424
|
+
ariaLabel="Follower"
|
|
425
|
+
legend={false}
|
|
426
|
+
tooltip={{ follow: false, onFollow: true }}
|
|
427
|
+
>
|
|
428
|
+
{() => null}
|
|
429
|
+
</CartesianChart>
|
|
430
|
+
</ChartCursorScope>,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
fireEvent.keyDown(screen.getByRole('slider', { name: 'Source' }), { key: 'ArrowRight' })
|
|
434
|
+
|
|
435
|
+
// The SOURCE still renders its own tooltip — only the follower with no resolvable point stays
|
|
436
|
+
// silent, rather than mounting an empty tooltip shell.
|
|
437
|
+
expect(await screen.findByText('a')).toBeTruthy()
|
|
438
|
+
expect(screen.queryByText('b')).toBeNull()
|
|
439
|
+
expect(screen.queryAllByRole('tooltip')).toHaveLength(1)
|
|
440
|
+
})
|
|
441
|
+
})
|
|
@@ -101,6 +101,23 @@ export type CartesianTooltipConfig<T> = {
|
|
|
101
101
|
prependRows?: (d: T, ctx: CartesianTooltipRowContext<T>) => ReactNode
|
|
102
102
|
/** Rows appended after the derived per-series rows. Same `ctx` as {@link prependRows}. */
|
|
103
103
|
extraRows?: (d: T, ctx: CartesianTooltipRowContext<T>) => ReactNode
|
|
104
|
+
/**
|
|
105
|
+
* Render this chart's tooltip when it is a cursor FOLLOWER (the crosshair came from a sibling
|
|
106
|
+
* chart sharing the cursor), not only when this chart is the SOURCE. Default false — today's
|
|
107
|
+
* behaviour, nothing moves. A follower has no pointer under it, so its tooltip is always ANCHORED
|
|
108
|
+
* to the crosshair — the same positioning `follow: false` gives the source — regardless of what
|
|
109
|
+
* `follow` is set to on this chart: there is no other valid position for a chart nobody is
|
|
110
|
+
* hovering. `follow` keeps governing the SOURCE's own tooltip exactly as before; the two options
|
|
111
|
+
* only interact for a follower, where `onFollow` always wins on positioning. Only the SOURCE's
|
|
112
|
+
* tooltip is `aria-live` — followers announce nothing, so a shared-cursor page doesn't fire N
|
|
113
|
+
* live-region updates per pointer move.
|
|
114
|
+
*
|
|
115
|
+
* Cost, since it scales with the page rather than the chart: anchoring reads one
|
|
116
|
+
* `getBoundingClientRect` per hovered frame (the same cost `follow: false` documents), so N
|
|
117
|
+
* opted-in followers pay N of them per frame. That is the reason this is opt-in per chart rather
|
|
118
|
+
* than a page-level switch — turn it on for the charts a reader actually correlates.
|
|
119
|
+
*/
|
|
120
|
+
onFollow?: boolean
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
/** Everything a mark renderer needs. Handed to `CartesianChart`'s child on every render. */
|
|
@@ -479,10 +496,17 @@ function CartesianPlot<T>({
|
|
|
479
496
|
const rows =
|
|
480
497
|
tooltipEnabled && point !== null ? deriveTooltipRows(tooltipSeries, point, leftFormat) : []
|
|
481
498
|
|
|
499
|
+
// This chart renders its tooltip either as the cursor SOURCE (always) or, opted in via
|
|
500
|
+
// `onFollow`, as a FOLLOWER (a sibling owns the cursor). A follower has no pointer under it, so
|
|
501
|
+
// it is always positioned via the anchored (crosshair) path below, regardless of `follow`.
|
|
502
|
+
const isFollowerRender = !cursor.isSource && tooltipCfg?.onFollow === true
|
|
503
|
+
const showTooltip = tooltipEnabled && point !== null && (cursor.isSource || isFollowerRender)
|
|
504
|
+
|
|
482
505
|
// Anchored mode resolves the crosshair's plot-local x into viewport space; following mode uses
|
|
483
|
-
// the pointer position `useChartCursor` already recorded.
|
|
484
|
-
|
|
485
|
-
|
|
506
|
+
// the pointer position `useChartCursor` already recorded. A follower render always anchors, since
|
|
507
|
+
// `follow`'s pointer-tracking path has no pointer to read on this chart.
|
|
508
|
+
const useAnchoredPosition = tooltipCfg?.follow === false || isFollowerRender
|
|
509
|
+
const anchorX = useAnchoredPosition && point !== null && cursorX !== null ? cursorX : null
|
|
486
510
|
const svgRect = anchorX === null ? undefined : svgRef.current?.getBoundingClientRect()
|
|
487
511
|
const tooltipAnchor =
|
|
488
512
|
svgRect === undefined || anchorX === null
|
|
@@ -595,8 +619,8 @@ function CartesianPlot<T>({
|
|
|
595
619
|
</Group>
|
|
596
620
|
</svg>
|
|
597
621
|
|
|
598
|
-
{
|
|
599
|
-
<ChartTooltipFloat anchor={tooltipAnchor}>
|
|
622
|
+
{showTooltip && (
|
|
623
|
+
<ChartTooltipFloat anchor={tooltipAnchor} ariaLive={cursor.isSource}>
|
|
600
624
|
<TooltipHeader
|
|
601
625
|
date={getX(point)}
|
|
602
626
|
{...(formatHeader !== undefined && {
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* `fmtTooltipDate` regexes `YYYY-MM-DD` out of the key and builds a LOCAL `Date`, so a UTC ISO
|
|
4
4
|
* domain key names the wrong day next to `formatX`/the tooltip badge, which both resolve locally.
|
|
5
5
|
*/
|
|
6
|
+
import { render, screen } from '@testing-library/react'
|
|
6
7
|
import { describe, expect, test } from 'bun:test'
|
|
7
8
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
8
|
-
import { TooltipHeader } from './ChartTooltip'
|
|
9
|
+
import { ChartTooltipFloat, TooltipHeader } from './ChartTooltip'
|
|
9
10
|
|
|
10
11
|
describe('TooltipHeader — format prop', () => {
|
|
11
12
|
test('defaults to fmtTooltipDate when omitted', () => {
|
|
@@ -21,3 +22,23 @@ describe('TooltipHeader — format prop', () => {
|
|
|
21
22
|
expect(html).not.toContain('Tue Apr 21 2026')
|
|
22
23
|
})
|
|
23
24
|
})
|
|
25
|
+
|
|
26
|
+
describe('ChartTooltipFloat — ariaLive', () => {
|
|
27
|
+
test('defaults to an aria-live="polite" region', () => {
|
|
28
|
+
render(
|
|
29
|
+
<ChartTooltipFloat anchor={{ x: 10, y: 10 }}>
|
|
30
|
+
<span>content</span>
|
|
31
|
+
</ChartTooltipFloat>,
|
|
32
|
+
)
|
|
33
|
+
expect(screen.getByRole('tooltip').getAttribute('aria-live')).toBe('polite')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('ariaLive: false drops the live-region attribute entirely — a follower is visual only', () => {
|
|
37
|
+
render(
|
|
38
|
+
<ChartTooltipFloat anchor={{ x: 10, y: 10 }} ariaLive={false}>
|
|
39
|
+
<span>content</span>
|
|
40
|
+
</ChartTooltipFloat>,
|
|
41
|
+
)
|
|
42
|
+
expect(screen.getByRole('tooltip').hasAttribute('aria-live')).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -155,9 +155,15 @@ const VIEWPORT_MARGIN = 8
|
|
|
155
155
|
*/
|
|
156
156
|
export function ChartTooltipFloat({
|
|
157
157
|
anchor,
|
|
158
|
+
ariaLive = true,
|
|
158
159
|
children,
|
|
159
160
|
}: {
|
|
160
161
|
anchor: { x: number; y: number } | null
|
|
162
|
+
/** Whether this tooltip announces itself to screen readers on every position change. Default
|
|
163
|
+
* true. Pass false for a cursor FOLLOWER's tooltip — N followers on a shared-cursor page all
|
|
164
|
+
* announcing on every pointer move would make the page unusable with a screen reader; only the
|
|
165
|
+
* cursor SOURCE should be `aria-live`. */
|
|
166
|
+
ariaLive?: boolean
|
|
161
167
|
children: ReactNode
|
|
162
168
|
}) {
|
|
163
169
|
const ref = useRef<HTMLDivElement>(null)
|
|
@@ -196,7 +202,7 @@ export function ChartTooltipFloat({
|
|
|
196
202
|
<div
|
|
197
203
|
ref={ref}
|
|
198
204
|
role="tooltip"
|
|
199
|
-
aria-live
|
|
205
|
+
{...(ariaLive && { 'aria-live': 'polite' as const })}
|
|
200
206
|
style={{ ...TOOLTIP_STYLES, left, top, visibility: box === null ? 'hidden' : 'visible' }}
|
|
201
207
|
>
|
|
202
208
|
{children}
|
package/src/charts/series.ts
CHANGED
|
@@ -66,6 +66,11 @@ export type SeriesStyle = {
|
|
|
66
66
|
* that lies is the thing the derived-legend rule exists to prevent). The TOOLTIP ROW swatch and
|
|
67
67
|
* the CROSSHAIR DOT deliberately do NOT honor it: those are 12px value-readout chips where a
|
|
68
68
|
* sub-1 opacity reads as a rendering bug, not as data.
|
|
69
|
+
*
|
|
70
|
+
* Applies to STROKES only, so it silently no-ops on a `mark: 'bar'`/`'area'` series — a bar
|
|
71
|
+
* swatch has no stroke to dim. Dim those with `fillOpacity` instead. This bites the
|
|
72
|
+
* zone/reference-legend idiom in particular (`mark: 'bar'`, `getValue: () => null`), where
|
|
73
|
+
* reaching for `strokeOpacity` looks right and does nothing.
|
|
69
74
|
*/
|
|
70
75
|
strokeOpacity?: number
|
|
71
76
|
/** Which y-axis this series is measured against. Default 'left'. `'right'` is what makes a
|