@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,72 @@
1
+ import type { SeriesSchema, TimeSeries } from 'pond-ts';
2
+ export interface BarChartProps<S extends SeriesSchema> {
3
+ /**
4
+ * The source series. **Interval / timeRange-keyed** is the primary form — each
5
+ * event's key `[begin, end]` is a bar's x-span. A **point-keyed** (`time`)
6
+ * series is supported too: each bar's width is derived from neighbour spacing
7
+ * (see {@link barsFromTimeSeries}).
8
+ */
9
+ series: TimeSeries<S>;
10
+ /** Name of the numeric value column for the bar height. */
11
+ column: string;
12
+ /**
13
+ * The series' semantic identifier — what the data _is_ / how it should read.
14
+ * The theme maps it to a {@link BarStyle} (`theme.bar[as] ?? theme.bar.default`).
15
+ * **Omitted ⇒ the `default` style** — `column` is the data, `as` is the
16
+ * identity, and there's no per-component colour override (the single styling
17
+ * channel; restyle via the theme).
18
+ */
19
+ as?: string;
20
+ /**
21
+ * Which `<YAxis>` (by its `id`) this bar scales against — picks the *scale*,
22
+ * where `as` picks the *style* (separate concerns). **Omitted ⇒ the row's
23
+ * default axis.**
24
+ */
25
+ axis?: string;
26
+ /**
27
+ * Pixel gap between adjacent bars — each bar's key span is inset by this total
28
+ * (half each side), so neighbours breathe. **Omitted ⇒ the theme's
29
+ * `bar[as].gap`.** A span the gap would invert collapses to the style's
30
+ * `minWidth`, so a too-thin bucket stays visible.
31
+ */
32
+ gap?: number;
33
+ /**
34
+ * @internal Declaration position among the `<Layers>` children, injected by
35
+ * `Layers` so z-order follows JSX order. Do not set.
36
+ */
37
+ index?: number;
38
+ }
39
+ /**
40
+ * A bar draw layer: one rectangle per event, spanning the key's `[begin, end]`
41
+ * (inset by `gap`) from the axis baseline to a numeric `column`'s value. Reads
42
+ * the key endpoints + column into a {@link BarSeries}, registers into the
43
+ * enclosing {@link Layers} (scaling against its `axis`), and renders nothing to
44
+ * the DOM — the row draws it. A gap (missing value) is skipped (no bar).
45
+ *
46
+ * **Baseline.** Bars rest on the zero line when the axis domain spans zero (the
47
+ * common all-positive auto-fit case — {@link barExtent} pulls `0` into the
48
+ * domain), or on the axis floor when an explicit `<YAxis min={…}>` sits above
49
+ * zero (see {@link resolveBarBaseline}).
50
+ *
51
+ * **Interaction.** Hover joins the tracker (`sampleAt` → the value of the bar
52
+ * **under the cursor**) and lights that bar (hover-highlight). Click selects the
53
+ * hit bar (`hitTest`); the matching bar — same key **and** this series' `label`,
54
+ * so two series sharing a timestamp don't both light up — draws highlighted
55
+ * (outlined for the committed select, fill-only for the transient hover). Both
56
+ * resolve by **containment**: the tracker by the bar's `[begin, end]` time span
57
+ * (`barIndexAtTime`), the click by the bar's pixel rect (`barAt`) — so the
58
+ * readout reads the same bar you click, even across a wide bucket (they differ
59
+ * only by the `gap` inset, where the pixel rect is narrower than the span).
60
+ *
61
+ * **Distance domain is deferred** — v1 bars scale on the shared **time** xScale
62
+ * only. estela's distance-domain (records over a monotonic value axis) needs
63
+ * value-axis support that isn't built yet (charts RFC perf section).
64
+ *
65
+ * ```tsx
66
+ * <Layers>
67
+ * <BarChart series={hourlyVolume} column="count" />
68
+ * </Layers>
69
+ * ```
70
+ */
71
+ export declare function BarChart<S extends SeriesSchema>({ series, column, as: semantic, axis, gap, index, }: BarChartProps<S>): null;
72
+ //# sourceMappingURL=BarChart.d.ts.map
@@ -0,0 +1,137 @@
1
+ import { useContext, useEffect, useMemo } from 'react';
2
+ import { barsFromTimeSeries } from './data.js';
3
+ import { barAt, barExtent, barIndexAtTime, drawBars, resolveBarBaseline, } from './bars.js';
4
+ import { ContainerContext, LayersContext, } from './context.js';
5
+ import { useSlotKey } from './use-slot-key.js';
6
+ /**
7
+ * A bar draw layer: one rectangle per event, spanning the key's `[begin, end]`
8
+ * (inset by `gap`) from the axis baseline to a numeric `column`'s value. Reads
9
+ * the key endpoints + column into a {@link BarSeries}, registers into the
10
+ * enclosing {@link Layers} (scaling against its `axis`), and renders nothing to
11
+ * the DOM — the row draws it. A gap (missing value) is skipped (no bar).
12
+ *
13
+ * **Baseline.** Bars rest on the zero line when the axis domain spans zero (the
14
+ * common all-positive auto-fit case — {@link barExtent} pulls `0` into the
15
+ * domain), or on the axis floor when an explicit `<YAxis min={…}>` sits above
16
+ * zero (see {@link resolveBarBaseline}).
17
+ *
18
+ * **Interaction.** Hover joins the tracker (`sampleAt` → the value of the bar
19
+ * **under the cursor**) and lights that bar (hover-highlight). Click selects the
20
+ * hit bar (`hitTest`); the matching bar — same key **and** this series' `label`,
21
+ * so two series sharing a timestamp don't both light up — draws highlighted
22
+ * (outlined for the committed select, fill-only for the transient hover). Both
23
+ * resolve by **containment**: the tracker by the bar's `[begin, end]` time span
24
+ * (`barIndexAtTime`), the click by the bar's pixel rect (`barAt`) — so the
25
+ * readout reads the same bar you click, even across a wide bucket (they differ
26
+ * only by the `gap` inset, where the pixel rect is narrower than the span).
27
+ *
28
+ * **Distance domain is deferred** — v1 bars scale on the shared **time** xScale
29
+ * only. estela's distance-domain (records over a monotonic value axis) needs
30
+ * value-axis support that isn't built yet (charts RFC perf section).
31
+ *
32
+ * ```tsx
33
+ * <Layers>
34
+ * <BarChart series={hourlyVolume} column="count" />
35
+ * </Layers>
36
+ * ```
37
+ */
38
+ export function BarChart({ series, column, as: semantic, axis, gap, index = 0, }) {
39
+ const container = useContext(ContainerContext);
40
+ if (container === null) {
41
+ throw new Error('<BarChart> must be rendered inside a <ChartContainer>');
42
+ }
43
+ const layers = useContext(LayersContext);
44
+ if (layers === null) {
45
+ throw new Error('<BarChart> must be rendered inside a <Layers>');
46
+ }
47
+ const bs = useMemo(() => barsFromTimeSeries(series, column), [series, column]);
48
+ // Styling: semantic identifier → theme bar style. The single styling channel.
49
+ const { bar } = container.theme;
50
+ const style = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
51
+ // Series identity for the readout + selection match (the `as` role, else the
52
+ // column name).
53
+ const label = semantic ?? column;
54
+ // The gap prop overrides the theme default; otherwise the style carries it.
55
+ const gapPx = gap ?? style.gap;
56
+ // The current selection, narrowed to what the highlight match needs (key +
57
+ // label). Read here so a selection change re-registers the layer (in the deps)
58
+ // → the data canvas repaints with the highlight. Infrequent (a click).
59
+ const selected = container.selected;
60
+ const selection = useMemo(() => selected === null ? null : { key: selected.key, label: selected.label }, [selected]);
61
+ // The transient hover-highlight, narrowed to the match key (key + label) like
62
+ // the selection. Read here so a hover change re-registers the layer → the data
63
+ // canvas repaints with the lit bar. Deduped in the container, so this only
64
+ // fires on a bar transition (not every pointer move).
65
+ const hoveredMark = container.hovered;
66
+ const hover = useMemo(() => hoveredMark === null
67
+ ? null
68
+ : { key: hoveredMark.key, label: hoveredMark.label }, [hoveredMark]);
69
+ const entry = useMemo(() => ({
70
+ layer: {
71
+ yExtent: () => barExtent(bs),
72
+ xKind: 'time',
73
+ xExtent: () => bs.length === 0 ? null : [bs.begin[0], bs.end[bs.length - 1]],
74
+ sampleAt: (time) => {
75
+ // The flag belongs to the bar **under the cursor** — the bar whose
76
+ // span `[begin, end]` contains `time` (barIndexAtTime), NOT
77
+ // nearest-by-begin (which flips to the next bar past a wide bar's
78
+ // midpoint, landing the flag on the wrong bar). For a point key the
79
+ // span is the neighbour-derived Voronoi cell (`barsFromTimeSeries`
80
+ // widens `begin === end` into one), so the cells tile the axis and a
81
+ // moving cursor always lands in one. Before the first / after the last
82
+ // bar → no readout, matching the line/area tracker.
83
+ if (bs.length === 0)
84
+ return [];
85
+ const i = barIndexAtTime(bs, time);
86
+ if (i < 0)
87
+ return [];
88
+ const v = bs.y[i];
89
+ if (!Number.isFinite(v))
90
+ return []; // a gap bar (missing value) reads nothing
91
+ // Anchor at the bar's **top-centre** (RFC): the span's centre time
92
+ // `(begin + end) / 2` (the bucket mid for an interval key; the Voronoi
93
+ // cell centre — ~on the point — for a point key), at `yScale(value)` =
94
+ // the bar top. A tall bar (top above the flag stack) drops the staff for
95
+ // free (the shared `s.py > stackBottom` rule).
96
+ return [
97
+ {
98
+ x: (bs.begin[i] + bs.end[i]) / 2,
99
+ value: v,
100
+ color: style.fill,
101
+ label,
102
+ },
103
+ ];
104
+ },
105
+ hitTest: (px, py, xScale, yScale) => {
106
+ const baseline = resolveBarBaseline(yScale);
107
+ const hit = barAt(bs, px, py, xScale, yScale, baseline, gapPx, style.minWidth);
108
+ if (hit === null)
109
+ return null;
110
+ const [, begin, value] = hit;
111
+ // key = the bar's begin (its stable identity); colour = the resolved
112
+ // fill; label = this series' identity (so the highlight targets the
113
+ // exact clicked series, not another sharing the timestamp).
114
+ return { key: begin, value, color: style.fill, label };
115
+ },
116
+ draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, style, resolveBarBaseline(yScale), gapPx, label, selection, hover),
117
+ },
118
+ axisId: axis,
119
+ index,
120
+ }), [bs, series, column, style, label, gapPx, selection, hover, axis, index]);
121
+ // A stable per-instance slot (see useSlotKey) keeps this layer's z-position
122
+ // fixed across series/style/selection updates (no jump to the front).
123
+ const slot = useSlotKey();
124
+ useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
125
+ useEffect(() => {
126
+ layers.registerLayer(slot, entry);
127
+ }, [layers, slot, entry]);
128
+ // Also a tracker source: the container fans in this series' value at the
129
+ // cursor for the (outside-the-chart) readout.
130
+ const { registerTrackerSource, unregisterTrackerSource } = container;
131
+ useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
132
+ useEffect(() => {
133
+ registerTrackerSource(slot, entry.layer);
134
+ }, [registerTrackerSource, slot, entry.layer]);
135
+ return null;
136
+ }
137
+ //# sourceMappingURL=BarChart.js.map
@@ -0,0 +1,77 @@
1
+ import type { SeriesSchema, TimeSeries } from 'pond-ts';
2
+ import { type BoxShape } from './box.js';
3
+ export interface BoxPlotProps<S extends SeriesSchema> {
4
+ /** The source series. Its interval key column supplies the time axis (the box
5
+ * x-span is the key's `[begin, end)`). */
6
+ series: TimeSeries<S>;
7
+ /** Name of the numeric column for the lower whisker end (e.g. `p5` / `min`). */
8
+ lower: string;
9
+ /** Name of the numeric column for the box bottom — first quartile (e.g. `p25`). */
10
+ q1: string;
11
+ /** Name of the numeric column for the median line (e.g. `p50`). */
12
+ median: string;
13
+ /** Name of the numeric column for the box top — third quartile (e.g. `p75`). */
14
+ q3: string;
15
+ /** Name of the numeric column for the upper whisker end (e.g. `p95` / `max`). */
16
+ upper: string;
17
+ /**
18
+ * The box series' semantic identifier — what the spread _is_ (e.g. `latency`).
19
+ * The theme maps it to a {@link BoxStyle} (`theme.box[as] ?? theme.box.default`
20
+ * — box fill/outline, median, whisker). **Omitted ⇒ the `default` box style**;
21
+ * there's no per-component colour/style override (restyle via the theme, the
22
+ * single styling channel).
23
+ */
24
+ as?: string;
25
+ /**
26
+ * Which `<YAxis>` (by its `id`) this box scales against — the *scale*, where
27
+ * `as` picks the *style*. **Omitted ⇒ the row's default axis.**
28
+ */
29
+ axis?: string;
30
+ /**
31
+ * Total horizontal inset between adjacent boxes in px (half each side), so they
32
+ * breathe — see `barSpanPx`. **Omitted ⇒ `0`** (boxes fill their interval span
33
+ * edge-to-edge). A box narrower than 1px after the inset collapses to a 1px
34
+ * mark centred in its slot, so a thin bucket stays visible.
35
+ */
36
+ gap?: number;
37
+ /**
38
+ * How each box renders its spread — `'whisker'` (default; thin stems + caps),
39
+ * `'solid'` (the candlestick look: a light outer bar over the full range with a
40
+ * darker inner q1→q3 box, no stems), or `'none'` (the q1→q3 box only, no spread
41
+ * marks). See {@link BoxShape}.
42
+ */
43
+ shape?: BoxShape;
44
+ /** Draw the median (centre) line across each box. Always optional; default
45
+ * `true`. (The `median` prop above names the *column*; this toggles the line.) */
46
+ showMedian?: boolean;
47
+ /**
48
+ * @internal Declaration position among the `<Layers>` children, injected by
49
+ * `Layers` so z-order follows JSX order. Do not set.
50
+ */
51
+ index?: number;
52
+ }
53
+ /**
54
+ * A discrete box-and-whisker draw layer — the bar-chart analog of the variance
55
+ * band. Reads five **pre-computed quantile columns** of `series` (typically a
56
+ * `rolling`/`aggregate` percentile pass — the chart does **not** compute them)
57
+ * into a {@link BoxSeries} and draws one box per key: the q1→q3 box, the median
58
+ * line, and whiskers out to lower/upper, over the key's interval x-span. Gap-aware
59
+ * (a key missing any quantile draws nothing) and registers itself into the
60
+ * enclosing {@link Layers}. Renders nothing to the DOM — the row draws it.
61
+ *
62
+ * There's no baseline — a box is a spread, not a bar to a floor; the y-domain
63
+ * auto-fits the whisker reach (lower→upper).
64
+ *
65
+ * ```tsx
66
+ * <Layers>
67
+ * <BoxPlot
68
+ * series={q}
69
+ * lower="p5" q1="p25" median="p50" q3="p75" upper="p95"
70
+ * as="latency"
71
+ * gap={6}
72
+ * />
73
+ * </Layers>
74
+ * ```
75
+ */
76
+ export declare function BoxPlot<S extends SeriesSchema>({ series, lower, q1, median, q3, upper, as: semantic, axis, gap, shape, showMedian, index, }: BoxPlotProps<S>): null;
77
+ //# sourceMappingURL=BoxPlot.d.ts.map
@@ -0,0 +1,137 @@
1
+ import { useContext, useEffect, useMemo } from 'react';
2
+ import { boxFromTimeSeries } from './data.js';
3
+ import { boxExtent, boxIndexAtTime, drawBox, isFiniteBox, } from './box.js';
4
+ import { ContainerContext, LayersContext, } from './context.js';
5
+ import { useSlotKey } from './use-slot-key.js';
6
+ /** Whisker collapse floor (px) — a too-thin box still draws a 1px mark. */
7
+ const MIN_BOX_WIDTH_PX = 1;
8
+ /**
9
+ * A discrete box-and-whisker draw layer — the bar-chart analog of the variance
10
+ * band. Reads five **pre-computed quantile columns** of `series` (typically a
11
+ * `rolling`/`aggregate` percentile pass — the chart does **not** compute them)
12
+ * into a {@link BoxSeries} and draws one box per key: the q1→q3 box, the median
13
+ * line, and whiskers out to lower/upper, over the key's interval x-span. Gap-aware
14
+ * (a key missing any quantile draws nothing) and registers itself into the
15
+ * enclosing {@link Layers}. Renders nothing to the DOM — the row draws it.
16
+ *
17
+ * There's no baseline — a box is a spread, not a bar to a floor; the y-domain
18
+ * auto-fits the whisker reach (lower→upper).
19
+ *
20
+ * ```tsx
21
+ * <Layers>
22
+ * <BoxPlot
23
+ * series={q}
24
+ * lower="p5" q1="p25" median="p50" q3="p75" upper="p95"
25
+ * as="latency"
26
+ * gap={6}
27
+ * />
28
+ * </Layers>
29
+ * ```
30
+ */
31
+ export function BoxPlot({ series, lower, q1, median, q3, upper, as: semantic, axis, gap = 0, shape = 'whisker', showMedian = true, index = 0, }) {
32
+ const container = useContext(ContainerContext);
33
+ if (container === null) {
34
+ throw new Error('<BoxPlot> must be rendered inside a <ChartContainer>');
35
+ }
36
+ const layers = useContext(LayersContext);
37
+ if (layers === null) {
38
+ throw new Error('<BoxPlot> must be rendered inside a <Layers>');
39
+ }
40
+ const bx = useMemo(() => boxFromTimeSeries(series, { lower, q1, median, q3, upper }), [series, lower, q1, median, q3, upper]);
41
+ // Styling: semantic identifier → theme box style. The single styling channel.
42
+ const { box } = container.theme;
43
+ const style = (semantic !== undefined ? box[semantic] : undefined) ?? box.default;
44
+ const entry = useMemo(() => ({
45
+ layer: {
46
+ yExtent: () => boxExtent(bx),
47
+ xKind: 'time',
48
+ xExtent: () => bx.length === 0 ? null : [bx.x[0], bx.xEnd[bx.length - 1]],
49
+ sampleAt: (time) => {
50
+ // The readout reads the box **under the cursor** (boxIndexAtTime — span
51
+ // containment, not nearest-by-begin which flips past a wide box's
52
+ // midpoint), anchored at the box **centre** `(x + xEnd) / 2`. Outside
53
+ // every box → no readout. Off-chart fan-in only; the in-chart flag is
54
+ // `cursorFlag`.
55
+ if (bx.length === 0)
56
+ return [];
57
+ const i = boxIndexAtTime(bx, time);
58
+ if (i < 0)
59
+ return [];
60
+ const at = (bx.x[i] + bx.xEnd[i]) / 2;
61
+ const samples = [];
62
+ // The median is the primary readout (median colour); the four quantile
63
+ // edges ride the whisker colour, each labelled by its own column. A
64
+ // single non-finite quantile is omitted (a gap key yields nothing — all
65
+ // five missing — but a malformed partial set still reads what it has).
66
+ push(samples, at, bx.upper[i], style.whisker, upper);
67
+ push(samples, at, bx.q3[i], style.whisker, q3);
68
+ push(samples, at, bx.median[i], style.median, median);
69
+ push(samples, at, bx.q1[i], style.whisker, q1);
70
+ push(samples, at, bx.lower[i], style.whisker, lower);
71
+ return samples;
72
+ },
73
+ cursorFlag: (time) => {
74
+ // The in-chart `flag`: all five values on **one** flag at the box's
75
+ // top-centre. The staff rises from `upper` (the mark's top); the values
76
+ // run high→low across one horizontal row (Layers renders them
77
+ // left→right), each coloured to its box piece. All-or-nothing — a gap
78
+ // box (any quantile non-finite, not drawn) shows no flag.
79
+ if (bx.length === 0)
80
+ return null;
81
+ const i = boxIndexAtTime(bx, time);
82
+ if (i < 0 || !isFiniteBox(bx, i))
83
+ return null;
84
+ return {
85
+ x: (bx.x[i] + bx.xEnd[i]) / 2,
86
+ topValue: bx.upper[i],
87
+ lines: [
88
+ { value: bx.upper[i], color: style.whisker, label: upper },
89
+ { value: bx.q3[i], color: style.whisker, label: q3 },
90
+ { value: bx.median[i], color: style.median, label: median },
91
+ { value: bx.q1[i], color: style.whisker, label: q1 },
92
+ { value: bx.lower[i], color: style.whisker, label: lower },
93
+ ],
94
+ };
95
+ },
96
+ draw: (ctx, xScale, yScale) => drawBox(ctx, bx, xScale, yScale, style, gap, MIN_BOX_WIDTH_PX, shape, showMedian),
97
+ },
98
+ axisId: axis,
99
+ index,
100
+ }), [
101
+ bx,
102
+ series,
103
+ lower,
104
+ q1,
105
+ median,
106
+ q3,
107
+ upper,
108
+ style,
109
+ gap,
110
+ shape,
111
+ showMedian,
112
+ axis,
113
+ index,
114
+ ]);
115
+ // Stable per-instance slot (see useSlotKey): keeps this box's z-position +
116
+ // identity across prop updates; the injected index drives the sort.
117
+ const slot = useSlotKey();
118
+ useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
119
+ useEffect(() => {
120
+ layers.registerLayer(slot, entry);
121
+ }, [layers, slot, entry]);
122
+ // Also a tracker source: the container fans in the box quantiles at the cursor
123
+ // for the (outside-the-chart) readout.
124
+ const { registerTrackerSource, unregisterTrackerSource } = container;
125
+ useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
126
+ useEffect(() => {
127
+ registerTrackerSource(slot, entry.layer);
128
+ }, [registerTrackerSource, slot, entry.layer]);
129
+ return null;
130
+ }
131
+ /** Append a tracker sample for a quantile, skipping a non-finite value. */
132
+ function push(out, x, value, color, label) {
133
+ if (typeof value === 'number' && Number.isFinite(value)) {
134
+ out.push({ x, value, color, label });
135
+ }
136
+ }
137
+ //# sourceMappingURL=BoxPlot.js.map
@@ -0,0 +1,37 @@
1
+ import { type CSSProperties } from 'react';
2
+ /**
3
+ * A draw callback. The 2D context it receives is already transformed to device
4
+ * pixels, so draw in **CSS-pixel coordinates** (`0..width`, `0..height`) and it
5
+ * renders crisply at any device-pixel ratio. The context is freshly transformed
6
+ * and cleared before each invocation.
7
+ */
8
+ export type CanvasDraw = (ctx: CanvasRenderingContext2D, width: number, height: number) => void;
9
+ export interface CanvasProps {
10
+ /** CSS width in pixels. */
11
+ width: number;
12
+ /** CSS height in pixels. */
13
+ height: number;
14
+ /** Synchronous draw callback, invoked after every (re)size or prop change. */
15
+ draw: CanvasDraw;
16
+ /**
17
+ * Device-pixel-ratio override (defaults to `window.devicePixelRatio || 1`).
18
+ * Exposed so tests can pin a deterministic backing-buffer size; production
19
+ * callers leave it unset.
20
+ */
21
+ dpr?: number;
22
+ className?: string;
23
+ style?: CSSProperties;
24
+ }
25
+ /**
26
+ * The DPR-aware `<canvas>` primitive every chart draw layer sits on. It sizes
27
+ * the backing buffer to `width*dpr × height*dpr`, keeps the CSS box at
28
+ * `width × height`, applies `setTransform(dpr, …)` so the {@link CanvasDraw}
29
+ * callback works in CSS-pixel coordinates, clears, and calls `draw`.
30
+ *
31
+ * Drawing runs in `useLayoutEffect` (synchronous, before paint) so there is no
32
+ * flash of an unsized or empty canvas. Setting `canvas.width`/`height` resets
33
+ * all context state, so the transform is re-applied on every run — see trap #7
34
+ * in `docs/rfcs/charts.md`.
35
+ */
36
+ export declare function Canvas({ width, height, draw, dpr, className, style, }: CanvasProps): import("react/jsx-runtime").JSX.Element;
37
+ //# sourceMappingURL=Canvas.d.ts.map
package/dist/Canvas.js ADDED
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef } from 'react';
3
+ /**
4
+ * The DPR-aware `<canvas>` primitive every chart draw layer sits on. It sizes
5
+ * the backing buffer to `width*dpr × height*dpr`, keeps the CSS box at
6
+ * `width × height`, applies `setTransform(dpr, …)` so the {@link CanvasDraw}
7
+ * callback works in CSS-pixel coordinates, clears, and calls `draw`.
8
+ *
9
+ * Drawing runs in `useLayoutEffect` (synchronous, before paint) so there is no
10
+ * flash of an unsized or empty canvas. Setting `canvas.width`/`height` resets
11
+ * all context state, so the transform is re-applied on every run — see trap #7
12
+ * in `docs/rfcs/charts.md`.
13
+ */
14
+ export function Canvas({ width, height, draw, dpr, className, style, }) {
15
+ const ref = useRef(null);
16
+ useLayoutEffect(() => {
17
+ const canvas = ref.current;
18
+ if (!canvas)
19
+ return;
20
+ const ratio = dpr ?? (typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1);
21
+ // Resizing the backing buffer clears it and resets context state, so this
22
+ // must happen before the transform + draw.
23
+ canvas.width = Math.round(width * ratio);
24
+ canvas.height = Math.round(height * ratio);
25
+ const ctx = canvas.getContext('2d');
26
+ if (!ctx)
27
+ return; // SSR / headless environments without a 2D backend
28
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
29
+ ctx.clearRect(0, 0, width, height);
30
+ draw(ctx, width, height);
31
+ }, [width, height, dpr, draw]);
32
+ return (_jsx("canvas", { ref: ref, className: className, style: {
33
+ width: `${width}px`,
34
+ height: `${height}px`,
35
+ display: 'block',
36
+ ...style,
37
+ } }));
38
+ }
39
+ //# sourceMappingURL=Canvas.js.map
@@ -0,0 +1,106 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { TimeRange } from 'pond-ts';
3
+ import { type CursorMode, type SelectInfo, type TrackerInfo } from './context.js';
4
+ import { type AxisFormat } from './format.js';
5
+ import { type ChartTheme } from './theme.js';
6
+ export interface ChartContainerProps {
7
+ /**
8
+ * The shared x **domain** `[begin, end]` — a tuple, or a `TimeRange`
9
+ * (`series.timeRange()`). Units follow the data: epoch-ms for a time axis,
10
+ * the value units (distance, …) for a value axis. **Omit to auto-fit** to the
11
+ * rows' extents. The axis *kind* is never taken from here — it's inferred from
12
+ * the data — so a tuple stays a time domain on a time chart.
13
+ */
14
+ range?: readonly [number, number] | TimeRange;
15
+ /** Total width in CSS pixels (plot + axis gutters). */
16
+ width: number;
17
+ /** Vertical space between rows in CSS pixels (not under the axis). Default 0. */
18
+ rowGap?: number;
19
+ /**
20
+ * Auto-render the shared x axis under the rows. **Default `true`.** Set
21
+ * `false` for a bare plot (a sparkline), or when you place your own `<XAxis>`
22
+ * child (e.g. with a label, custom ticks, or on `side="top"`). Named
23
+ * `showAxis` (not `axis`) to avoid clashing with a layer's `axis` prop, which
24
+ * picks *which* `<YAxis>` it scales against — a different axis entirely.
25
+ */
26
+ showAxis?: boolean;
27
+ /**
28
+ * Controlled tracker position (epoch ms) — pins the synced crosshair across
29
+ * rows. Omit for uncontrolled (the chart tracks the pointer itself); pass
30
+ * `null` to force it hidden. See {@link onTrackerChanged}.
31
+ */
32
+ trackerPosition?: number | null;
33
+ /**
34
+ * In-chart cursor presentation — the default for all rows (a row may override
35
+ * via `<ChartRow cursor>`). **Default `'line'`** — the synced vertical line,
36
+ * with values surfaced *outside* the chart via {@link onTrackerChanged}.
37
+ * `'point'` / `'inline'` / `'flag'` add per-series marks; `'none'` hides it.
38
+ * See {@link CursorMode}.
39
+ */
40
+ cursor?: CursorMode;
41
+ /**
42
+ * Fires on pointer move with the hovered time + every series' value there (so
43
+ * you can render a readout outside the chart), and `null` on leave.
44
+ */
45
+ onTrackerChanged?: (info: TrackerInfo | null) => void;
46
+ /**
47
+ * Controlled selection — the selected mark (echo the `onSelect` arg back), or
48
+ * `null`. **Omitted ⇒ uncontrolled** (a click on a selectable layer manages it
49
+ * internally; pass `null` to force nothing selected). Selectable layers
50
+ * (`BarChart`, `BoxPlot`, `ScatterChart`) highlight the mark matching both its
51
+ * key and series — so two series sharing a timestamp don't both light up.
52
+ */
53
+ selected?: SelectInfo | null;
54
+ /**
55
+ * Fires when a selectable layer's mark is clicked, with the hit mark, or `null`
56
+ * when a click misses every mark (clears the selection). Notification only —
57
+ * works in both controlled and uncontrolled mode.
58
+ */
59
+ onSelect?: (hit: SelectInfo | null) => void;
60
+ /**
61
+ * Enable pan/zoom: drag the plot to pan the time range, wheel to zoom around
62
+ * the cursor. **Default off** — so it doesn't capture drag/scroll unless asked.
63
+ */
64
+ panZoom?: boolean;
65
+ /**
66
+ * Controlled view range — fires on pan/zoom with the new `[start, end]`. Wire
67
+ * it back to `range` for a controlled chart; omit for uncontrolled (the
68
+ * container holds the view internally). **Uncontrolled + `panZoom` seeds the
69
+ * internal view from `range` whenever it isn't actively holding one — so
70
+ * toggling `panZoom` on, or a controlled→uncontrolled switch, starts from the
71
+ * current range, not the mount-time one. Once uncontrolled, later `range`
72
+ * changes are ignored so they can't fight the user's pan. To drive the range
73
+ * externally — or to follow a live sliding window — use controlled mode (this
74
+ * callback).**
75
+ */
76
+ onTimeRangeChange?: (range: [number, number]) => void;
77
+ /** Zoom-in floor — the minimum visible duration in ms. Default `1`. */
78
+ minDuration?: number;
79
+ /**
80
+ * Show the cursor's time atop the in-chart readout (when a row's `cursor` draws
81
+ * one). **Default `false`.** Formatted by {@link timeFormat} to match the time
82
+ * axis.
83
+ */
84
+ cursorTime?: boolean;
85
+ /**
86
+ * Time-axis value formatting — a d3 time specifier string (e.g. `'%H:%M'`) or a
87
+ * `(epochMs) => string` function ({@link AxisFormat}); applies to both the time
88
+ * axis labels and the cursor-time readout. **Omitted ⇒ d3's multi-scale time
89
+ * format** (`12 PM`, `12:10`, …).
90
+ */
91
+ timeFormat?: AxisFormat;
92
+ /** Visual theme for all rows; defaults to {@link defaultTheme}. */
93
+ theme?: ChartTheme;
94
+ children?: ReactNode;
95
+ }
96
+ /**
97
+ * The top of the chart layout (react-timeseries-charts-style). Owns the shared
98
+ * **x geometry**: it collects each row's per-slot gutter widths, reserves each
99
+ * slot's max across rows (so the innermost axis aligns column-by-column and
100
+ * every row's plot left-aligns), and from the slot sums derives `plotWidth` and
101
+ * the shared time `xScale`. It renders its rows (separated by `rowGap`) then one
102
+ * {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
103
+ * (`<YAxis>`).
104
+ */
105
+ export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, panZoom, onTimeRangeChange, minDuration, cursor, cursorTime, timeFormat, theme, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
106
+ //# sourceMappingURL=ChartContainer.d.ts.map