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.
- package/CHANGELOG.md +7 -0
- package/agent/rules/basalt-charts.md +46 -4
- package/dist/charts/cursor/resolve.d.ts +29 -5
- package/dist/charts/cursor/resolve.d.ts.map +1 -1
- package/dist/charts/cursor/resolve.js +25 -4
- package/dist/charts/hooks/useChartCursor.d.ts +8 -1
- package/dist/charts/hooks/useChartCursor.d.ts.map +1 -1
- package/dist/charts/hooks/useChartCursor.js +6 -2
- package/dist/charts/index.d.ts +1 -0
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.d.ts +9 -0
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +4 -0
- package/dist/charts/kinds/DualPanel.d.ts +17 -0
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +29 -6
- package/dist/charts/kinds/MultiLine.d.ts +9 -0
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +10 -8
- package/dist/charts/kinds/StackedArea.d.ts +9 -0
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +16 -1
- package/dist/charts/kinds/StackedArea.test.d.ts +2 -0
- package/dist/charts/kinds/StackedArea.test.d.ts.map +1 -0
- package/dist/charts/kinds/ZonedLine.d.ts +9 -0
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +4 -0
- package/dist/charts/primitives/CartesianChart.d.ts +23 -2
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +11 -4
- package/dist/charts/primitives/ChartFrame.d.ts +8 -1
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +1 -1
- 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 +3 -2
- package/dist/charts/primitives/ChartTooltip.test.d.ts +2 -0
- package/dist/charts/primitives/ChartTooltip.test.d.ts.map +1 -0
- package/dist/charts/series.d.ts +4 -1
- package/dist/charts/series.d.ts.map +1 -1
- package/dist/charts/utils/domain.d.ts +15 -0
- package/dist/charts/utils/domain.d.ts.map +1 -1
- package/dist/charts/utils/domain.js +6 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/cursor/resolve.test.ts +62 -0
- package/src/charts/cursor/resolve.ts +71 -7
- package/src/charts/hooks/useChartCursor.ts +12 -1
- package/src/charts/index.ts +1 -0
- package/src/charts/kinds/Bars.test.tsx +85 -0
- package/src/charts/kinds/Bars.tsx +13 -0
- package/src/charts/kinds/DualPanel.test.tsx +310 -0
- package/src/charts/kinds/DualPanel.tsx +48 -4
- package/src/charts/kinds/MultiLine.test.tsx +181 -0
- package/src/charts/kinds/MultiLine.tsx +19 -8
- package/src/charts/kinds/StackedArea.test.tsx +114 -0
- package/src/charts/kinds/StackedArea.tsx +25 -1
- package/src/charts/kinds/ZonedLine.test.tsx +110 -0
- package/src/charts/kinds/ZonedLine.tsx +13 -0
- package/src/charts/primitives/CartesianChart.test.tsx +111 -2
- package/src/charts/primitives/CartesianChart.tsx +35 -7
- package/src/charts/primitives/ChartFrame.test.tsx +42 -0
- package/src/charts/primitives/ChartFrame.tsx +9 -2
- package/src/charts/primitives/ChartTooltip.test.tsx +23 -0
- package/src/charts/primitives/ChartTooltip.tsx +7 -1
- package/src/charts/series.ts +3 -2
- package/src/charts/utils/domain.test.ts +31 -1
- 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 {
|
|
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 @@
|
|
|
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":"
|
|
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
|
-
/**
|
|
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":"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;
|
|
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
|
|
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="
|
|
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
|
|
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: "
|
|
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,
|
|
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:
|
|
46
|
+
children: format(date)
|
|
46
47
|
}
|
|
47
48
|
),
|
|
48
49
|
label !== void 0 && /* @__PURE__ */ jsx(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChartTooltip.test.d.ts","sourceRoot":"","sources":["../../../src/charts/primitives/ChartTooltip.test.tsx"],"names":[],"mappings":""}
|
package/dist/charts/series.d.ts
CHANGED
|
@@ -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":"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;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;6FACyF;IACzF,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB,wBAAwB;IACxB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;gCAC4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,uFAAuF;AACvF,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,WAAW,GAAG;IACzC,6CAA6C;IAC7C,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,MAAM,GAAG,IAAI,CAAA;IACjC;;eAEW;IACX,WAAW,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,KAAK,MAAM,CAAA;IACzC
|
|
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.
|
|
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.
|
|
22
|
+
Version: 1.17.0
|
|
23
23
|
|
|
24
24
|
## basalt-ui
|
|
25
25
|
Import: import { ... } from 'basalt-ui'
|
package/package.json
CHANGED
|
@@ -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>(
|
|
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)
|
|
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
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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>(
|
|
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
|