basalt-ui 1.16.0 → 1.17.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/agent/rules/basalt-charts.md +46 -4
  3. package/dist/charts/cursor/resolve.d.ts +29 -5
  4. package/dist/charts/cursor/resolve.d.ts.map +1 -1
  5. package/dist/charts/cursor/resolve.js +25 -4
  6. package/dist/charts/hooks/useChartCursor.d.ts +8 -1
  7. package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
  8. package/dist/charts/hooks/useChartCursor.js +6 -2
  9. package/dist/charts/index.d.ts +1 -0
  10. package/dist/charts/index.d.ts.map +1 -1
  11. package/dist/charts/kinds/Bars.d.ts +9 -0
  12. package/dist/charts/kinds/Bars.d.ts.map +1 -1
  13. package/dist/charts/kinds/Bars.js +4 -0
  14. package/dist/charts/kinds/DualPanel.d.ts +17 -0
  15. package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
  16. package/dist/charts/kinds/DualPanel.js +29 -6
  17. package/dist/charts/kinds/MultiLine.d.ts +9 -0
  18. package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
  19. package/dist/charts/kinds/MultiLine.js +10 -8
  20. package/dist/charts/kinds/StackedArea.d.ts +9 -0
  21. package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
  22. package/dist/charts/kinds/StackedArea.js +16 -1
  23. package/dist/charts/kinds/StackedArea.test.d.ts +2 -0
  24. package/dist/charts/kinds/StackedArea.test.d.ts.map +1 -0
  25. package/dist/charts/kinds/ZonedLine.d.ts +9 -0
  26. package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
  27. package/dist/charts/kinds/ZonedLine.js +4 -0
  28. package/dist/charts/primitives/CartesianChart.d.ts +23 -2
  29. package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
  30. package/dist/charts/primitives/CartesianChart.js +11 -4
  31. package/dist/charts/primitives/ChartFrame.d.ts +8 -1
  32. package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
  33. package/dist/charts/primitives/ChartFrame.js +1 -1
  34. package/dist/charts/primitives/ChartTooltip.d.ts +6 -1
  35. package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
  36. package/dist/charts/primitives/ChartTooltip.js +3 -2
  37. package/dist/charts/primitives/ChartTooltip.test.d.ts +2 -0
  38. package/dist/charts/primitives/ChartTooltip.test.d.ts.map +1 -0
  39. package/dist/charts/series.d.ts +4 -1
  40. package/dist/charts/series.d.ts.map +1 -1
  41. package/dist/charts/utils/domain.d.ts +15 -0
  42. package/dist/charts/utils/domain.d.ts.map +1 -1
  43. package/dist/charts/utils/domain.js +6 -1
  44. package/llms.txt +2 -2
  45. package/package.json +1 -1
  46. package/src/charts/cursor/resolve.test.ts +62 -0
  47. package/src/charts/cursor/resolve.ts +71 -7
  48. package/src/charts/hooks/useChartCursor.ts +12 -1
  49. package/src/charts/index.ts +1 -0
  50. package/src/charts/kinds/Bars.test.tsx +85 -0
  51. package/src/charts/kinds/Bars.tsx +13 -0
  52. package/src/charts/kinds/DualPanel.test.tsx +310 -0
  53. package/src/charts/kinds/DualPanel.tsx +48 -4
  54. package/src/charts/kinds/MultiLine.test.tsx +181 -0
  55. package/src/charts/kinds/MultiLine.tsx +19 -8
  56. package/src/charts/kinds/StackedArea.test.tsx +114 -0
  57. package/src/charts/kinds/StackedArea.tsx +25 -1
  58. package/src/charts/kinds/ZonedLine.test.tsx +110 -0
  59. package/src/charts/kinds/ZonedLine.tsx +13 -0
  60. package/src/charts/primitives/CartesianChart.test.tsx +111 -2
  61. package/src/charts/primitives/CartesianChart.tsx +35 -7
  62. package/src/charts/primitives/ChartFrame.test.tsx +42 -0
  63. package/src/charts/primitives/ChartFrame.tsx +9 -2
  64. package/src/charts/primitives/ChartTooltip.test.tsx +23 -0
  65. package/src/charts/primitives/ChartTooltip.tsx +7 -1
  66. package/src/charts/series.ts +3 -2
  67. package/src/charts/utils/domain.test.ts +31 -1
  68. package/src/charts/utils/domain.ts +19 -0
@@ -4,7 +4,20 @@ import { AreaStack } from "@visx/shape";
4
4
  import { memo, useCallback, useMemo } from "react";
5
5
  import { CartesianChart } from "../primitives/CartesianChart.js";
6
6
  function StackedAreaInner(props) {
7
- const { data, chartId, getX, series, y, xTicks, height, legend, ariaLabel, isPending } = props;
7
+ const {
8
+ data,
9
+ chartId,
10
+ getX,
11
+ series,
12
+ y,
13
+ xTicks,
14
+ formatX,
15
+ cursorResolution,
16
+ height,
17
+ legend,
18
+ ariaLabel,
19
+ isPending
20
+ } = props;
8
21
  const yConfig = useMemo(() => {
9
22
  const base = y ?? {};
10
23
  if (base.domain !== void 0 && base.domain !== "auto") return base;
@@ -47,6 +60,8 @@ function StackedAreaInner(props) {
47
60
  y: yConfig,
48
61
  cursorValue: stackedCursorValue,
49
62
  ...xTicks !== void 0 && { xTicks },
63
+ ...formatX !== void 0 && { formatX },
64
+ ...cursorResolution !== void 0 && { cursorResolution },
50
65
  ...height !== void 0 && { height },
51
66
  ...legend !== void 0 && { legend },
52
67
  ...ariaLabel !== void 0 && { ariaLabel },
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=StackedArea.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StackedArea.test.d.ts","sourceRoot":"","sources":["../../../src/charts/kinds/StackedArea.test.tsx"],"names":[],"mappings":""}
@@ -1,3 +1,4 @@
1
+ import type { CursorResolution } from '../cursor/resolve.js';
1
2
  import type { CartesianTooltipConfig, AxisConfig } from '../primitives/CartesianChart.js';
2
3
  import type { XZoneSpec } from '../primitives/XZoneRects.js';
3
4
  import type { ZoneSpec } from '../primitives/ZoneRects.js';
@@ -39,6 +40,14 @@ export type ZonedLineProps<T> = {
39
40
  refLines?: ZonedLineRefLine[];
40
41
  /** Exact number of x ticks. Default: as many as fit. */
41
42
  xTicks?: number;
43
+ /** X tick label formatter. Default `fmtAxisDate` (DD.MM). */
44
+ formatX?: (key: string) => string;
45
+ /**
46
+ * How a sibling chart's broadcast cursor key resolves against this chart's points. Default
47
+ * `'nearest'`. Pass `'leading'` when `getX` returns a bucket's leading edge (a weekly series
48
+ * keyed by its Monday) — see `CursorResolution`.
49
+ */
50
+ cursorResolution?: CursorResolution;
42
51
  /** Tooltip config — `label` for a right-aligned header badge (e.g. zone name with zone color),
43
52
  * `extraRows` for rows appended after the derived row. `false` disables the tooltip entirely. */
44
53
  tooltip?: CartesianTooltipConfig<T> | false;
@@ -1 +1 @@
1
- {"version":3,"file":"ZonedLine.d.ts","sourceRoot":"","sources":["../../../src/charts/kinds/ZonedLine.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,UAAU,EAAe,MAAM,8BAA8B,CAAA;AAEnG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAG/D,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAA;AAEpC,mFAAmF;AACnF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACtB;;wEAEoE;IACpE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACxB;gEAC4D;IAC5D,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IACjB,KAAK,CAAC,EAAE,aAAa,EAAE,CAAA;IACvB;sBACkB;IAClB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAA;IACpB,UAAU,CAAC,EAAE,kBAAkB,EAAE,CAAA;IACjC,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC7B,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;qGACiG;IACjG,OAAO,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B;4CACwC;IACxC,MAAM,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAA;IAClC,mGAAmG;IACnG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAiBD;;;;;;;;;;;;GAYG;AACH,iBAAS,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,+BA2DlD;AA+ED;;;GAGG;AACH,eAAO,MAAM,SAAS,EAA2B,OAAO,cAAc,CAAA"}
1
+ {"version":3,"file":"ZonedLine.d.ts","sourceRoot":"","sources":["../../../src/charts/kinds/ZonedLine.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEzD,OAAO,KAAK,EAAE,sBAAsB,EAAE,UAAU,EAAe,MAAM,8BAA8B,CAAA;AAEnG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAG/D,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAA;AAEpC,mFAAmF;AACnF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,0EAA0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,IAAI,EAAE,CAAC,EAAE,CAAA;IACT,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACtB;;wEAEoE;IACpE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACxB;gEAC4D;IAC5D,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IACjB,KAAK,CAAC,EAAE,aAAa,EAAE,CAAA;IACvB;sBACkB;IAClB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAA;IACpB,UAAU,CAAC,EAAE,kBAAkB,EAAE,CAAA;IACjC,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC7B,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACjC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAA;IACnC;qGACiG;IACjG,OAAO,CAAC,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAA;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC3B;4CACwC;IACxC,MAAM,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAA;IAClC,mGAAmG;IACnG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAiBD;;;;;;;;;;;;GAYG;AACH,iBAAS,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,+BA+DlD;AA+ED;;;GAGG;AACH,eAAO,MAAM,SAAS,EAA2B,OAAO,cAAc,CAAA"}
@@ -30,6 +30,8 @@ function ZonedLineInner(props) {
30
30
  thresholds,
31
31
  refLines,
32
32
  xTicks,
33
+ formatX,
34
+ cursorResolution,
33
35
  tooltip,
34
36
  areaFill,
35
37
  height,
@@ -56,6 +58,8 @@ function ZonedLineInner(props) {
56
58
  ...xZones !== void 0 && { xZones },
57
59
  ...refLines !== void 0 && { refLines },
58
60
  ...xTicks !== void 0 && { xTicks },
61
+ ...formatX !== void 0 && { formatX },
62
+ ...cursorResolution !== void 0 && { cursorResolution },
59
63
  ...tooltip !== void 0 && { tooltip },
60
64
  ...height !== void 0 && { height },
61
65
  ...legend !== void 0 && { legend },
@@ -1,6 +1,7 @@
1
1
  import { scaleLinear, scalePoint } from '@visx/scale';
2
2
  import type { ReactNode } from 'react';
3
3
  import type { ChartMargin } from '../../tokens/index.js';
4
+ import type { CursorResolution } from '../cursor/resolve.js';
4
5
  import type { ChartLegendConfig, ChartSeries } from '../series.js';
5
6
  import type { XZoneSpec } from './XZoneRects.js';
6
7
  import type { ZoneSpec } from './ZoneRects.js';
@@ -16,7 +17,11 @@ export type AxisConfig<T> = {
16
17
  * (summing `props.series` there would leave a permanent gap above the stack).
17
18
  */
18
19
  domain?: [number, number] | 'auto' | ((data: readonly T[], visible: readonly ChartSeries<T>[]) => [number, number]);
19
- /** When 'auto': the upper bound is at least this. */
20
+ /**
21
+ * When 'auto': the raw upper bound is at least this, and padding is applied AFTER — mirroring
22
+ * `autoMinCeil`, which clamps first and pads second. A floor that padded before the clamp used
23
+ * to land the axis top exactly on the floor value with zero headroom.
24
+ */
20
25
  autoMaxFloor?: number;
21
26
  /** When 'auto': the lower bound is at most this. Default 0 (forced zero baseline). `Infinity`
22
27
  * pads down from the raw data minimum instead. */
@@ -58,6 +63,14 @@ export type CartesianTooltipConfig<T> = {
58
63
  text: string;
59
64
  color: string;
60
65
  } | null;
66
+ /**
67
+ * Overrides the tooltip header's date text. Default: `TooltipHeader`'s own `fmtTooltipDate`
68
+ * behavior, unchanged. `fmtTooltipDate` regexes `YYYY-MM-DD` out of the domain key and builds a
69
+ * LOCAL `Date` from it — a UTC ISO key then names the wrong day next to `formatX`, the tooltip
70
+ * badge, and every sibling chart, which all resolve locally. Receives the raw `getX` key
71
+ * alongside the hovered datum, so a caller can format from either.
72
+ */
73
+ formatHeader?: (key: string, d: T) => string;
61
74
  /** Rows rendered BEFORE the derived per-series rows (a total, a context line). The second
62
75
  * argument carries the same `visible`/`hidden` the plot draws from, so a hand-authored row
63
76
  * stays in sync with legend toggling instead of naming a series the plot no longer draws. */
@@ -124,6 +137,14 @@ export type CartesianChartProps<T> = {
124
137
  * line the reader is actually looking at.
125
138
  */
126
139
  cursorValue?: (point: T, series: ChartSeries<T>, visible: readonly ChartSeries<T>[]) => number | null;
140
+ /**
141
+ * How a sibling chart's broadcast cursor key resolves against THIS chart's points. Default
142
+ * `'nearest'`, correct when `getX` returns an instant. Pass `'leading'` when it returns a
143
+ * bucket's leading edge (a weekly series keyed by its Monday): under `'nearest'` a hover landing
144
+ * in the back half of a bucket resolves to the FOLLOWING bucket, so the shared crosshair sits
145
+ * one column right of the data the reader is pointing at.
146
+ */
147
+ cursorResolution?: CursorResolution;
127
148
  ariaLabel?: string;
128
149
  isPending?: boolean;
129
150
  /** Draw ONLY the marks. Everything around them is the primitive's job. */
@@ -156,6 +177,6 @@ extraBounds?: readonly number[]): [number, number];
156
177
  * Margins are MEASURED from the labels that will actually be painted, so a wide tick label widens
157
178
  * its own gutter instead of clipping or needing a hand-tuned `margin` prop.
158
179
  */
159
- export declare function CartesianChart<T>({ data, chartId, getX, series, y, y2, xTicks, formatX, zones, xZones, refLines, height, aspectRatio, fill, legend, tooltip, margin: marginOverride, cursorValue, ariaLabel, isPending, children, }: CartesianChartProps<T>): ReactNode;
180
+ export declare function CartesianChart<T>({ data, chartId, getX, series, y, y2, xTicks, formatX, zones, xZones, refLines, height, aspectRatio, fill, legend, tooltip, margin: marginOverride, cursorValue, cursorResolution, ariaLabel, isPending, children, }: CartesianChartProps<T>): ReactNode;
160
181
  export {};
161
182
  //# sourceMappingURL=CartesianChart.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CartesianChart.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/CartesianChart.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CartesianChart.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/CartesianChart.tsx"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@ import { VX } from "../../tokens/index.js";
7
7
  import { useChartCursor } from "../hooks/useChartCursor.js";
8
8
  import { autoMargin, probeAxisLabels } from "../layout/auto-margin.js";
9
9
  import { deriveTooltipRows } from "../series.js";
10
- import { padAutoLower } from "../utils/domain.js";
10
+ import { padAutoLower, padAutoUpper } from "../utils/domain.js";
11
11
  import { fmtAxisDate } from "../utils/format.js";
12
12
  import { smartTicks, smartTicksEvery } from "../utils/ticks.js";
13
13
  import { AxisBottomDate, AxisLeftNumeric, AxisRightNumeric } from "./Axes.js";
@@ -41,8 +41,7 @@ function resolveAxisDomain(cfg, data, series, extraBounds = []) {
41
41
  if (min === Infinity) return [0, cfg?.autoMaxFloor ?? 1];
42
42
  const pad = cfg?.autoPad ?? DEFAULT_AUTO_PAD;
43
43
  const ceil = cfg?.autoMinCeil ?? 0;
44
- const paddedMax = max > 0 ? max * pad : max < 0 ? max / pad : 0;
45
- const upper = Math.max(paddedMax, cfg?.autoMaxFloor ?? -Infinity);
44
+ const upper = padAutoUpper(Math.max(max, cfg?.autoMaxFloor ?? -Infinity), pad);
46
45
  const lower = padAutoLower(Math.min(min, ceil), pad);
47
46
  if (lower === upper) return [Math.min(lower, 0), upper === 0 ? 1 : upper * pad];
48
47
  return [lower, upper];
@@ -66,6 +65,7 @@ function CartesianChart({
66
65
  tooltip,
67
66
  margin: marginOverride,
68
67
  cursorValue,
68
+ cursorResolution,
69
69
  ariaLabel,
70
70
  isPending,
71
71
  children
@@ -99,6 +99,7 @@ function CartesianChart({
99
99
  ...tooltip !== void 0 && { tooltip },
100
100
  ...marginOverride !== void 0 && { marginOverride },
101
101
  ...cursorValue !== void 0 && { cursorValue },
102
+ ...cursorResolution !== void 0 && { cursorResolution },
102
103
  ...ariaLabel !== void 0 && { ariaLabel },
103
104
  plot,
104
105
  highlighted,
@@ -123,6 +124,7 @@ function CartesianPlot({
123
124
  tooltip,
124
125
  marginOverride,
125
126
  cursorValue,
127
+ cursorResolution,
126
128
  ariaLabel,
127
129
  plot,
128
130
  highlighted,
@@ -209,7 +211,8 @@ function CartesianPlot({
209
211
  chartId,
210
212
  getKey: getX,
211
213
  xScale,
212
- marginLeft: margin.left
214
+ marginLeft: margin.left,
215
+ ...cursorResolution !== void 0 && { resolution: cursorResolution }
213
216
  });
214
217
  const svgRef = useRef(null);
215
218
  const tooltipEnabled = tooltip !== false;
@@ -217,6 +220,7 @@ function CartesianPlot({
217
220
  const point = cursor.point;
218
221
  const cursorX = point === null ? null : xScale(getX(point)) ?? null;
219
222
  const badge = tooltipCfg?.label === void 0 || point === null ? null : tooltipCfg.label(point);
223
+ const formatHeader = tooltipCfg?.formatHeader;
220
224
  const tooltipSeries = useMemo(
221
225
  () => visible.map(
222
226
  (s) => s.formatValue === void 0 ? { ...s, formatValue: s.axis === "right" ? rightFormat : leftFormat } : s
@@ -331,6 +335,9 @@ function CartesianPlot({
331
335
  TooltipHeader,
332
336
  {
333
337
  date: getX(point),
338
+ ...formatHeader !== void 0 && {
339
+ format: (key) => formatHeader(key, point)
340
+ },
334
341
  ...badge !== null && { label: badge.text, labelColor: badge.color }
335
342
  }
336
343
  ),
@@ -43,9 +43,16 @@ export type ChartFrameProps = {
43
43
  */
44
44
  isPending?: boolean;
45
45
  /**
46
- * Accessible text alternative for the chart, applied as `aria-label` (+ `role="img"`) on the
46
+ * Accessible text alternative for the chart, applied as `aria-label` (+ `role="group"`) on the
47
47
  * outer container so screen readers announce something other than an unlabeled graphic. Every
48
48
  * kind composing `ChartFrame` should accept and forward this from its own props.
49
+ *
50
+ * MUST stay `role="group"`, never `role="img"`. Per the ARIA spec, every descendant of an
51
+ * `role="img"` element is presentational, which erases the `HoverOverlay`'s `role="slider"` from
52
+ * the accessibility tree entirely — a screen reader announces the label and then the
53
+ * keyboard-scrubbable slider underneath it is simply unreachable, silently, with no error
54
+ * anywhere. `role="group"` announces the same label while keeping descendants exposed. Do not
55
+ * "simplify" this back to `img` — it looks like a no-op refactor and it is not.
49
56
  */
50
57
  ariaLabel?: string;
51
58
  /** Draw the SVG marks given the plot rect that already excludes the legend band, plus the set
@@ -1 +1 @@
1
- {"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAOhF;0EAC0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,uGAAuG;IACvG,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IAC9B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAA;IACjC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oDACgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAA;CACxC,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAcD;oFACoF;AACpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,EAC7C,KAAK,CAAC,EAAE,qBAAqB,GAC5B,gBAAgB,GAAG,KAAK,CAS1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,WAAW,EACX,IAAY,EACZ,QAA4B,EAC5B,OAAO,EACP,MAAW,EACX,SAAS,EACT,SAAiB,EACjB,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CAsE7B"}
1
+ {"version":3,"file":"ChartFrame.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartFrame.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAIrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAOhF;0EAC0E;AAC1E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wBAAwB;IACxB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,uGAAuG;IACvG,MAAM,EAAE,SAAS,WAAW,EAAE,CAAA;IAC9B,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAA;IACjC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oDACgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,SAAS,CAAA;CACxC,CAAA;AAED,0FAA0F;AAC1F,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAA;AAcD;oFACoF;AACpF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;CAC1C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,iBAAiB,GAAG,KAAK,GAAG,SAAS,EAC7C,KAAK,CAAC,EAAE,qBAAqB,GAC5B,gBAAgB,GAAG,KAAK,CAS1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,EACzB,MAAM,EACN,MAAM,EACN,WAAW,EACX,IAAY,EACZ,QAA4B,EAC5B,OAAO,EACP,MAAW,EACX,SAAS,EACT,SAAiB,EACjB,QAAQ,GACT,EAAE,eAAe,GAAG,SAAS,CAsE7B"}
@@ -80,7 +80,7 @@ function ChartFrame({
80
80
  {
81
81
  ref: containerRef,
82
82
  style: outerStyle(fill, vertical),
83
- ...ariaLabel !== void 0 && { role: "img", "aria-label": ariaLabel },
83
+ ...ariaLabel !== void 0 && { role: "group", "aria-label": ariaLabel },
84
84
  ...isPending && { "aria-busy": "true" },
85
85
  children: [
86
86
  legendNode !== null && (placement === "top" || placement === "left") && legendNode,
@@ -1,9 +1,14 @@
1
1
  import type { ReactNode } from 'react';
2
2
  /** Tooltip header — shows formatted date + optional right-aligned label with color. */
3
- export declare function TooltipHeader({ date, label, labelColor, }: {
3
+ export declare function TooltipHeader({ date, label, labelColor, format, }: {
4
4
  date: string;
5
5
  label?: string;
6
6
  labelColor?: string;
7
+ /** Overrides `fmtTooltipDate` for the header text. Default: today's `fmtTooltipDate` behavior,
8
+ * unchanged. The seam exists because `fmtTooltipDate` regexes `YYYY-MM-DD` out of the key and
9
+ * builds a LOCAL `Date`, so a UTC ISO domain key names the wrong day next to `formatX`/the
10
+ * tooltip badge, which both resolve locally — a caller with a UTC key needs to supply its own. */
11
+ format?: (date: string) => string;
7
12
  }): import("react").JSX.Element;
8
13
  /** Tooltip row — swatch + label + value. */
9
14
  export declare function TooltipRow({ color, label, value, valueColor, shape, strokeWidth, dashed, }: {
@@ -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,GACX,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,+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,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"}
@@ -19,7 +19,8 @@ const TOOLTIP_STYLES = {
19
19
  function TooltipHeader({
20
20
  date,
21
21
  label,
22
- labelColor
22
+ labelColor,
23
+ format = fmtTooltipDate
23
24
  }) {
24
25
  return /* @__PURE__ */ jsxs(
25
26
  "div",
@@ -42,7 +43,7 @@ function TooltipHeader({
42
43
  fontFamily: "var(--basalt-font-mono)",
43
44
  color: VX.ink
44
45
  },
45
- children: fmtTooltipDate(date)
46
+ children: format(date)
46
47
  }
47
48
  ),
48
49
  label !== void 0 && /* @__PURE__ */ jsx(
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ChartTooltip.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartTooltip.test.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.test.tsx"],"names":[],"mappings":""}
@@ -87,10 +87,13 @@ export type ChartSeries<T> = SeriesStyle & {
87
87
  * alongside the value so a row can read fields beyond the plotted number (e.g. `97.5 kg (92.5 ×
88
88
  * 3)`). */
89
89
  formatValue?: (v: number, d: T) => string;
90
- /** PR star / status dot. Return null for no marker at that point. */
90
+ /** PR star / status dot. Return null for no marker at that point. `ring` defaults true (today's
91
+ * punched-out stroke, unchanged); `false` omits the stroke entirely. `fillOpacity` defaults 1. */
91
92
  getMarker?: (d: T) => {
92
93
  color?: string;
93
94
  r?: number;
95
+ fillOpacity?: number;
96
+ ring?: boolean;
94
97
  } | null;
95
98
  };
96
99
  /** A single derived tooltip row, ready for `TooltipRow`. */
@@ -1 +1 @@
1
- {"version":3,"file":"series.d.ts","sourceRoot":"","sources":["../../src/charts/series.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"series.d.ts","sourceRoot":"","sources":["../../src/charts/series.ts"],"names":[],"mappings":"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"}
@@ -17,4 +17,19 @@
17
17
  * pre-existing, still-correct behavior for genuinely negative-swinging metrics.
18
18
  */
19
19
  export declare function padAutoLower(candidate: number, pad: number): number;
20
+ /**
21
+ * Auto y-domain upper-bound padding — the exact mirror of {@link padAutoLower}, sharing its
22
+ * sign-safety: pad AWAY from zero (`upper >= candidate`), never toward it. `candidate` is the
23
+ * caller's `Math.max(dataMax, floor)` — the `autoMaxFloor` clamp has already been applied, so
24
+ * padding runs AFTER the floor, not before it. Applying the floor post-pad (the pre-existing bug)
25
+ * lands a floored axis top exactly on the floor value with zero headroom — a target line pinned to
26
+ * that floor then sits glued to the plot edge instead of getting the same breathing room every
27
+ * other bound gets.
28
+ *
29
+ * - `candidate === 0` stays exactly 0 — never pushed positive.
30
+ * - `candidate > 0` multiplies by `pad`, moving the ceiling further UP (away from zero).
31
+ * - `candidate < 0` divides by `pad`, moving the ceiling UP toward (and past) zero — the mirror of
32
+ * `padAutoLower`'s `candidate > 0` branch.
33
+ */
34
+ export declare function padAutoUpper(candidate: number, pad: number): number;
20
35
  //# sourceMappingURL=domain.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../../src/charts/utils/domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGnE"}
1
+ {"version":3,"file":"domain.d.ts","sourceRoot":"","sources":["../../../src/charts/utils/domain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGnE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGnE"}
@@ -2,6 +2,11 @@ function padAutoLower(candidate, pad) {
2
2
  if (candidate === 0) return 0;
3
3
  return candidate > 0 ? candidate / pad : candidate * pad;
4
4
  }
5
+ function padAutoUpper(candidate, pad) {
6
+ if (candidate === 0) return 0;
7
+ return candidate > 0 ? candidate * pad : candidate / pad;
8
+ }
5
9
  export {
6
- padAutoLower
10
+ padAutoLower,
11
+ padAutoUpper
7
12
  };
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.16.0
1
+ # basalt-ui v1.17.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.16.0
22
+ Version: 1.17.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -59,3 +59,65 @@ describe('resolveCursorPoint', () => {
59
59
  expect(resolveCursorPoint(i, '2026-08-02')).toBeNull()
60
60
  })
61
61
  })
62
+
63
+ describe("resolveCursorPoint — 'leading' resolution (bucket-keyed domains)", () => {
64
+ const weekly = (days: string[]) => buildDomainIndex(daily(days), (p) => p.date, 'leading')
65
+
66
+ test('resolves an exact key, same as nearest', () => {
67
+ const i = weekly(['01', '08', '15'])
68
+ expect(resolveCursorPoint(i, '2026-08-08')?.date).toBe('2026-08-08')
69
+ })
70
+
71
+ test("a target anywhere inside a bucket resolves to that bucket's LEADING key, not the nearest one", () => {
72
+ // The bug this replaces: a weekly chart keyed by the Monday of each week (step 7 days),
73
+ // hovered from a daily sibling landing in the back half of the bucket, used to resolve to the
74
+ // FOLLOWING week under nearest-match. Every back-half day must resolve to the bucket it is
75
+ // actually inside.
76
+ const i = weekly(['01', '08', '15'])
77
+ expect(resolveCursorPoint(i, '2026-08-02')?.date).toBe('2026-08-01')
78
+ expect(resolveCursorPoint(i, '2026-08-05')?.date).toBe('2026-08-01')
79
+ expect(resolveCursorPoint(i, '2026-08-07')?.date).toBe('2026-08-01')
80
+ expect(resolveCursorPoint(i, '2026-08-09')?.date).toBe('2026-08-08')
81
+ expect(resolveCursorPoint(i, '2026-08-14')?.date).toBe('2026-08-08')
82
+ })
83
+
84
+ test('a key BEFORE the first bucket resolves to null — no bucket contains it', () => {
85
+ // Deliberately NOT 'nearest''s one-step tolerance. The first key is the leading edge of the
86
+ // first bucket, so anything earlier is outside every bucket; snapping it onto the first one
87
+ // would paint a crosshair on a bucket that provably does not contain the hovered key.
88
+ const i = weekly(['02', '09', '16'])
89
+ expect(resolveCursorPoint(i, '2026-08-01')).toBeNull()
90
+ expect(resolveCursorPoint(i, '2026-08-02')?.date).toBe('2026-08-02')
91
+ })
92
+
93
+ test('the final bucket ends at last + step — that instant is already outside it', () => {
94
+ const i = weekly(['01', '08', '15']) // step = 7, so the last bucket covers [Aug 15, Aug 22)
95
+ expect(resolveCursorPoint(i, '2026-08-21')?.date).toBe('2026-08-15') // last day inside
96
+ expect(resolveCursorPoint(i, '2026-08-22')).toBeNull() // exactly last + step, exclusive
97
+ expect(resolveCursorPoint(i, '2026-08-23')).toBeNull()
98
+ })
99
+
100
+ test('returns null well beyond the domain, so unrelated charts never sync', () => {
101
+ const i = weekly(['01', '08', '15'])
102
+ expect(resolveCursorPoint(i, '2026-09-30')).toBeNull()
103
+ })
104
+
105
+ test('categorical domains fall back to exact match only, same as nearest', () => {
106
+ const points = [{ date: 'Direct' }, { date: 'Referral' }]
107
+ const i = buildDomainIndex(points, (p) => p.date, 'leading')
108
+ expect(resolveCursorPoint(i, 'Direct')?.date).toBe('Direct')
109
+ expect(resolveCursorPoint(i, 'Organic')).toBeNull()
110
+ })
111
+
112
+ test('a single-point chart resolves only its own key', () => {
113
+ const i = weekly(['01'])
114
+ expect(resolveCursorPoint(i, '2026-08-01')?.date).toBe('2026-08-01')
115
+ expect(resolveCursorPoint(i, '2026-08-02')).toBeNull()
116
+ })
117
+
118
+ test('an explicit resolution argument overrides the index-built default', () => {
119
+ // Built as 'nearest', resolved as 'leading' — the per-call argument wins.
120
+ const i = index(daily(['01', '08', '15']))
121
+ expect(resolveCursorPoint(i, '2026-08-05', 'leading')?.date).toBe('2026-08-01')
122
+ })
123
+ })
@@ -22,6 +22,27 @@ export function parseKey(key: string): number | null {
22
22
  return Number.isNaN(t) ? null : t
23
23
  }
24
24
 
25
+ /**
26
+ * How a foreign key maps onto a chart's own points.
27
+ *
28
+ * `'nearest'` (default) treats every key as a POINT — the foreign key resolves to whichever own
29
+ * point sits closest to it. Correct when a chart's x-domain is a set of instants.
30
+ *
31
+ * `'leading'` treats every key as the LEADING EDGE of a bucket covering `[key, nextKey)` — the
32
+ * foreign key resolves to the LAST own key `<=` it. Use this when a chart's keys are bucket
33
+ * starts (a weekly series keyed by the Monday of each week, a monthly series keyed by the 1st):
34
+ * under `'nearest'`, a target landing in the back half of a bucket resolves to the FOLLOWING
35
+ * bucket instead of the one it's actually inside, and the shared crosshair lands one column off
36
+ * for every back-half hover.
37
+ *
38
+ * The two modes differ at the DOMAIN EDGES too, and deliberately: `'nearest'` tolerates one step
39
+ * past each end (the target is an instant, so the closest point is still a defensible answer),
40
+ * while `'leading'` bounds strictly to `[first, last + step)` — outside that range no bucket
41
+ * CONTAINS the key, and answering with the nearest one would paint a crosshair on a bucket that
42
+ * provably excludes it.
43
+ */
44
+ export type CursorResolution = 'nearest' | 'leading'
45
+
25
46
  export type DomainIndex<T> = {
26
47
  byKey: Map<string, T>
27
48
  /** Parsed positions, ascending. Empty when the domain isn't parseable. */
@@ -30,10 +51,16 @@ export type DomainIndex<T> = {
30
51
  points: T[]
31
52
  /** Median spacing — the tolerance within which a foreign key still resolves. */
32
53
  step: number
54
+ /** The resolution strategy this index was built for. */
55
+ resolution: CursorResolution
33
56
  }
34
57
 
35
58
  /** Build the lookup structure once per `data` identity. */
36
- export function buildDomainIndex<T>(data: readonly T[], getKey: (d: T) => string): DomainIndex<T> {
59
+ export function buildDomainIndex<T>(
60
+ data: readonly T[],
61
+ getKey: (d: T) => string,
62
+ resolution: CursorResolution = 'nearest',
63
+ ): DomainIndex<T> {
37
64
  const byKey = new Map<string, T>()
38
65
  const parsed: { value: number; point: T }[] = []
39
66
  let parseable = true
@@ -48,7 +75,9 @@ export function buildDomainIndex<T>(data: readonly T[], getKey: (d: T) => string
48
75
 
49
76
  // Under two parseable points there is no step to reason about, so nearest-match would be
50
77
  // unbounded — fall back to exact match.
51
- if (!parseable || parsed.length < 2) return { byKey, values: [], points: [], step: 0 }
78
+ if (!parseable || parsed.length < 2) {
79
+ return { byKey, values: [], points: [], step: 0, resolution }
80
+ }
52
81
 
53
82
  parsed.sort((a, b) => a.value - b.value)
54
83
  const values = parsed.map((p) => p.value)
@@ -60,15 +89,39 @@ export function buildDomainIndex<T>(data: readonly T[], getKey: (d: T) => string
60
89
  }
61
90
  diffs.sort((a, b) => a - b)
62
91
 
63
- return { byKey, values, points, step: diffs[Math.floor(diffs.length / 2)] ?? 0 }
92
+ return { byKey, values, points, step: diffs[Math.floor(diffs.length / 2)] ?? 0, resolution }
93
+ }
94
+
95
+ /** `'leading'` resolution: the last own position `<= target`. The caller has already rejected
96
+ * anything outside `[first, last + step)`, so a containing bucket is guaranteed to exist. */
97
+ function resolveLeading<T>(index: DomainIndex<T>, target: number): T {
98
+ let lo = 0
99
+ let hi = index.values.length - 1
100
+ let result = 0
101
+ while (lo <= hi) {
102
+ const mid = (lo + hi) >> 1
103
+ if ((index.values[mid] as number) <= target) {
104
+ result = mid
105
+ lo = mid + 1
106
+ } else {
107
+ hi = mid - 1
108
+ }
109
+ }
110
+ return index.points[result] as T
64
111
  }
65
112
 
66
113
  /**
67
- * The own point a broadcast `key` maps to: exact match first, else the nearest parsed position
68
- * within one domain step. Out of range → null, so charts over unrelated domains never paint a
69
- * cursor for each other — which is what makes "shared by default" safe.
114
+ * The own point a broadcast `key` maps to: exact match first, else — per `index.resolution` — the
115
+ * nearest parsed position or the last position at/before the target, both within one domain step.
116
+ * Out of range → null, so charts over unrelated domains never paint a cursor for each other —
117
+ * which is what makes "shared by default" safe. A categorical (unparseable) domain falls back to
118
+ * exact-match-only in BOTH resolutions, since "nearest"/"leading" over a category is meaningless.
70
119
  */
71
- export function resolveCursorPoint<T>(index: DomainIndex<T>, key: string): T | null {
120
+ export function resolveCursorPoint<T>(
121
+ index: DomainIndex<T>,
122
+ key: string,
123
+ resolution: CursorResolution = index.resolution,
124
+ ): T | null {
72
125
  const exact = index.byKey.get(key)
73
126
  if (exact !== undefined) return exact
74
127
 
@@ -77,6 +130,17 @@ export function resolveCursorPoint<T>(index: DomainIndex<T>, key: string): T | n
77
130
 
78
131
  const first = index.values[0] as number
79
132
  const last = index.values[index.values.length - 1] as number
133
+
134
+ if (resolution === 'leading') {
135
+ // Containment, NOT tolerance. A bucket keyed `k` covers `[k, nextKey)`, so a target before the
136
+ // first key sits in no bucket at all, and `last + step` is the first instant past the final
137
+ // one. Borrowing `'nearest'`'s symmetric ±step window here would snap a pre-domain target onto
138
+ // the first bucket and a post-domain one onto the last — painting a crosshair on a bucket that
139
+ // provably does not contain the hovered key, which is the exact lie this mode exists to remove.
140
+ if (target < first || target >= last + index.step) return null
141
+ return resolveLeading(index, target)
142
+ }
143
+
80
144
  if (target < first - index.step || target > last + index.step) return null
81
145
 
82
146
  let lo = 0