@pond-ts/charts 0.31.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 (79) hide show
  1. package/CHANGELOG.md +3254 -0
  2. package/LICENSE +21 -0
  3. package/README.md +229 -0
  4. package/dist/AreaChart.d.ts +85 -0
  5. package/dist/AreaChart.js +119 -0
  6. package/dist/BandChart.d.ts +55 -0
  7. package/dist/BandChart.js +93 -0
  8. package/dist/BarChart.d.ts +72 -0
  9. package/dist/BarChart.js +137 -0
  10. package/dist/BoxPlot.d.ts +77 -0
  11. package/dist/BoxPlot.js +137 -0
  12. package/dist/Canvas.d.ts +37 -0
  13. package/dist/Canvas.js +39 -0
  14. package/dist/ChartContainer.d.ts +106 -0
  15. package/dist/ChartContainer.js +306 -0
  16. package/dist/ChartRow.d.ts +29 -0
  17. package/dist/ChartRow.js +215 -0
  18. package/dist/Layers.d.ts +22 -0
  19. package/dist/Layers.js +399 -0
  20. package/dist/LineChart.d.ts +60 -0
  21. package/dist/LineChart.js +105 -0
  22. package/dist/ScatterChart.d.ts +84 -0
  23. package/dist/ScatterChart.js +139 -0
  24. package/dist/TimeAxis.d.ts +9 -0
  25. package/dist/TimeAxis.js +12 -0
  26. package/dist/XAxis.d.ts +39 -0
  27. package/dist/XAxis.js +84 -0
  28. package/dist/YAxis.d.ts +42 -0
  29. package/dist/YAxis.js +86 -0
  30. package/dist/annotations.d.ts +110 -0
  31. package/dist/annotations.js +459 -0
  32. package/dist/area.d.ts +54 -0
  33. package/dist/area.js +186 -0
  34. package/dist/band.d.ts +31 -0
  35. package/dist/band.js +57 -0
  36. package/dist/bars.d.ts +96 -0
  37. package/dist/bars.js +171 -0
  38. package/dist/box.d.ts +59 -0
  39. package/dist/box.js +140 -0
  40. package/dist/chip.d.ts +23 -0
  41. package/dist/chip.js +43 -0
  42. package/dist/cjs-fallback.cjs +16 -0
  43. package/dist/context.d.ts +362 -0
  44. package/dist/context.js +5 -0
  45. package/dist/curve.d.ts +22 -0
  46. package/dist/curve.js +13 -0
  47. package/dist/data.d.ts +154 -0
  48. package/dist/data.js +197 -0
  49. package/dist/domain.d.ts +19 -0
  50. package/dist/domain.js +61 -0
  51. package/dist/encoding.d.ts +89 -0
  52. package/dist/encoding.js +144 -0
  53. package/dist/format.d.ts +53 -0
  54. package/dist/format.js +47 -0
  55. package/dist/gaps.d.ts +146 -0
  56. package/dist/gaps.js +209 -0
  57. package/dist/grid.d.ts +11 -0
  58. package/dist/grid.js +29 -0
  59. package/dist/index.d.ts +53 -0
  60. package/dist/index.js +34 -0
  61. package/dist/line.d.ts +46 -0
  62. package/dist/line.js +88 -0
  63. package/dist/range.d.ts +15 -0
  64. package/dist/range.js +27 -0
  65. package/dist/scatter.d.ts +70 -0
  66. package/dist/scatter.js +213 -0
  67. package/dist/select.d.ts +13 -0
  68. package/dist/select.js +23 -0
  69. package/dist/slots.d.ts +48 -0
  70. package/dist/slots.js +64 -0
  71. package/dist/theme.d.ts +224 -0
  72. package/dist/theme.js +232 -0
  73. package/dist/tracker.d.ts +30 -0
  74. package/dist/tracker.js +47 -0
  75. package/dist/use-slot-key.d.ts +21 -0
  76. package/dist/use-slot-key.js +25 -0
  77. package/dist/viewport.d.ts +20 -0
  78. package/dist/viewport.js +30 -0
  79. package/package.json +67 -0
@@ -0,0 +1,306 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
+ import { scaleLinear, scaleTime } from 'd3-scale';
4
+ import { ContainerContext, } from './context.js';
5
+ import { maxSlotWidths, sum } from './slots.js';
6
+ import { resolveCursorX, DEFAULT_CURSOR_MODE } from './tracker.js';
7
+ import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
8
+ import { TimeAxis } from './TimeAxis.js';
9
+ import { defaultTheme } from './theme.js';
10
+ /** Time-axis tick count — matches `<TimeAxis>` so the cursor-time formatter is
11
+ * calibrated as the time-axis labels are. */
12
+ const TIME_TICK_COUNT = 5;
13
+ /**
14
+ * Normalize the `range` prop — a `[begin, end]` tuple or a `TimeRange` — to a
15
+ * plain `[number, number]`, or `undefined` when omitted (→ auto-fit). The
16
+ * `'begin' in range` check distinguishes the `TimeRange` from the tuple (a
17
+ * tuple has no `begin` key).
18
+ */
19
+ function normalizeRange(range) {
20
+ if (range === undefined)
21
+ return undefined;
22
+ return 'begin' in range ? [range.begin(), range.end()] : [range[0], range[1]];
23
+ }
24
+ /**
25
+ * The top of the chart layout (react-timeseries-charts-style). Owns the shared
26
+ * **x geometry**: it collects each row's per-slot gutter widths, reserves each
27
+ * slot's max across rows (so the innermost axis aligns column-by-column and
28
+ * every row's plot left-aligns), and from the slot sums derives `plotWidth` and
29
+ * the shared time `xScale`. It renders its rows (separated by `rowGap`) then one
30
+ * {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
31
+ * (`<YAxis>`).
32
+ */
33
+ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorTime = false, timeFormat, theme, children, }) {
34
+ // The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
35
+ // ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
36
+ // is the placeholder while auto-fitting.
37
+ const explicitDomain = normalizeRange(range);
38
+ const seed = explicitDomain ?? [0, 1];
39
+ // View range: pan/zoom moves it. Controlled (onTimeRangeChange) reads the prop
40
+ // and routes gestures back through the callback; uncontrolled holds it
41
+ // internally. With panZoom off, the seed is used directly — so a static or live
42
+ // (sliding-prop) chart tracks the prop as before.
43
+ const [internalRange, setInternalRange] = useState([
44
+ seed[0],
45
+ seed[1],
46
+ ]);
47
+ const uncontrolled = panZoom && onTimeRangeChange === undefined;
48
+ // While the internal view isn't in use (not uncontrolled), keep it synced to
49
+ // the prop — so *entering* uncontrolled pan/zoom (toggling panZoom on, or a
50
+ // controlled→uncontrolled switch) starts from the current range, not the
51
+ // mount-time one. While uncontrolled, leave it alone so a range change
52
+ // can't fight the user's pan. (Adjusting state during render — React re-renders
53
+ // before commit, no extra paint; the guard makes it converge in one step.)
54
+ if (!uncontrolled &&
55
+ (internalRange[0] !== seed[0] || internalRange[1] !== seed[1])) {
56
+ setInternalRange([seed[0], seed[1]]);
57
+ }
58
+ const view = uncontrolled ? internalRange : seed;
59
+ const t0 = view[0];
60
+ const t1 = view[1];
61
+ // Latest onTimeRangeChange in a ref so applyRange stays stable. Written after
62
+ // commit (not in render) so a gesture never reads a callback from a frame that
63
+ // was abandoned under concurrent rendering.
64
+ const onRangeRef = useRef(onTimeRangeChange);
65
+ useLayoutEffect(() => {
66
+ onRangeRef.current = onTimeRangeChange;
67
+ });
68
+ const applyRange = useCallback((range) => {
69
+ const cb = onRangeRef.current;
70
+ if (cb)
71
+ cb(range);
72
+ else
73
+ setInternalRange(range);
74
+ }, []);
75
+ // Cross-row tracker. We store the cursor's plot-pixel x (not a timestamp), so a
76
+ // still cursor stays put while a live window slides under it; a controlled
77
+ // `trackerPosition` resolves to a pixel below.
78
+ const [hoverX, setHoverX] = useState(null);
79
+ // Draw layers register as tracker sources; on hover we fan in their values at
80
+ // the cursor and hand them out via onTrackerChanged (held in a ref so an
81
+ // inline callback doesn't churn the frame). This powers a readout rendered
82
+ // *outside* the chart — the preferred surface for hover values.
83
+ const [sources, setSources] = useState(() => new Map());
84
+ const registerTrackerSource = useCallback((key, source) => setSources((m) => new Map(m).set(key, source)), []);
85
+ const unregisterTrackerSource = useCallback((key) => {
86
+ setSources((m) => {
87
+ if (!m.has(key))
88
+ return m;
89
+ const next = new Map(m);
90
+ next.delete(key);
91
+ return next;
92
+ });
93
+ }, []);
94
+ // The shared x scale's kind, **inferred from the registered layers**: a
95
+ // ValueSeries row plots on a value axis, a TimeSeries on time. A container
96
+ // has one shared x (the synced cursor's whole point), so the rows must agree
97
+ // — a mix is a hard error. Defaults to `'time'` until a layer registers (the
98
+ // two-pass: register → re-resolve → rescale).
99
+ const resolvedKind = useMemo(() => {
100
+ let kind;
101
+ for (const s of sources.values()) {
102
+ if (kind === undefined)
103
+ kind = s.xKind;
104
+ else if (kind !== s.xKind) {
105
+ throw new Error(`ChartContainer: rows mix x-axis kinds ('${kind}' and '${s.xKind}'). ` +
106
+ `A container has one shared x axis — every row must plot the same ` +
107
+ `kind (all time-keyed, or all value-keyed).`);
108
+ }
109
+ }
110
+ return kind ?? 'time';
111
+ }, [sources]);
112
+ // Auto-fit extent — the union of the layers' x extents — used as the domain
113
+ // when no explicit `range` is given. (Same source registry as the kind; the
114
+ // two-pass register→resolve applies.)
115
+ const autoExtent = useMemo(() => {
116
+ let lo = Infinity;
117
+ let hi = -Infinity;
118
+ for (const s of sources.values()) {
119
+ const e = s.xExtent();
120
+ if (e) {
121
+ if (e[0] < lo)
122
+ lo = e[0];
123
+ if (e[1] > hi)
124
+ hi = e[1];
125
+ }
126
+ }
127
+ return lo <= hi ? [lo, hi] : null;
128
+ }, [sources]);
129
+ const onTrackerRef = useRef(onTrackerChanged);
130
+ onTrackerRef.current = onTrackerChanged;
131
+ // Selection: controlled (`selected` prop) or uncontrolled (internal). A click
132
+ // on a selectable layer calls `select()` after hit-testing; `onSelect` notifies
133
+ // in both modes, the internal state is managed only when uncontrolled. The full
134
+ // SelectInfo is the identity (key + series), so multi-series marks at one
135
+ // timestamp stay distinct. Refs written after commit (not in render) so the
136
+ // click handler never reads a callback / mode from a frame abandoned under
137
+ // concurrent rendering.
138
+ const [internalSelected, setInternalSelected] = useState(null);
139
+ const controlledSelection = selected !== undefined;
140
+ const selectedValue = controlledSelection
141
+ ? (selected ?? null)
142
+ : internalSelected;
143
+ const onSelectRef = useRef(onSelect);
144
+ const controlledSelectionRef = useRef(controlledSelection);
145
+ useLayoutEffect(() => {
146
+ onSelectRef.current = onSelect;
147
+ controlledSelectionRef.current = controlledSelection;
148
+ });
149
+ const select = useCallback((hit) => {
150
+ onSelectRef.current?.(hit);
151
+ if (!controlledSelectionRef.current)
152
+ setInternalSelected(hit);
153
+ }, []);
154
+ // Hover-highlight: the transient mark under the pointer (distinct from the
155
+ // committed selection). Deduped by key+label so the data canvas repaints only
156
+ // when the hovered mark changes — not on every pointer move (the move itself
157
+ // just slides the SVG cursor, which never touches the data canvas).
158
+ const [hovered, setHoveredState] = useState(null);
159
+ const setHovered = useCallback((hit) => {
160
+ setHoveredState((prev) => prev === hit ||
161
+ (prev !== null &&
162
+ hit !== null &&
163
+ prev.key === hit.key &&
164
+ prev.label === hit.label)
165
+ ? prev
166
+ : hit);
167
+ }, []);
168
+ // Rows report their per-slot gutter widths; we reserve each slot's max.
169
+ const [gutters, setGutters] = useState([]);
170
+ const registerGutter = useCallback((req) => {
171
+ setGutters((g) => [...g, req]);
172
+ return () => setGutters((g) => g.filter((x) => x !== req));
173
+ }, []);
174
+ // Rows register on mount so we can mark the first (topmost) one — the shared
175
+ // cursor-time chip shows there only. Effect order = mount order = top-to-bottom
176
+ // for siblings, so the first row registers first (`rowKeys[0]`); this is robust
177
+ // even when rows are wrapped in a fragment/helper component, where an index
178
+ // injected into our direct children wouldn't reach through the wrapper.
179
+ const [rowKeys, setRowKeys] = useState([]);
180
+ const registerRow = useCallback((key) => {
181
+ setRowKeys((k) => (k.includes(key) ? k : [...k, key]));
182
+ return () => setRowKeys((k) => k.filter((x) => x !== key));
183
+ }, []);
184
+ const firstRowKey = rowKeys[0] ?? null;
185
+ const leftSlots = useMemo(() => maxSlotWidths(gutters.map((g) => g.left)), [gutters]);
186
+ const rightSlots = useMemo(() => maxSlotWidths(gutters.map((g) => g.right)), [gutters]);
187
+ const leftGutter = sum(leftSlots);
188
+ const rightGutter = sum(rightSlots);
189
+ const plotWidth = Math.max(0, width - leftGutter - rightGutter);
190
+ // The resolved x domain: while panning an explicit domain it's the live view
191
+ // (t0/t1); otherwise the auto-fit extent (→ [0, 1] before any layer registers,
192
+ // the two-pass settle). This is what the scale + cursor + axis read.
193
+ const [d0, d1] = explicitDomain !== undefined ? [t0, t1] : (autoExtent ?? [0, 1]);
194
+ // The shared x scale + the formatter for its ticks / cursor readout, built
195
+ // together so each branch keeps its concrete scale type (no casts): a value
196
+ // axis is a `scaleLinear` formatted by `resolveAxisFormat`, time is a
197
+ // `scaleTime` formatted by d3's multi-scale `resolveTimeFormat`. `formatTime`
198
+ // is the one formatter <TimeAxis> + the cursor readout share, so a tick and
199
+ // the cursor read identically. (The `formatTime` name predates the value axis
200
+ // — on a value axis it formats the value, not a time.)
201
+ const { xScale, formatTime } = useMemo(() => {
202
+ if (resolvedKind === 'value') {
203
+ const s = scaleLinear().domain([d0, d1]).range([0, plotWidth]);
204
+ return {
205
+ xScale: s,
206
+ formatTime: resolveAxisFormat(s, TIME_TICK_COUNT, timeFormat),
207
+ };
208
+ }
209
+ const s = scaleTime().domain([d0, d1]).range([0, plotWidth]);
210
+ return {
211
+ xScale: s,
212
+ formatTime: resolveTimeFormat(s, TIME_TICK_COUNT, timeFormat),
213
+ };
214
+ }, [resolvedKind, d0, d1, plotWidth, timeFormat]);
215
+ // The crosshair pixel (see resolveCursorX). A stored hoverX is a *plot* pixel;
216
+ // if plotWidth changes mid-hover (a gutter reserving, or a width change) it's
217
+ // briefly stale until the next pointer move — rare, and the bounds check below
218
+ // hides an out-of-plot crosshair meanwhile.
219
+ const cursorX = resolveCursorX(trackerPosition, hoverX, xScale);
220
+ // Emit { time, values } for an outside readout — recomputed as the cursor moves
221
+ // *or* the window slides under it (xScale change → new time at the same pixel).
222
+ // Out of the plot (null, or a controlled trackerPosition d3 extrapolated past
223
+ // the edges) → no readout, matching the hidden overlay; the ref guard keeps a
224
+ // not-hovering live chart from spamming `null`.
225
+ const lastNullRef = useRef(false);
226
+ useEffect(() => {
227
+ const cb = onTrackerRef.current;
228
+ if (cb === undefined)
229
+ return;
230
+ if (cursorX === null || cursorX < 0 || cursorX > plotWidth) {
231
+ if (!lastNullRef.current)
232
+ cb(null);
233
+ lastNullRef.current = true;
234
+ return;
235
+ }
236
+ lastNullRef.current = false;
237
+ const time = +xScale.invert(cursorX);
238
+ const values = Array.from(sources.values()).flatMap((s) => s.sampleAt(time));
239
+ cb({ time, values });
240
+ }, [cursorX, xScale, sources, plotWidth]);
241
+ const frame = useMemo(() => ({
242
+ timeRange: [d0, d1],
243
+ width,
244
+ theme: theme ?? defaultTheme,
245
+ plotWidth,
246
+ leftSlots,
247
+ rightSlots,
248
+ leftGutter,
249
+ rightGutter,
250
+ rowGap,
251
+ cursorX,
252
+ setHoverX,
253
+ selected: selectedValue,
254
+ select,
255
+ hovered,
256
+ setHovered,
257
+ cursor,
258
+ cursorTime,
259
+ formatTime,
260
+ registerTrackerSource,
261
+ unregisterTrackerSource,
262
+ xScale,
263
+ xKind: resolvedKind,
264
+ panZoom,
265
+ minDuration,
266
+ applyRange,
267
+ registerGutter,
268
+ registerRow,
269
+ firstRowKey,
270
+ }), [
271
+ d0,
272
+ d1,
273
+ width,
274
+ theme,
275
+ plotWidth,
276
+ leftSlots,
277
+ rightSlots,
278
+ leftGutter,
279
+ rightGutter,
280
+ rowGap,
281
+ cursorX,
282
+ selectedValue,
283
+ select,
284
+ hovered,
285
+ setHovered,
286
+ cursor,
287
+ cursorTime,
288
+ formatTime,
289
+ registerTrackerSource,
290
+ unregisterTrackerSource,
291
+ xScale,
292
+ resolvedKind,
293
+ panZoom,
294
+ minDuration,
295
+ applyRange,
296
+ registerGutter,
297
+ registerRow,
298
+ firstRowKey,
299
+ ]);
300
+ return (_jsx(ContainerContext.Provider, { value: frame, children: _jsxs("div", { style: { width: `${width}px` }, children: [_jsx("div", { style: {
301
+ display: 'flex',
302
+ flexDirection: 'column',
303
+ gap: `${rowGap}px`,
304
+ }, children: children }), showAxis && _jsx(TimeAxis, {})] }) }));
305
+ }
306
+ //# sourceMappingURL=ChartContainer.js.map
@@ -0,0 +1,29 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type CursorMode } from './context.js';
3
+ export interface ChartRowProps {
4
+ /** Row height in CSS pixels. */
5
+ height: number;
6
+ /** Cursor presentation for this row, overriding the container's default
7
+ * ({@link ChartContainerProps.cursor}). Omit to inherit. See {@link CursorMode}. */
8
+ cursor?: CursorMode;
9
+ children?: ReactNode;
10
+ }
11
+ /**
12
+ * A horizontal band sharing the container's time axis. `ChartRow` owns the
13
+ * **horizontal layout** (axes left/right around a `<Layers>` plot area) and
14
+ * coordinates the row's two registries — axes (`<YAxis>`) and draw layers
15
+ * (`<LineChart>`, registered through `<Layers>`). From the layers it derives a
16
+ * y-scale **per axis** (each axis auto-fits the layers linked to it, or uses its
17
+ * explicit `[min, max]`), and provides them via context.
18
+ *
19
+ * The x geometry (plot width, time scale) is shared and lives on the
20
+ * {@link ChartContainer}: the row reports its per-slot gutter widths so the
21
+ * container can reserve each slot's max, then sizes each axis to its slot and
22
+ * pads the outer slots it lacks, so its plot left-aligns with every other row
23
+ * under the one time axis.
24
+ *
25
+ * Children lay out left-to-right in author order, so `<YAxis side="left"/>` goes
26
+ * before `<Layers/>` and `<YAxis side="right"/>` after.
27
+ */
28
+ export declare function ChartRow({ height, cursor, children }: ChartRowProps): import("react/jsx-runtime").JSX.Element;
29
+ //# sourceMappingURL=ChartRow.d.ts.map
@@ -0,0 +1,215 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, cloneElement, isValidElement, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
3
+ import { scaleLinear } from 'd3-scale';
4
+ import { resolveYDomain } from './domain.js';
5
+ import { resolveAxisFormat } from './format.js';
6
+ import { placeAxisSlots } from './slots.js';
7
+ import { useSlotKey } from './use-slot-key.js';
8
+ import { YAxis } from './YAxis.js';
9
+ import { ContainerContext, RowContext, } from './context.js';
10
+ /** Sentinel id for the implicit axis a row gets when no `<YAxis>` is declared. */
11
+ const IMPLICIT_AXIS_ID = '__default__';
12
+ /** Axis tick count for the per-axis formatter — matches `<YAxis>`'s tick count
13
+ * so the readout formatter is calibrated exactly as the labels are. */
14
+ const AXIS_TICK_COUNT = 5;
15
+ /**
16
+ * A horizontal band sharing the container's time axis. `ChartRow` owns the
17
+ * **horizontal layout** (axes left/right around a `<Layers>` plot area) and
18
+ * coordinates the row's two registries — axes (`<YAxis>`) and draw layers
19
+ * (`<LineChart>`, registered through `<Layers>`). From the layers it derives a
20
+ * y-scale **per axis** (each axis auto-fits the layers linked to it, or uses its
21
+ * explicit `[min, max]`), and provides them via context.
22
+ *
23
+ * The x geometry (plot width, time scale) is shared and lives on the
24
+ * {@link ChartContainer}: the row reports its per-slot gutter widths so the
25
+ * container can reserve each slot's max, then sizes each axis to its slot and
26
+ * pads the outer slots it lacks, so its plot left-aligns with every other row
27
+ * under the one time axis.
28
+ *
29
+ * Children lay out left-to-right in author order, so `<YAxis side="left"/>` goes
30
+ * before `<Layers/>` and `<YAxis side="right"/>` after.
31
+ */
32
+ export function ChartRow({ height, cursor, children }) {
33
+ const container = useContext(ContainerContext);
34
+ if (container === null) {
35
+ throw new Error('<ChartRow> must be rendered inside a <ChartContainer>');
36
+ }
37
+ // Register on mount so the container can mark the first (topmost) row by
38
+ // mount order — the shared cursor-time chip renders there only.
39
+ const rowKey = useSlotKey();
40
+ const { registerRow } = container;
41
+ useEffect(() => registerRow(rowKey), [registerRow, rowKey]);
42
+ const isFirstRow = container.firstRowKey === rowKey;
43
+ // Keyed by a stable per-instance id (Map preserves insertion order; setting an
44
+ // existing key updates in place). So a re-register on a prop change keeps the
45
+ // entry's slot — the axis-default (first axis) and layer z-order stay stable
46
+ // across updates; only mount/unmount reorders. (registerAxis/Layer return
47
+ // void and update in place — *not* unregister-and-append, which would let a
48
+ // min/max or series change silently rebind axes / reorder the z-stack.)
49
+ const [axes, setAxes] = useState(() => new Map());
50
+ const [layers, setLayers] = useState(() => new Map());
51
+ const registerAxis = useCallback((key, spec) => {
52
+ setAxes((m) => new Map(m).set(key, spec));
53
+ }, []);
54
+ const unregisterAxis = useCallback((key) => {
55
+ setAxes((m) => {
56
+ if (!m.has(key))
57
+ return m;
58
+ const next = new Map(m);
59
+ next.delete(key);
60
+ return next;
61
+ });
62
+ }, []);
63
+ const registerLayer = useCallback((key, entry) => {
64
+ setLayers((m) => new Map(m).set(key, entry));
65
+ }, []);
66
+ const unregisterLayer = useCallback((key) => {
67
+ setLayers((m) => {
68
+ if (!m.has(key))
69
+ return m;
70
+ const next = new Map(m);
71
+ next.delete(key);
72
+ return next;
73
+ });
74
+ }, []);
75
+ // Layers in declaration order (the z-stack) — sorted by their injected JSX
76
+ // index, so order follows the markup regardless of mount timing.
77
+ const layerList = useMemo(() => Array.from(layers.values()).sort((a, b) => a.index - b.index), [layers]);
78
+ // Real declared axes in declaration order (by injected index) — the rendered
79
+ // <YAxis> children, as [slot key, spec] so layout can key off the per-instance
80
+ // symbol (not the data id, which may repeat across a mirror). A row with none
81
+ // gets a single implicit auto-domain axis, for *scaling* only (zero width, not
82
+ // rendered), so it still has a default.
83
+ const realEntries = useMemo(() => Array.from(axes.entries()).sort((a, b) => a[1].index - b[1].index), [axes]);
84
+ const realAxes = useMemo(() => realEntries.map(([, spec]) => spec), [realEntries]);
85
+ const effectiveAxes = useMemo(() => realAxes.length > 0
86
+ ? realAxes
87
+ : [
88
+ {
89
+ id: IMPLICIT_AXIS_ID,
90
+ side: 'left',
91
+ width: 0,
92
+ min: undefined,
93
+ max: undefined,
94
+ format: undefined,
95
+ index: 0,
96
+ },
97
+ ], [realAxes]);
98
+ const defaultAxisId = effectiveAxes[0].id;
99
+ // This row's axes per side (as {key, width}), in slot order (slot 0 = innermost,
100
+ // nearest the plot). Left axes are authored outer→inner so reverse them; right
101
+ // axes are authored inner→outer already. Reported to the container as the
102
+ // per-slot widths it maxes across rows.
103
+ const { leftAxes, rightAxes, ownLeftSlots, ownRightSlots } = useMemo(() => {
104
+ const l = [];
105
+ const r = [];
106
+ for (const [key, spec] of realEntries) {
107
+ (spec.side === 'left' ? l : r).push({ key, width: spec.width });
108
+ }
109
+ return {
110
+ leftAxes: l,
111
+ rightAxes: r,
112
+ ownLeftSlots: l.map((a) => a.width).reverse(),
113
+ ownRightSlots: r.map((a) => a.width),
114
+ };
115
+ }, [realEntries]);
116
+ const { registerGutter } = container;
117
+ const gutterReq = useMemo(() => ({ left: ownLeftSlots, right: ownRightSlots }), [ownLeftSlots, ownRightSlots]);
118
+ // Depend on the *stable* registerGutter (a useCallback) + the memoized req —
119
+ // not the container frame, which is recreated whenever the reservation
120
+ // changes (depending on it would loop register → re-render → re-register).
121
+ useEffect(() => registerGutter(gutterReq), [registerGutter, gutterReq]);
122
+ // Map each axis id to its reserved slot width + the outer-slot padding this
123
+ // row lacks (see placeAxisSlots — slot 0 nearest the plot, pad keeps the plot
124
+ // aligned). Falls back to own width until the container has reserved.
125
+ const containerLeftSlots = container.leftSlots;
126
+ const containerRightSlots = container.rightSlots;
127
+ const { axisSlots, leftPad, rightPad } = useMemo(() => placeAxisSlots(leftAxes, rightAxes, containerLeftSlots, containerRightSlots), [leftAxes, rightAxes, containerLeftSlots, containerRightSlots]);
128
+ // One y-scale per axis. A layer counts toward an axis when its (late-resolved)
129
+ // axis id matches; `resolveYDomain` handles the auto-fit + empty/flat/inverted
130
+ // edges. yExtent() is O(points), so only walk the layers when a bound auto-fits.
131
+ const yScales = useMemo(() => {
132
+ const map = new Map();
133
+ for (const ax of effectiveAxes) {
134
+ const extents = ax.min === undefined || ax.max === undefined
135
+ ? layerList
136
+ .filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
137
+ .map((entry) => entry.layer.yExtent())
138
+ : [];
139
+ const [lo, hi] = resolveYDomain(ax.min, ax.max, extents);
140
+ map.set(ax.id, scaleLinear().domain([lo, hi]).range([height, 0]));
141
+ }
142
+ return map;
143
+ }, [effectiveAxes, layerList, height, defaultAxisId]);
144
+ // A value formatter per axis (its `format` resolved against its scale) — shared
145
+ // by the axis tick labels and the cursor readout so a value reads the same.
146
+ const formats = useMemo(() => {
147
+ const map = new Map();
148
+ for (const ax of effectiveAxes) {
149
+ const sc = yScales.get(ax.id);
150
+ if (sc)
151
+ map.set(ax.id, resolveAxisFormat(sc, AXIS_TICK_COUNT, ax.format));
152
+ }
153
+ return map;
154
+ }, [effectiveAxes, yScales]);
155
+ const frame = useMemo(() => ({
156
+ height,
157
+ cursor,
158
+ isFirstRow,
159
+ yScales,
160
+ formats,
161
+ defaultAxisId,
162
+ axisSlots,
163
+ registerAxis,
164
+ unregisterAxis,
165
+ registerLayer,
166
+ unregisterLayer,
167
+ layers: layerList,
168
+ }), [
169
+ height,
170
+ cursor,
171
+ isFirstRow,
172
+ yScales,
173
+ formats,
174
+ defaultAxisId,
175
+ axisSlots,
176
+ registerAxis,
177
+ unregisterAxis,
178
+ registerLayer,
179
+ unregisterLayer,
180
+ layerList,
181
+ ]);
182
+ // Inject each direct child's JSX position so axes register their declaration
183
+ // order (the default-axis source). `<Layers>` receives an index too (harmless
184
+ // — it's not an axis) and injects its own into the draw layers.
185
+ const indexedChildren = Children.map(children, (child, index) => isValidElement(child)
186
+ ? cloneElement(child, { index })
187
+ : child);
188
+ // Place axes by their `side`, not by JSX author position — so a `side="right"`
189
+ // axis always renders right of the plot (and a left axis left), **consistent
190
+ // with the side-based gutter reservation above**. (Author position only
191
+ // injects the index, which still drives slot order within a side + the
192
+ // default-axis pick.) This makes `side` the single source of truth for both
193
+ // placement *and* reserved space; mis-authoring can no longer desync them
194
+ // (the bug: a right axis authored before `<Layers>` rendered left while its
195
+ // gutter was reserved right). Non-axis children (`<Layers>`) stay in the middle.
196
+ const leftAxisEls = [];
197
+ const plotEls = [];
198
+ const rightAxisEls = [];
199
+ for (const child of indexedChildren ?? []) {
200
+ if (isValidElement(child) && child.type === YAxis) {
201
+ const side = child.props.side ?? 'left';
202
+ (side === 'right' ? rightAxisEls : leftAxisEls).push(child);
203
+ }
204
+ else {
205
+ plotEls.push(child);
206
+ }
207
+ }
208
+ return (_jsx(RowContext.Provider, { value: frame, children: _jsxs("div", { style: {
209
+ display: 'flex',
210
+ flexDirection: 'row',
211
+ width: `${container.width}px`,
212
+ height: `${height}px`,
213
+ }, children: [leftPad > 0 && _jsx("div", { style: { flex: `0 0 ${leftPad}px` } }), leftAxisEls, plotEls, rightAxisEls, rightPad > 0 && _jsx("div", { style: { flex: `0 0 ${rightPad}px` } })] }) }));
214
+ }
215
+ //# sourceMappingURL=ChartRow.js.map
@@ -0,0 +1,22 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface LayersProps {
3
+ children?: ReactNode;
4
+ }
5
+ /**
6
+ * The plot area of a {@link ChartRow}: a single `<canvas>` plus the draw-layer
7
+ * registry. It is the boundary where the row's horizontal layout flips to
8
+ * z-stacking — child layers ({@link LineChart}, …) register here and paint into
9
+ * the one canvas, each with its own axis's y-scale (looked up by the layer's
10
+ * `axis` id, defaulting to the row's default axis).
11
+ *
12
+ * **Z-order — declaration order, last child on top** (SVG / DOM / RTC). A row is
13
+ * authored back-to-front: `<BandChart/>` then `<LineChart/>` puts the line over
14
+ * its band. Order comes from each child's **injected JSX index** (so the stack
15
+ * follows the markup regardless of mount timing — a layer toggled in between two
16
+ * others slots into place, not onto the top), and each layer keeps a stable,
17
+ * id-keyed slot so a series/style update holds its position (no jump to the
18
+ * front — the trap that bites live charts). Draw layers must be **direct
19
+ * children** of `<Layers>` for the index to reach them.
20
+ */
21
+ export declare function Layers({ children }: LayersProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=Layers.d.ts.map