@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
package/dist/box.js ADDED
@@ -0,0 +1,140 @@
1
+ import { barSpanPx } from './range.js';
2
+ /** Fraction of the box width the whisker end-caps span (centred on the stem). */
3
+ const WHISKER_CAP_FRACTION = 0.5;
4
+ /**
5
+ * The `[min, max]` vertical extent of the **drawn** boxes — the lowest `lower`
6
+ * whisker and highest `upper` whisker over keys where **all five** quantiles are
7
+ * finite — or `null` if none are. Gap keys (any quantile `NaN`) are excluded,
8
+ * matching what {@link drawBox} draws, so they don't drag the y-domain.
9
+ *
10
+ * Only `lower`/`upper` bound the extent: they are the outermost reach of a key
11
+ * (the whisker ends), so `q1`/`median`/`q3` lie within `[lower, upper]` for any
12
+ * well-formed quantile set and never widen it. (A malformed set where, say,
13
+ * `q3 > upper` would clip — that's an upstream data error, not the chart's to
14
+ * paper over; document, don't defend.)
15
+ */
16
+ export function boxExtent(box) {
17
+ let min = Infinity;
18
+ let max = -Infinity;
19
+ for (let i = 0; i < box.length; i += 1) {
20
+ if (!isFiniteBox(box, i))
21
+ continue;
22
+ const lo = box.lower[i];
23
+ const hi = box.upper[i];
24
+ if (lo < min)
25
+ min = lo;
26
+ if (hi > max)
27
+ max = hi;
28
+ }
29
+ return min === Infinity ? null : [min, max];
30
+ }
31
+ /**
32
+ * The index of the box whose interval `[x, xEnd]` contains `time` — the box
33
+ * **under the cursor** — or `-1` if `time` is in no box. The box analog of
34
+ * `barIndexAtTime`: containment, not nearest-by-`begin` (which flips to the next
35
+ * box past a wide box's midpoint). Boxes are sorted by `x`; at a shared edge the
36
+ * left box wins. A gap box (some quantile non-finite) still owns its span here;
37
+ * the caller drops it on the finiteness check. O(N) over the boxes (view-scale).
38
+ */
39
+ export function boxIndexAtTime(box, time) {
40
+ for (let i = 0; i < box.length; i += 1) {
41
+ if (time >= box.x[i] && time <= box.xEnd[i])
42
+ return i;
43
+ }
44
+ return -1;
45
+ }
46
+ /**
47
+ * Draw a discrete box per key of `box`, mapping data→pixels through
48
+ * `xScale`/`yScale`. The bar-chart analog of {@link drawBand}: each key gets its
49
+ * own mark over its interval x-span (`barSpanPx`, inset by `gapPx` so adjacent
50
+ * boxes breathe), in the chosen {@link BoxShape}:
51
+ *
52
+ * - **`whisker`** (default) — the graded `q1→q3` box fill + outline, two whisker
53
+ * stems with end-caps out to `lower`/`upper`.
54
+ * - **`solid`** — a light outer bar over `lower→upper` (the spread) with a
55
+ * more-prominent inner `q1→q3` box (the same fill at rising opacity — so it
56
+ * reads darker on a light ground, brighter on a dark one), no stems/outline.
57
+ * - **`none`** — the `q1→q3` box fill + outline only, no spread marks.
58
+ *
59
+ * Then, if `showMedian`, the median line across the box on top. Fills are
60
+ * bracketed by `save`/`restore` so their `globalAlpha` doesn't leak.
61
+ *
62
+ * **Gap-aware**: a key with any quantile non-finite is skipped entirely (no
63
+ * partial box) — the same contract as a band gap.
64
+ *
65
+ * O(N) over the keys, a fixed number of path ops each — no per-key allocation
66
+ * beyond the `barSpanPx` tuple.
67
+ */
68
+ export function drawBox(ctx, box, xScale, yScale, style, gapPx = 0, minWidthPx = 1, shape = 'whisker', showMedian = true) {
69
+ for (let i = 0; i < box.length; i += 1) {
70
+ if (!isFiniteBox(box, i))
71
+ continue;
72
+ const [x0, x1] = barSpanPx(box.x[i], box.xEnd[i], xScale, gapPx, minWidthPx);
73
+ const mid = (x0 + x1) / 2;
74
+ const yLower = yScale(box.lower[i]);
75
+ const yQ1 = yScale(box.q1[i]);
76
+ const yMedian = yScale(box.median[i]);
77
+ const yQ3 = yScale(box.q3[i]);
78
+ const yUpper = yScale(box.upper[i]);
79
+ if (shape === 'solid') {
80
+ // Candlestick: a light outer bar over the full lower→upper spread, then a
81
+ // more-prominent inner q1→q3 box on top (same fill at rising opacity, so the
82
+ // inner reads darker on a light ground / brighter on a dark one). No stems,
83
+ // no outline.
84
+ ctx.save();
85
+ ctx.fillStyle = style.fill;
86
+ ctx.globalAlpha = style.fillOpacity;
87
+ ctx.fillRect(x0, yUpper, x1 - x0, yLower - yUpper);
88
+ ctx.globalAlpha = Math.min(1, style.fillOpacity * 2);
89
+ ctx.fillRect(x0, yQ3, x1 - x0, yQ1 - yQ3);
90
+ ctx.restore();
91
+ }
92
+ else {
93
+ // `whisker` / `none`: the graded q1→q3 box fill + outline.
94
+ ctx.save();
95
+ ctx.fillStyle = style.fill;
96
+ ctx.globalAlpha = style.fillOpacity;
97
+ ctx.fillRect(x0, yQ3, x1 - x0, yQ1 - yQ3);
98
+ ctx.restore();
99
+ ctx.strokeStyle = style.stroke;
100
+ ctx.lineWidth = style.strokeWidth;
101
+ ctx.strokeRect(x0, yQ3, x1 - x0, yQ1 - yQ3);
102
+ if (shape === 'whisker') {
103
+ // Whiskers: a stem from each box edge to the whisker end, with a cap.
104
+ const capHalf = ((x1 - x0) * WHISKER_CAP_FRACTION) / 2;
105
+ ctx.strokeStyle = style.whisker;
106
+ ctx.lineWidth = style.whiskerWidth;
107
+ ctx.beginPath();
108
+ // Upper: stem q3 → upper, cap at upper.
109
+ ctx.moveTo(mid, yQ3);
110
+ ctx.lineTo(mid, yUpper);
111
+ ctx.moveTo(mid - capHalf, yUpper);
112
+ ctx.lineTo(mid + capHalf, yUpper);
113
+ // Lower: stem q1 → lower, cap at lower.
114
+ ctx.moveTo(mid, yQ1);
115
+ ctx.lineTo(mid, yLower);
116
+ ctx.moveTo(mid - capHalf, yLower);
117
+ ctx.lineTo(mid + capHalf, yLower);
118
+ ctx.stroke();
119
+ }
120
+ }
121
+ // The median line across the box, on top — always optional.
122
+ if (showMedian) {
123
+ ctx.strokeStyle = style.median;
124
+ ctx.lineWidth = style.medianWidth;
125
+ ctx.beginPath();
126
+ ctx.moveTo(x0, yMedian);
127
+ ctx.lineTo(x1, yMedian);
128
+ ctx.stroke();
129
+ }
130
+ }
131
+ }
132
+ /** All five quantiles finite at `i` — i.e. this key is drawn. */
133
+ export function isFiniteBox(box, i) {
134
+ return (Number.isFinite(box.lower[i]) &&
135
+ Number.isFinite(box.q1[i]) &&
136
+ Number.isFinite(box.median[i]) &&
137
+ Number.isFinite(box.q3[i]) &&
138
+ Number.isFinite(box.upper[i]));
139
+ }
140
+ //# sourceMappingURL=box.js.map
package/dist/chip.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { ChartTheme } from './theme.js';
3
+ /**
4
+ * The shared **chip** look — one source of truth for the cursor's value flag and
5
+ * the annotation labels, so a placed label and a cursor flag read as the same
6
+ * object. A filled panel (the theme's `chip.background`) with crisp tabular text
7
+ * and **no outline** — the fill delineates it (a border read as a hard edge on a
8
+ * dark ground). The caller layers on `color` (the series / annotation hue) and the
9
+ * position (`top` + `left`/`right`).
10
+ *
11
+ * Note: on a light theme whose `chip.background` doesn't contrast with the plot
12
+ * ground, the borderless chip needs another delineator (a subtle shadow, or a
13
+ * contrasting chip background) — a token to settle before this ships.
14
+ */
15
+ export declare function flagChipStyle(theme: ChartTheme): CSSProperties;
16
+ /**
17
+ * Horizontal placement for a flag chip beside a vertical pole at plot-x `x`:
18
+ * `FLAG_GAP` to the right, flipping to the left near the right edge so it stays
19
+ * in-plot. Shared by the cursor value flag and the annotation labels so a chip
20
+ * sits **identically** relative to its pole in both.
21
+ */
22
+ export declare function flagChipX(x: number, plotWidth: number): CSSProperties;
23
+ //# sourceMappingURL=chip.d.ts.map
package/dist/chip.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The shared **chip** look — one source of truth for the cursor's value flag and
3
+ * the annotation labels, so a placed label and a cursor flag read as the same
4
+ * object. A filled panel (the theme's `chip.background`) with crisp tabular text
5
+ * and **no outline** — the fill delineates it (a border read as a hard edge on a
6
+ * dark ground). The caller layers on `color` (the series / annotation hue) and the
7
+ * position (`top` + `left`/`right`).
8
+ *
9
+ * Note: on a light theme whose `chip.background` doesn't contrast with the plot
10
+ * ground, the borderless chip needs another delineator (a subtle shadow, or a
11
+ * contrasting chip background) — a token to settle before this ships.
12
+ */
13
+ export function flagChipStyle(theme) {
14
+ return {
15
+ position: 'absolute',
16
+ background: theme.chip?.background,
17
+ borderRadius: '3px',
18
+ padding: '0 4px',
19
+ fontFamily: theme.font.family,
20
+ fontSize: `${theme.font.size}px`,
21
+ fontVariantNumeric: 'tabular-nums',
22
+ whiteSpace: 'nowrap',
23
+ pointerEvents: 'none',
24
+ lineHeight: 1.5,
25
+ };
26
+ }
27
+ /** Gap (px) between a flag chip and its pole — the cursor staff or an annotation's
28
+ * line — so the chip floats just beside the pole rather than sitting on it. */
29
+ const FLAG_GAP = 4;
30
+ /** Past this fraction of the plot, a flag flips to the left of its pole to stay in. */
31
+ const FLAG_FLIP = 0.85;
32
+ /**
33
+ * Horizontal placement for a flag chip beside a vertical pole at plot-x `x`:
34
+ * `FLAG_GAP` to the right, flipping to the left near the right edge so it stays
35
+ * in-plot. Shared by the cursor value flag and the annotation labels so a chip
36
+ * sits **identically** relative to its pole in both.
37
+ */
38
+ export function flagChipX(x, plotWidth) {
39
+ return x > plotWidth * FLAG_FLIP
40
+ ? { right: `${plotWidth - x + FLAG_GAP}px` }
41
+ : { left: `${x + FLAG_GAP}px` };
42
+ }
43
+ //# sourceMappingURL=chip.js.map
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ // @pond-ts/charts ships as ES modules only. This stub is the `require` target
4
+ // in the package's `exports` map so that CommonJS consumers get a clear,
5
+ // actionable error instead of Node's cryptic `ERR_PACKAGE_PATH_NOT_EXPORTED`.
6
+ //
7
+ // It is copied verbatim into `dist/` during `prepack` (see package.json) so it
8
+ // rides along in the published tarball; the source of truth lives at the
9
+ // package root and is never touched by `tsc`.
10
+
11
+ throw new Error(
12
+ '@pond-ts/charts is an ES module package and cannot be loaded with ' +
13
+ "require(). Use `import { ChartContainer } from '@pond-ts/charts'` instead, " +
14
+ "or a dynamic `await import('@pond-ts/charts')` from CommonJS. See " +
15
+ 'https://nodejs.org/api/esm.html.',
16
+ );
@@ -0,0 +1,362 @@
1
+ import type { ScaleLinear, ScaleTime } from 'd3-scale';
2
+ import type { ChartTheme } from './theme.js';
3
+ import type { AxisFormat } from './format.js';
4
+ /**
5
+ * The frame a {@link ChartContainer} provides to its rows and the time axis.
6
+ * The container owns the **shared x geometry**: each side is split into *slots*
7
+ * (one axis column each, indexed from the plot outward — slot 0 nearest the
8
+ * plot), and the container reserves each slot's max width across rows (see
9
+ * {@link GutterReq}). The slot sums (`leftGutter`/`rightGutter`) are uniform, so
10
+ * every row's plot left-aligns under one time axis; `plotWidth` and the shared
11
+ * time→pixel `xScale` follow. Y scales stay per-row (row-local data), on the
12
+ * {@link RowFrame}.
13
+ */
14
+ export interface ContainerFrame {
15
+ readonly timeRange: readonly [number, number];
16
+ readonly width: number;
17
+ readonly theme: ChartTheme;
18
+ /** Plot width in px after the gutters — shared by every row. */
19
+ readonly plotWidth: number;
20
+ /**
21
+ * Reserved width of each left/right slot, slot 0 nearest the plot. A row
22
+ * aligns its axis toward the plot within its slot's reserved width and pads
23
+ * the outer slots it lacks. `leftGutter`/`rightGutter` are the sums.
24
+ */
25
+ readonly leftSlots: readonly number[];
26
+ readonly rightSlots: readonly number[];
27
+ /** Total reserved gutter each side (sum of the slot widths) — the plot offsets. */
28
+ readonly leftGutter: number;
29
+ readonly rightGutter: number;
30
+ /** Vertical space between rows in px (not under the time axis). */
31
+ readonly rowGap: number;
32
+ /**
33
+ * The crosshair's **plot-pixel x** (`0..plotWidth`), shared across rows so the
34
+ * tracker syncs, or `null` when not hovering. A *pixel*, not a timestamp — so a
35
+ * still cursor stays put while a live window slides under it (a stored
36
+ * timestamp would drift sideways as `xScale` changes). A controlled
37
+ * `trackerPosition` (a timestamp) resolves to a pixel here.
38
+ */
39
+ readonly cursorX: number | null;
40
+ /** Set the hovered plot-pixel x; a row's event surface calls this on pointer move. */
41
+ setHoverX(x: number | null): void;
42
+ /**
43
+ * The selected mark, or `null`. Shared across rows (single selection). A layer
44
+ * highlights the mark matching **both** the key (epoch ms) and the series
45
+ * (`label`) — so two series sharing a timestamp don't both light up. A
46
+ * controlled `selected` prop pins it; otherwise a click on a selectable layer
47
+ * sets it. The full {@link SelectInfo} (not just the key) is the identity so
48
+ * multi-series Bar/Scatter can target the exact clicked mark.
49
+ */
50
+ readonly selected: SelectInfo | null;
51
+ /**
52
+ * Select a mark, or `null` to clear — a row's click surface calls this after
53
+ * hit-testing its layers. Always fires `onSelect`; manages the internal
54
+ * selection only when uncontrolled (no `selectedKey` prop). The split mirrors
55
+ * the tracker's `trackerPosition` (controlled by a *value* prop) + its
56
+ * `onTrackerChanged` notification — not `applyRange`, which is controlled by
57
+ * the presence of a *callback*.
58
+ */
59
+ select(hit: SelectInfo | null): void;
60
+ /**
61
+ * The **hovered** mark, or `null` — the transient hover-highlight, distinct
62
+ * from the committed `selected`. A row's pointer-move surface hit-tests its
63
+ * selectable layers and sets it; a layer that supports hover-highlight (Bar)
64
+ * draws the matching mark lit (a lighter treatment than `selected`'s outline).
65
+ * Set-on-change (deduped by key+label) so the data canvas repaints only on a
66
+ * mark transition, not every pointer move.
67
+ */
68
+ readonly hovered: SelectInfo | null;
69
+ /** Set the hovered mark (or `null` to clear) from a pointer-move hit-test;
70
+ * deduped, so an unchanged mark is a no-op (no repaint). */
71
+ setHovered(hit: SelectInfo | null): void;
72
+ /** The default in-chart cursor presentation for all rows ({@link CursorMode});
73
+ * a row may override it via its own `cursor`. */
74
+ readonly cursor: CursorMode;
75
+ /** Show the cursor's time atop the in-chart readout (when a row's cursor draws
76
+ * one), formatted by {@link formatTime} to match the time axis. */
77
+ readonly cursorTime: boolean;
78
+ /** Format an epoch-ms instant the same way the time axis labels its ticks —
79
+ * shared by `<TimeAxis>` and the cursor-time readout. */
80
+ readonly formatTime: (epochMs: number) => string;
81
+ /**
82
+ * Register a draw layer as a tracker source so the container can fan in every
83
+ * series' value at the cursor for `onTrackerChanged`. Keyed by the layer's
84
+ * per-instance slot key; unregister on unmount.
85
+ */
86
+ registerTrackerSource(key: symbol, source: TrackerSource): void;
87
+ unregisterTrackerSource(key: symbol): void;
88
+ /**
89
+ * Shared x→pixel scale, range `[0, plotWidth]`. A d3 `scaleTime` (default) so
90
+ * ticks land on wall-clock boundaries, or a `scaleLinear` when the data is
91
+ * value-keyed (a **value axis** — distance, cumulative work; see {@link xKind}).
92
+ * The domain is the container's resolved `range` (auto-fit if omitted). Both
93
+ * scales are callable
94
+ * (`value → px`) and expose `invert`/`ticks`/`tickFormat`; consumers use only
95
+ * that shared surface (the cursor coerces `invert` via `+`, `<TimeAxis>` keys
96
+ * ticks via `+d`), so either kind drops in.
97
+ */
98
+ readonly xScale: ScaleTime<number, number> | ScaleLinear<number, number>;
99
+ /**
100
+ * The resolved kind of the shared x scale — `'time'` (a `scaleTime`) or
101
+ * `'value'` (a `scaleLinear`), inferred from the layers' data. `<XAxis>` reads
102
+ * it to pick its default tick formatter (a time format vs a number format),
103
+ * and the cursor readout to format the x position.
104
+ */
105
+ readonly xKind: 'time' | 'value';
106
+ /** Pan/zoom enabled — the plot drag-pans and wheel-zooms the shared time range. */
107
+ readonly panZoom: boolean;
108
+ /** Minimum visible duration (ms) — the zoom-in floor. */
109
+ readonly minDuration: number;
110
+ /**
111
+ * Apply a new view range from a pan/zoom gesture. Routes to `onTimeRangeChange`
112
+ * (controlled) or the container's internal view state (uncontrolled). Only
113
+ * called while `panZoom` is on.
114
+ */
115
+ applyRange(range: readonly [number, number]): void;
116
+ /**
117
+ * A row reports its per-slot gutter widths each side; the container reserves
118
+ * each slot's max so every row's plot left-aligns. Returns an unregister fn.
119
+ */
120
+ registerGutter(req: GutterReq): () => void;
121
+ /**
122
+ * A row registers on mount so the container can identify the **first** (top)
123
+ * row by mount/DOM order — its key becomes {@link firstRowKey}. Used to show
124
+ * the shared cursor-time chip once, atop the first row, not repeated per row.
125
+ * Keyed by the row's per-instance `useSlotKey` symbol; returns an unregister fn.
126
+ */
127
+ registerRow(key: symbol): () => void;
128
+ /** The first (topmost) row's key, or `null` before any row has registered. */
129
+ readonly firstRowKey: symbol | null;
130
+ }
131
+ /**
132
+ * A row's per-slot axis widths each side, **slot 0 nearest the plot** (so the
133
+ * innermost axis aligns across rows). A row with `k` axes on a side fills slots
134
+ * `0..k-1`; it has no entry for the outer slots, which it pads.
135
+ */
136
+ export interface GutterReq {
137
+ readonly left: readonly number[];
138
+ readonly right: readonly number[];
139
+ }
140
+ export declare const ContainerContext: import("react").Context<ContainerFrame | null>;
141
+ /**
142
+ * A draw layer ({@link LineChart}, …) registered into a {@link Layers}, paired
143
+ * with the id of the axis it scales against. The row computes a y-scale per
144
+ * axis from the union of its linked layers' extents (or the axis's explicit
145
+ * domain); each layer draws with its own axis's scale.
146
+ */
147
+ export interface RowLayer {
148
+ /** This layer's finite-value `[min, max]`, or `null` if it has none. */
149
+ yExtent(): [number, number] | null;
150
+ /**
151
+ * The **kind of x axis** this layer's data lives on — `'time'` for a
152
+ * `TimeSeries`, `'value'` for a `ValueSeries`. The container infers the one
153
+ * shared x scale from its layers (all must agree — a mix is an error), so the
154
+ * axis kind never needs declaring. See {@link ContainerFrame.xScale}.
155
+ */
156
+ readonly xKind: 'time' | 'value';
157
+ /**
158
+ * This layer's `[min, max]` along the **x** axis (the key / value-axis extent),
159
+ * or `null` if empty. The container unions these to auto-fit the shared x
160
+ * domain when no explicit `range` is given.
161
+ */
162
+ xExtent(): readonly [number, number] | null;
163
+ /**
164
+ * The layer's value(s) at `time` — the nearest sample — for the scrub tracker:
165
+ * one for a line, two (lower/upper) for a band, empty at a gap. Each carries
166
+ * the sample's own `x` (the dot snaps onto the data point) and dot colour.
167
+ */
168
+ sampleAt(time: number): readonly TrackerSample[];
169
+ /**
170
+ * The layer's **consolidated flag** at `time` — several values on **one** flag,
171
+ * each its own colour, anchored to a single point. For chart types whose `flag`
172
+ * cursor is one multi-line flag rather than a chip per series. **Optional:** only
173
+ * {@link BoxPlot} implements it (low/q1/median/q3/high on one flag at the box's
174
+ * top-centre); line/area/bar/scatter omit it and use the per-sample flag from
175
+ * {@link sampleAt}. `null` when nothing is under the cursor. (`sampleAt` still
176
+ * fans the same values to the off-chart readout; `cursorFlag` is the in-chart
177
+ * presentation only.)
178
+ */
179
+ cursorFlag?(time: number): CursorFlag | null;
180
+ /**
181
+ * Hit-test plot-pixel `(px, py)` against this layer's marks for click
182
+ * selection — the select-analog of {@link sampleAt}. Returns the hit mark or
183
+ * `null`. **Optional:** layers without discrete selectable marks (line, band,
184
+ * area) omit it; bar / box / scatter implement it. `xScale`/`yScale` map
185
+ * data→pixels (the row resolves the layer's axis scale, as for `draw`).
186
+ */
187
+ hitTest?(px: number, py: number, xScale: (value: number) => number, yScale: (value: number) => number): SelectInfo | null;
188
+ /** Draw into the plot canvas. `xScale`/`yScale` map data→pixels. */
189
+ draw(ctx: CanvasRenderingContext2D, xScale: (value: number) => number, yScale: (value: number) => number): void;
190
+ }
191
+ /** One tracker readout point — a dot + value the overlay draws at the cursor. */
192
+ export interface TrackerSample {
193
+ /** The sample's time (epoch ms); the dot sits at `xScale(x)`. */
194
+ readonly x: number;
195
+ /** The sample's value (y), placed at the layer's axis `yScale(value)`. */
196
+ readonly value: number;
197
+ /** Dot / label colour — the layer's resolved style colour. */
198
+ readonly color: string;
199
+ /** Series identity (`as` ?? column) — labels the value in a readout. */
200
+ readonly label: string;
201
+ }
202
+ /** One line of a {@link CursorFlag} — a labelled, coloured value. */
203
+ export interface CursorFlagLine {
204
+ readonly value: number;
205
+ readonly color: string;
206
+ readonly label: string;
207
+ }
208
+ /**
209
+ * A consolidated multi-value flag for a {@link RowLayer.cursorFlag} layer (the
210
+ * BoxPlot): several values on **one** flag, anchored to `(x, topValue)` — the
211
+ * mark's centre time and the value its staff rises from (the box top). The lines
212
+ * render left→right in one horizontal row, each in its own colour (matched to its
213
+ * box piece).
214
+ */
215
+ export interface CursorFlag {
216
+ readonly x: number;
217
+ readonly topValue: number;
218
+ readonly lines: readonly CursorFlagLine[];
219
+ }
220
+ /**
221
+ * A source of tracker samples — a draw layer, registered with the container so
222
+ * it can fan in every series' value at the cursor for {@link onTrackerChanged}.
223
+ * Also carries the layer's x-axis {@link RowLayer.xKind} + {@link RowLayer.xExtent}
224
+ * so the container can infer the shared x scale's kind + auto-fit its domain
225
+ * (the source registry is the container's only handle on its layers).
226
+ */
227
+ export interface TrackerSource {
228
+ sampleAt(time: number): readonly TrackerSample[];
229
+ readonly xKind: 'time' | 'value';
230
+ xExtent(): readonly [number, number] | null;
231
+ }
232
+ /**
233
+ * One selected mark — what {@link RowLayer.hitTest} returns and `onSelect`
234
+ * reports. Mirrors {@link TrackerSample}: the mark's key (its stable identity,
235
+ * for controlled selection + highlight matching), value, colour, and series
236
+ * label.
237
+ */
238
+ export interface SelectInfo {
239
+ /** The mark's key as epoch ms (its event's `begin`) — its stable identity. */
240
+ readonly key: number;
241
+ /** The mark's value (the plotted column). */
242
+ readonly value: number;
243
+ /** The mark's resolved style colour. */
244
+ readonly color: string;
245
+ /** Series identity (`as` ?? column) — labels the selection in a readout. */
246
+ readonly label: string;
247
+ }
248
+ /** The hover snapshot handed to `onTrackerChanged` — the cursor time + every
249
+ * series' value there, so a consumer can render the readout outside the chart. */
250
+ export interface TrackerInfo {
251
+ readonly time: number;
252
+ readonly values: readonly TrackerSample[];
253
+ }
254
+ /**
255
+ * The in-chart cursor presentation for a row (the synced vertical line is shared
256
+ * across rows). Exclusive modes — pick one:
257
+ *
258
+ * - `none` — no in-chart cursor.
259
+ * - `line` — the synced vertical line only, no per-series marks (pair with an
260
+ * off-chart readout via {@link onTrackerChanged}).
261
+ * - `point` — a dot on each series at the cursor, no line.
262
+ * - `inline` — dots + a value chip beside each.
263
+ * - `flag` — dots + value flags (a staffed flag from each point; the staff
264
+ * geometry lands in a later phase — for now flags stack at the top).
265
+ */
266
+ export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag';
267
+ /** A registered layer plus the axis id it draws against. */
268
+ export interface LayerEntry {
269
+ readonly layer: RowLayer;
270
+ /**
271
+ * The axis id this layer draws against, or `undefined` for the row's default
272
+ * axis. Resolved late (at scale/draw time), so a layer that mounts before its
273
+ * `<YAxis>` still binds to it.
274
+ */
275
+ readonly axisId: string | undefined;
276
+ /**
277
+ * Declaration position among the `<Layers>` children, injected by the parent
278
+ * (see `Layers`). The row sorts layers by this for z-order, so the stack
279
+ * follows JSX order regardless of mount timing — a layer toggled in between
280
+ * two others slots into place rather than landing on top.
281
+ */
282
+ readonly index: number;
283
+ }
284
+ /** A y-axis declared in a {@link ChartRow} via `<YAxis>`. */
285
+ export interface AxisSpec {
286
+ readonly id: string;
287
+ readonly side: 'left' | 'right';
288
+ /** Gutter width in CSS pixels. */
289
+ readonly width: number;
290
+ /** Explicit domain bounds, or `undefined` to auto-fit linked layers. */
291
+ readonly min: number | undefined;
292
+ readonly max: number | undefined;
293
+ /** Value formatting for the tick labels + the cursor readout ({@link AxisFormat}),
294
+ * or `undefined` for the scale's d3 default. */
295
+ readonly format: AxisFormat | undefined;
296
+ /**
297
+ * Declaration position among the row's children, injected by `ChartRow`. The
298
+ * row sorts axes by this, so the **first declared** axis is the default
299
+ * regardless of which axis last re-rendered.
300
+ */
301
+ readonly index: number;
302
+ }
303
+ /**
304
+ * The frame a {@link ChartRow} provides to its axes (`<YAxis>`) and plot area
305
+ * (`<Layers>`): the row height, the per-axis y-scales, and the registries.
306
+ * `ChartRow` coordinates two registries — axes and layers — and computes a
307
+ * y-scale **per axis id** (range `[height, 0]`). The x geometry (plot width,
308
+ * time scale) is shared and lives on the {@link ContainerFrame}.
309
+ */
310
+ export interface RowFrame {
311
+ readonly height: number;
312
+ readonly yScales: ReadonlyMap<string, ScaleLinear<number, number>>;
313
+ /** Value formatter per axis id (resolved from the axis's {@link AxisSpec.format}
314
+ * against its scale) — used by both the tick labels and the cursor readout, so
315
+ * a value reads identically in both. */
316
+ readonly formats: ReadonlyMap<string, (value: number) => string>;
317
+ /** This row's cursor-mode override, or `undefined` to inherit the container's
318
+ * default ({@link ContainerFrame.cursor}). */
319
+ readonly cursor: CursorMode | undefined;
320
+ /** Whether this is the first (topmost) row — the shared cursor-time chip shows
321
+ * here only, not repeated on every row. Derived from {@link ContainerFrame.firstRowKey}. */
322
+ readonly isFirstRow: boolean;
323
+ /** The axis a layer uses when it names none (the first declared, or implicit). */
324
+ readonly defaultAxisId: string;
325
+ /**
326
+ * Reserved slot width for each axis, keyed by its **instance** slot key (the
327
+ * `useSlotKey` symbol), not its data id — two axes may share an id (a
328
+ * left/right mirror) yet need distinct slots. A `<YAxis>` sizes its box to this
329
+ * and aligns its own narrower content toward the plot, so axes line up
330
+ * column-by-column across rows.
331
+ */
332
+ readonly axisSlots: ReadonlyMap<symbol, number>;
333
+ /**
334
+ * Register or **update** an axis, keyed by a stable per-instance slot key (a
335
+ * `Symbol` from `useSlotKey` — instance identity, not the data `id`). Update
336
+ * is in place — the entry keeps its slot — so a `min`/`max`/`side` change
337
+ * doesn't reorder the axes (the first declared stays the default). Pair with
338
+ * `unregisterAxis(key)` on unmount only.
339
+ */
340
+ registerAxis(key: symbol, spec: AxisSpec): void;
341
+ unregisterAxis(key: symbol): void;
342
+ /** Register or update a draw layer by stable slot key; in-place so a
343
+ * series/style change keeps the layer's z-slot. Unregister on unmount. */
344
+ registerLayer(key: symbol, entry: LayerEntry): void;
345
+ unregisterLayer(key: symbol): void;
346
+ /** Draw layers in stable declaration order — the z-stack, first at the back. */
347
+ readonly layers: readonly LayerEntry[];
348
+ }
349
+ export declare const RowContext: import("react").Context<RowFrame | null>;
350
+ /**
351
+ * The registry a {@link Layers} exposes to its child draw layers — the boundary
352
+ * that makes a layer a layer (children here register; a layer outside `<Layers>`
353
+ * errors). Forwards to the row's layer registry; layers are keyed by a stable
354
+ * per-instance slot key (`useSlotKey`) so re-registering on a prop change
355
+ * updates in place rather than reordering the z-stack.
356
+ */
357
+ export interface LayerRegistry {
358
+ registerLayer(key: symbol, entry: LayerEntry): void;
359
+ unregisterLayer(key: symbol): void;
360
+ }
361
+ export declare const LayersContext: import("react").Context<LayerRegistry | null>;
362
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react';
2
+ export const ContainerContext = createContext(null);
3
+ export const RowContext = createContext(null);
4
+ export const LayersContext = createContext(null);
5
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1,22 @@
1
+ import { type CurveFactory } from 'd3-shape';
2
+ /**
3
+ * Render-time path interpolation for a line / band — how the path is drawn
4
+ * *between* points, a pure view concern (it does not change the data; denoising
5
+ * is pond's `smooth()`, upstream). Sparse aggregated bands look angular as a
6
+ * polyline; a curve smooths the path (RTC's `interpolation`).
7
+ *
8
+ * - `linear` (default) — straight segments; passes through every point.
9
+ * - `monotone` — smooth, monotone in x, **passes through** points (no overshoot;
10
+ * best for **lines**). It assumes *increasing* x, so on a band — whose lower
11
+ * edge is drawn right→left — it smooths the two edges asymmetrically; prefer a
12
+ * symmetric curve for bands.
13
+ * - `natural` — smooth natural cubic spline, passes through points; **symmetric**
14
+ * (direction-independent), so a good band curve.
15
+ * - `basis` — B-spline; smoothest and symmetric, but **approximates** (does not
16
+ * touch points). The classic band/envelope curve.
17
+ * - `step` — right-angle steps.
18
+ */
19
+ export type Curve = 'linear' | 'monotone' | 'natural' | 'basis' | 'step';
20
+ /** Resolve a {@link Curve} name to its d3 curve factory; `undefined` → linear. */
21
+ export declare function resolveCurve(curve?: Curve): CurveFactory;
22
+ //# sourceMappingURL=curve.d.ts.map
package/dist/curve.js ADDED
@@ -0,0 +1,13 @@
1
+ import { curveBasis, curveLinear, curveMonotoneX, curveNatural, curveStep, } from 'd3-shape';
2
+ const CURVES = {
3
+ linear: curveLinear,
4
+ monotone: curveMonotoneX,
5
+ natural: curveNatural,
6
+ basis: curveBasis,
7
+ step: curveStep,
8
+ };
9
+ /** Resolve a {@link Curve} name to its d3 curve factory; `undefined` → linear. */
10
+ export function resolveCurve(curve) {
11
+ return CURVES[curve ?? 'linear'];
12
+ }
13
+ //# sourceMappingURL=curve.js.map