@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Murphy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # pond-ts
2
+
3
+ **Highly optimised, fully typed Timeseries library for TypeScript**
4
+
5
+ Schema-driven events, composable batch transforms, push-based streaming
6
+ ingest, multi-entity partitioning, and an optional React integration —
7
+ all strict TypeScript end to end, all immutable.
8
+
9
+ **pond-ts** is the TypeScript-first successor to
10
+ [pondjs](https://github.com/esnet/pond), rewritten from scratch with a
11
+ focus on type safety, composability, and the live-streaming patterns
12
+ that pondjs never grew.
13
+
14
+ ```sh
15
+ npm install pond-ts # core
16
+ npm install @pond-ts/react # React hooks (optional)
17
+ ```
18
+
19
+ - **Typed schemas** — declare once, every transform downstream narrows
20
+ off it. `event.get('cpu')` returns `number | undefined` straight from
21
+ the schema; no `as` casts.
22
+ - **Batch + streaming with the same vocabulary** — `filter`, `map`,
23
+ `aggregate`, `rolling`, `diff`, `rate`, `fill`, `cumulative`,
24
+ `sample`, `reduce` all exist on both `TimeSeries` and `LiveSeries`.
25
+ - **Multi-entity by construction** — `partitionBy('host')` routes per
26
+ entity; `rolling` / `aggregate` / `fill` / `sample` over a partitioned
27
+ view all become per-entity automatically.
28
+ - **Bounded-memory streaming** — retention policies, eviction-aware
29
+ views, and sampling decouple downstream window length
30
+ from event rate at firehose loads (up to 500k events/sec on a
31
+ single node.js instance.)
32
+ - **Triggers** — for control of rolling emission cadences. Synchronised
33
+ partitioned rolling fires across partitions on every boundary.
34
+ - **Typed column extraction** — `series.column('cpu')` returns a
35
+ schema-narrowed typed column with single-pass reductions
36
+ (`min`/`max`/`sum`/`mean`/`stdev`/`median`/`percentile`/`minMax`),
37
+ index downsampling (`bin`), and a zero-copy `toFloat64Array()` for
38
+ canvas / WebGL draw loops — no per-event allocation on the hot path.
39
+ - **No legacy baggage**
40
+
41
+ ## Quick start: batch
42
+
43
+ ```ts
44
+ import { Sequence, TimeSeries } from 'pond-ts';
45
+
46
+ const schema = [
47
+ { name: 'time', kind: 'time' },
48
+ { name: 'cpu', kind: 'number' },
49
+ { name: 'requests', kind: 'number' },
50
+ { name: 'host', kind: 'string' },
51
+ ] as const;
52
+
53
+ const cpu = TimeSeries.fromJSON({
54
+ name: 'cpu',
55
+ schema,
56
+ rows: [
57
+ ['2025-01-01T00:00:00Z', 0.31, 120, 'host1'],
58
+ ['2025-01-01T00:01:00Z', 0.44, 135, 'host2'],
59
+ ['2025-01-01T00:02:00Z', 0.52, 141, 'host1'],
60
+ ['2025-01-01T00:03:00Z', 0.48, 128, 'host1'],
61
+ ['2025-01-01T00:04:00Z', 0.63, 166, 'host3'],
62
+ ],
63
+ });
64
+
65
+ const byMinute = cpu.aggregate(Sequence.every('1m'), {
66
+ cpu: 'avg',
67
+ requests: 'sum',
68
+ host: 'last',
69
+ });
70
+
71
+ const bands = cpu.baseline('cpu', { window: '2m', sigma: 2 });
72
+ // ^ appends rolling avg / sd / upper / lower in one pass.
73
+
74
+ const anomalies = cpu.outliers('cpu', { window: '2m', sigma: 2 });
75
+ // ^ schema-preserving filter — same columns, just the spikes.
76
+ ```
77
+
78
+ The full batch surface (`align`, `rolling`, `smooth`, `groupBy`, `join`,
79
+ `reduce`, `diff`, `rate`, `fill`, `dedupe`, `materialize`, `sample`,
80
+ `partitionBy`, `pivotByGroup`, …) follows the same shape: TimeSeries
81
+ in, TimeSeries out, schema preserved.
82
+
83
+ ## Quick start: live (streaming)
84
+
85
+ ```ts
86
+ import { LiveSeries, Sequence } from 'pond-ts';
87
+
88
+ // 1. Same schema; this is a live append buffer with retention.
89
+ const live = new LiveSeries({
90
+ name: 'cpu',
91
+ schema,
92
+ retention: { maxAge: '10m' }, // keep only the last 10 minutes
93
+ });
94
+
95
+ // 2. Push as events arrive. Each push is validated against the schema.
96
+ live.push([Date.now(), 0.45, 128, 'api-1']);
97
+
98
+ // 3. Compose live views — incremental, push-driven, eviction-aware.
99
+ const recentAvg = live.rolling('5m', { cpu: 'avg' });
100
+ recentAvg.on('event', (e) => render(e.get('cpu')));
101
+
102
+ // 4. Snapshot to a TimeSeries for batch analytics at any time.
103
+ const snap = live.toTimeSeries();
104
+ ```
105
+
106
+ The full live surface (`filter`, `map`, `select`, `window`, `aggregate`,
107
+ `rolling`, `reduce`, `diff`, `rate`, `pctChange`, `fill`, `cumulative`,
108
+ `sample`) is incremental — events flow, views emit, retention bounds
109
+ memory.
110
+
111
+ ## Quick start: multi-entity
112
+
113
+ `partitionBy` routes events into per-key buffers. Every stateful
114
+ operator downstream of `partitionBy` runs per-partition automatically:
115
+
116
+ ```ts
117
+ const perHost = cpu
118
+ .partitionBy('host')
119
+ .rolling('5m', { cpu: 'avg', cpu_sd: 'stdev' });
120
+
121
+ // .collect() fans the per-partition outputs back into a flat TimeSeries
122
+ // with the partition key auto-injected as a column.
123
+ const flat = perHost.collect();
124
+ ```
125
+
126
+ Same shape on the live side — `live.partitionBy('host')` returns a
127
+ `LivePartitionedSeries` whose `rolling` / `fill` / `diff` / `sample`
128
+ methods all maintain per-partition state.
129
+
130
+ ## Quick start: bounded-memory sampling
131
+
132
+ At firehose rates, a long rolling baseline blows the heap. `sample({
133
+ stride: N })` decouples baseline length from event rate; chain it
134
+ between `partitionBy` and `rolling`:
135
+
136
+ ```ts
137
+ // Per-host 1-in-10 stride feeding a per-host 5m baseline.
138
+ live
139
+ .partitionBy('host')
140
+ .sample({ stride: 10 })
141
+ .rolling('5m', { cpu_avg: 'avg', cpu_sd: 'stdev' });
142
+ ```
143
+
144
+ For visualization, the snapshot side ships reservoir sampling too —
145
+ single-pass Algorithm R, sorted by key, fixed point count regardless of
146
+ source size:
147
+
148
+ ```ts
149
+ const points = series.sample({ reservoir: { size: 500 } }).toRows();
150
+ // 500 uncorrelated points drawn uniformly from the source.
151
+ ```
152
+
153
+ ## Performance
154
+
155
+ pond-ts is faster on **every** comparable operation, with no regressions —
156
+ a **~17x** geometric-mean speedup across the measurable ops, plus a handful
157
+ of transforms (`select` / `rename`) that are **effectively instant** (O(1)
158
+ column rebinds, below the timer's resolution). The advantage grows with data
159
+ size.
160
+
161
+ | Category | Speedup (N=16k) | Notes |
162
+ | ----------------- | ---------------------------------------------------- | --------------------------------------------- |
163
+ | **Rate** | ~120x | Single columnar walk vs Pipeline |
164
+ | **Fill** | 77–87x | Single columnar pass vs Pipeline per strategy |
165
+ | **Aggregation** | 57–82x | O(N+B) bucketing vs O(N×B) Pipeline |
166
+ | **Statistics** | 18–80x | Typed-array reduce vs ImmutableJS iteration |
167
+ | **Alignment** | 42x | Forward cursor vs repeated binary search |
168
+ | **Construction** | 13x | Columnar intake vs ImmutableJS wrapping |
169
+ | **Chained** | 8x | Derived constructors vs per-step Pipeline |
170
+ | **Transforms** | `select`/`rename` instant; `collapse` 30x; `map` ~4x | Column reshapes vs Pipeline |
171
+ | **Event access** | 6x | Array indexing vs ImmutableJS `get()` |
172
+ | **Serialization** | 4x | Lightweight columnar representation |
173
+
174
+ See the [full benchmark results](website/docs/reference/benchmarks.mdx)
175
+ for detailed numbers. Run locally:
176
+
177
+ ```sh
178
+ npm run build && node packages/core/bench/vs-pondjs.cjs
179
+ ```
180
+
181
+ ## Documentation
182
+
183
+ The full guide is at **<https://pjm17971.github.io/pond-ts/>**.
184
+
185
+ - **[Start here](https://pjm17971.github.io/pond-ts/docs/)**
186
+ — five-minute walkthrough with batch, live, and React examples.
187
+ - **[Concepts](https://pjm17971.github.io/pond-ts/docs/start-here/concepts)**
188
+ — temporal keys, sequences, windowing, partitioning, triggers, late
189
+ data.
190
+ - **[Transforms reference](https://pjm17971.github.io/pond-ts/docs/pond-ts/transforms/queries)**
191
+ — every batch operator (queries, aggregation, alignment, rolling,
192
+ smoothing, sampling, cleaning, reshape, anomaly detection).
193
+ - **[Live reference](https://pjm17971.github.io/pond-ts/docs/pond-ts/live/live-series)**
194
+ — `LiveSeries`, live transforms, triggering.
195
+ - **[How-to guides](https://pjm17971.github.io/pond-ts/docs/how-to-guides)**
196
+ — building a dashboard, ingesting messy data.
197
+ - **[API reference (auto-generated)](https://pjm17971.github.io/pond-ts/generated-api/core/)**
198
+ — TypeDoc output, every public class and method.
199
+ - **[CHANGELOG](./CHANGELOG.md)** — what shipped in each release.
200
+
201
+ ## Examples
202
+
203
+ - **[pond-ts-dashboard](https://github.com/pjm17971/pond-ts-dashboard)**
204
+ — a working React dashboard that streams synthetic per-host CPU /
205
+ request metrics, computes per-host rolling baselines, flags anomalies
206
+ against ±σ bands, and renders everything as live line and bar charts
207
+ (~600 lines of TypeScript). Walked through end-to-end in
208
+ [Building a dashboard](website/docs/how-to-guides/dashboard-guide.mdx).
209
+
210
+ ## Develop
211
+
212
+ The repo is an npm-workspaces monorepo with two published packages
213
+ (`pond-ts`, `@pond-ts/react`). Node 18+ for runtime; Node 20+ for the
214
+ docs site (Docusaurus).
215
+
216
+ ```sh
217
+ npm install # one-time, hoists deps for both packages
218
+ npm run build # build both packages
219
+ npm test # runtime + type-level tests on both packages
220
+ npm run format # prettier write across the repo
221
+ npm run verify # format check + build + test (CI parity)
222
+ ```
223
+
224
+ `packages/core/` is the `pond-ts` package; `packages/react/` is
225
+ `@pond-ts/react`. Docs live in `website/`.
226
+
227
+ ## License
228
+
229
+ MIT
@@ -0,0 +1,85 @@
1
+ import type { SeriesSchema, TimeSeries } from 'pond-ts';
2
+ import { type Curve } from './curve.js';
3
+ import { type GapMode } from './gaps.js';
4
+ export interface AreaChartProps<S extends SeriesSchema> {
5
+ /** The source series. Its key column supplies the time axis. */
6
+ series: TimeSeries<S>;
7
+ /** Name of the numeric value column to fill from. */
8
+ column: string;
9
+ /**
10
+ * The series' semantic identifier — what the data _is_ / how it should read
11
+ * (e.g. `elevation`, or a signed-traffic role like `in` / `out`). The theme
12
+ * maps it to an {@link AreaStyle} (`theme.area[as] ?? theme.area.default`) —
13
+ * outline colour/width + the graded fill. **Omitted ⇒ the `default` style**;
14
+ * there's no per-component colour/style override (restyle via the theme, the
15
+ * single styling channel).
16
+ */
17
+ as?: string;
18
+ /**
19
+ * Which `<YAxis>` (by its `id`) this area scales against — picks the *scale*,
20
+ * where `as` picks the *style*. **Omitted ⇒ the row's default axis.**
21
+ */
22
+ axis?: string;
23
+ /**
24
+ * The value the fill rests on — the flat edge opposite the value line. Two
25
+ * forms:
26
+ *
27
+ * - **Omitted ⇒ the axis's lower bound** (the bottom of the plot): the
28
+ * elevation form — fill from the line down to the floor, shade grading down.
29
+ * - **A number (e.g. `0`) ⇒ a fixed baseline**: the above/below-axis form —
30
+ * values above it fill up, below it fill down, each side's shade fading
31
+ * toward the baseline. For the esnet two-colour traffic look, compose two
32
+ * `<AreaChart>`s (an "in" column and an "out" column, distinct `as` roles).
33
+ *
34
+ * A fixed baseline is pulled into the auto-fit domain so the baseline line is
35
+ * always visible (an above/below area with `baseline={0}` shows the zero
36
+ * axis).
37
+ */
38
+ baseline?: number;
39
+ /**
40
+ * Render-time path interpolation for the outline + fill edge — a view concern
41
+ * (denoise the data with pond's `smooth()` upstream). **Omitted ⇒ `'linear'`**
42
+ * (straight segments). `'monotone'` is a smooth edge that still passes through
43
+ * points.
44
+ */
45
+ curve?: Curve;
46
+ /**
47
+ * How a **gap** (a coast / dropout — a run of NaN in `column`) is rendered (a
48
+ * {@link GapMode}). **Omitted ⇒ `'empty'`**: the fill *and* outline break at
49
+ * the gap, leaving a hole (the honest default). `'none'` fills + bridges
50
+ * straight across. For `'dashed'` / `'step'` / `'fade'` the **fill stays
51
+ * broken** and only the **outline** gets the inferred connector across the gap
52
+ * — a faint dashed straight bridge, a faint flat dashed line at the average of
53
+ * the edge values, or estela's fade-to-baseline (which drops to this area's own
54
+ * `baseline`, the fill floor). Shared with `<LineChart>` — one concept. (The
55
+ * `'dashed'` / `'step'` connector faintness is the theme's `gap.connectorOpacity`.)
56
+ */
57
+ gaps?: GapMode;
58
+ /**
59
+ * @internal Declaration position among the `<Layers>` children, injected by
60
+ * `Layers` so z-order follows JSX order. Do not set.
61
+ */
62
+ index?: number;
63
+ }
64
+ /**
65
+ * An area draw layer: fills between a value `column` and a `baseline`, with a
66
+ * graded (gradient) shade — opaque at the line, transparent at the baseline —
67
+ * and an outline stroke on top. Reads `column` into a {@link ChartSeries}
68
+ * (columnar, gaps as NaN), registers into the enclosing {@link Layers} (scaling
69
+ * against its `axis`), and renders nothing to the DOM — the row draws it. The
70
+ * fill + outline break at gaps rather than spanning them.
71
+ *
72
+ * Two forms via `baseline` (see {@link AreaChartProps.baseline}): omit it for
73
+ * the **elevation** form (rest on the axis floor) or pass `0` for the
74
+ * **above/below-axis** form (positive up, negative down). The esnet two-colour
75
+ * traffic look composes two layers, each with its own `as` role:
76
+ *
77
+ * ```tsx
78
+ * <Layers>
79
+ * <AreaChart series={s} column="in" baseline={0} as="in" />
80
+ * <AreaChart series={s} column="out" baseline={0} as="out" />
81
+ * </Layers>
82
+ * ```
83
+ */
84
+ export declare function AreaChart<S extends SeriesSchema>({ series, column, as: semantic, axis, baseline, curve, gaps, index, }: AreaChartProps<S>): null;
85
+ //# sourceMappingURL=AreaChart.d.ts.map
@@ -0,0 +1,119 @@
1
+ import { useContext, useEffect, useMemo } from 'react';
2
+ import { fromTimeSeries } from './data.js';
3
+ import { areaExtent, drawArea } from './area.js';
4
+ import { resolveCurve } from './curve.js';
5
+ import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
6
+ import { ContainerContext, LayersContext } from './context.js';
7
+ import { useSlotKey } from './use-slot-key.js';
8
+ /** Read a d3 linear scale's domain lower bound (the axis floor) from the plain
9
+ * `(value) => pixel` function the row hands to `draw`. The runtime object is a
10
+ * d3 `ScaleLinear` (it carries `.domain()`); the {@link RowLayer} type narrows
11
+ * it to the call signature, so this reads the bound through a localized,
12
+ * documented shape rather than widening `drawArea`'s contract to d3-scale. */
13
+ function domainFloor(yScale) {
14
+ const d = yScale.domain?.();
15
+ return d && d.length > 0 ? d[0] : 0;
16
+ }
17
+ /**
18
+ * An area draw layer: fills between a value `column` and a `baseline`, with a
19
+ * graded (gradient) shade — opaque at the line, transparent at the baseline —
20
+ * and an outline stroke on top. Reads `column` into a {@link ChartSeries}
21
+ * (columnar, gaps as NaN), registers into the enclosing {@link Layers} (scaling
22
+ * against its `axis`), and renders nothing to the DOM — the row draws it. The
23
+ * fill + outline break at gaps rather than spanning them.
24
+ *
25
+ * Two forms via `baseline` (see {@link AreaChartProps.baseline}): omit it for
26
+ * the **elevation** form (rest on the axis floor) or pass `0` for the
27
+ * **above/below-axis** form (positive up, negative down). The esnet two-colour
28
+ * traffic look composes two layers, each with its own `as` role:
29
+ *
30
+ * ```tsx
31
+ * <Layers>
32
+ * <AreaChart series={s} column="in" baseline={0} as="in" />
33
+ * <AreaChart series={s} column="out" baseline={0} as="out" />
34
+ * </Layers>
35
+ * ```
36
+ */
37
+ export function AreaChart({ series, column, as: semantic, axis, baseline, curve, gaps = DEFAULT_GAP_MODE, index = 0, }) {
38
+ const container = useContext(ContainerContext);
39
+ if (container === null) {
40
+ throw new Error('<AreaChart> must be rendered inside a <ChartContainer>');
41
+ }
42
+ const layers = useContext(LayersContext);
43
+ if (layers === null) {
44
+ throw new Error('<AreaChart> must be rendered inside a <Layers>');
45
+ }
46
+ const cs = useMemo(() => fromTimeSeries(series, column), [series, column]);
47
+ // Styling: semantic identifier → theme area style. The single styling channel.
48
+ const { area } = container.theme;
49
+ const style = (semantic !== undefined ? area[semantic] : undefined) ?? area.default;
50
+ // Series identity for the readout (the `as` role, else the column name).
51
+ const label = semantic ?? column;
52
+ const curveFactory = resolveCurve(curve);
53
+ // Faintness of the inferred dashed connectors (dashed / step) — theme-level,
54
+ // falling back to the shared default so a theme without it still renders faint.
55
+ const gapConnectorOpacity = container.theme.gap?.connectorOpacity ?? DEFAULT_GAP_CONNECTOR_OPACITY;
56
+ const entry = useMemo(() => ({
57
+ layer: {
58
+ yExtent: () => areaExtent(cs, baseline),
59
+ xKind: 'time',
60
+ xExtent: () => cs.length === 0 ? null : [cs.x[0], cs.x[cs.length - 1]],
61
+ sampleAt: (time) => {
62
+ // No readout past the data (tracker policy — core's nearest() clamps
63
+ // to an endpoint outside the span); bounds from the columnar time axis.
64
+ if (cs.length === 0 ||
65
+ time < cs.x[0] ||
66
+ time > cs.x[cs.length - 1]) {
67
+ return [];
68
+ }
69
+ const e = series.nearest(time);
70
+ if (e === undefined)
71
+ return [];
72
+ // get() wants a literal key; column is a runtime string. Cast the
73
+ // *event* (not the method — that would detach `this`) to a
74
+ // string-keyed get; runtime-safe read + guard.
75
+ const v = e.get(column);
76
+ // The readout dot rides the value line (not the baseline), coloured by
77
+ // the outline stroke. A gap yields no readout (like the fill).
78
+ return typeof v === 'number' && Number.isFinite(v)
79
+ ? [{ x: e.begin(), value: v, color: style.color, label }]
80
+ : [];
81
+ },
82
+ draw: (ctx, xScale, yScale) => drawArea(ctx, cs, xScale, yScale, style,
83
+ // Omitted baseline rests on the axis floor (resolved late from the
84
+ // scale, so it tracks the auto-fit domain); a fixed baseline is used
85
+ // verbatim.
86
+ baseline ?? domainFloor(yScale), curveFactory, gaps, gapConnectorOpacity),
87
+ },
88
+ axisId: axis,
89
+ index,
90
+ }), [
91
+ cs,
92
+ series,
93
+ column,
94
+ style,
95
+ label,
96
+ baseline,
97
+ curveFactory,
98
+ gaps,
99
+ gapConnectorOpacity,
100
+ axis,
101
+ index,
102
+ ]);
103
+ // A stable per-instance slot (see useSlotKey) keeps this layer's z-position
104
+ // fixed across series/style/prop updates (no jump to the front on live update).
105
+ const slot = useSlotKey();
106
+ useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
107
+ useEffect(() => {
108
+ layers.registerLayer(slot, entry);
109
+ }, [layers, slot, entry]);
110
+ // Also a tracker source: the container fans in this series' value at the
111
+ // cursor for the (outside-the-chart) readout.
112
+ const { registerTrackerSource, unregisterTrackerSource } = container;
113
+ useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
114
+ useEffect(() => {
115
+ registerTrackerSource(slot, entry.layer);
116
+ }, [registerTrackerSource, slot, entry.layer]);
117
+ return null;
118
+ }
119
+ //# sourceMappingURL=AreaChart.js.map
@@ -0,0 +1,55 @@
1
+ import type { SeriesSchema, TimeSeries } from 'pond-ts';
2
+ import { type Curve } from './curve.js';
3
+ export interface BandChartProps<S extends SeriesSchema> {
4
+ /** The source series. Its key column supplies the time axis. */
5
+ series: TimeSeries<S>;
6
+ /** Name of the numeric column for the band's lower edge (e.g. `p25`). */
7
+ lower: string;
8
+ /** Name of the numeric column for the band's upper edge (e.g. `p75`). */
9
+ upper: string;
10
+ /**
11
+ * The band's semantic identifier — what the spread _is_ (e.g. `outer` for a
12
+ * p5/p95 envelope, `inner` for p25/p75). The theme maps it to a
13
+ * {@link BandStyle} (`theme.band[as] ?? theme.band.default`). **Omitted ⇒ the
14
+ * `default` band style** — no per-component fill/opacity override (restyle via
15
+ * the theme, the single styling channel).
16
+ */
17
+ as?: string;
18
+ /**
19
+ * Which `<YAxis>` (by its `id`) this band scales against — the *scale*, where
20
+ * `as` picks the *style*. **Omitted ⇒ the row's default axis.**
21
+ */
22
+ axis?: string;
23
+ /**
24
+ * Render-time edge interpolation — both edges drawn with this curve. **Omitted
25
+ * ⇒ `'linear'`.** Prefer a **symmetric** curve (`'natural'`/`'basis'`) to
26
+ * smooth a sparse aggregated envelope (RTC's `interpolation`) — `'monotone'`
27
+ * assumes increasing x and smooths the right→left lower edge asymmetrically.
28
+ * Denoise the underlying values with `smooth()`, not this.
29
+ */
30
+ curve?: Curve;
31
+ /**
32
+ * @internal Declaration position among the `<Layers>` children, injected by
33
+ * `Layers` so z-order follows JSX order. Do not set.
34
+ */
35
+ index?: number;
36
+ }
37
+ /**
38
+ * A variance-band draw layer: fills the envelope between the `lower` and `upper`
39
+ * columns of `series` (typically `rollingByColumn` percentiles), gap-aware, and
40
+ * registers itself into the enclosing {@link Layers}. Renders nothing to the
41
+ * DOM — the row draws it.
42
+ *
43
+ * Compose two for a two-tone spread — author the wider band first so it sits
44
+ * behind (declaration order = z-order, back-to-front), then the line on top:
45
+ *
46
+ * ```tsx
47
+ * <Layers>
48
+ * <BandChart series={s} lower="p5" upper="p95" as="outer" />
49
+ * <BandChart series={s} lower="p25" upper="p75" as="inner" />
50
+ * <LineChart series={s} column="p50" />
51
+ * </Layers>
52
+ * ```
53
+ */
54
+ export declare function BandChart<S extends SeriesSchema>({ series, lower, upper, as: semantic, axis, curve, index, }: BandChartProps<S>): null;
55
+ //# sourceMappingURL=BandChart.d.ts.map
@@ -0,0 +1,93 @@
1
+ import { useContext, useEffect, useMemo } from 'react';
2
+ import { bandFromTimeSeries } from './data.js';
3
+ import { bandExtent, drawBand } from './band.js';
4
+ import { resolveCurve } from './curve.js';
5
+ import { ContainerContext, LayersContext } from './context.js';
6
+ import { useSlotKey } from './use-slot-key.js';
7
+ /**
8
+ * A variance-band draw layer: fills the envelope between the `lower` and `upper`
9
+ * columns of `series` (typically `rollingByColumn` percentiles), gap-aware, and
10
+ * registers itself into the enclosing {@link Layers}. Renders nothing to the
11
+ * DOM — the row draws it.
12
+ *
13
+ * Compose two for a two-tone spread — author the wider band first so it sits
14
+ * behind (declaration order = z-order, back-to-front), then the line on top:
15
+ *
16
+ * ```tsx
17
+ * <Layers>
18
+ * <BandChart series={s} lower="p5" upper="p95" as="outer" />
19
+ * <BandChart series={s} lower="p25" upper="p75" as="inner" />
20
+ * <LineChart series={s} column="p50" />
21
+ * </Layers>
22
+ * ```
23
+ */
24
+ export function BandChart({ series, lower, upper, as: semantic, axis, curve, index = 0, }) {
25
+ const container = useContext(ContainerContext);
26
+ if (container === null) {
27
+ throw new Error('<BandChart> must be rendered inside a <ChartContainer>');
28
+ }
29
+ const layers = useContext(LayersContext);
30
+ if (layers === null) {
31
+ throw new Error('<BandChart> must be rendered inside a <Layers>');
32
+ }
33
+ const bs = useMemo(() => bandFromTimeSeries(series, lower, upper), [series, lower, upper]);
34
+ // Styling: semantic identifier → theme band style. The single styling channel.
35
+ const { band } = container.theme;
36
+ const style = (semantic !== undefined ? band[semantic] : undefined) ?? band.default;
37
+ const curveFactory = resolveCurve(curve);
38
+ const entry = useMemo(() => ({
39
+ layer: {
40
+ yExtent: () => bandExtent(bs),
41
+ xKind: 'time',
42
+ xExtent: () => bs.length === 0 ? null : [bs.x[0], bs.x[bs.length - 1]],
43
+ sampleAt: (time) => {
44
+ // No readout past the data (tracker policy — nearest() clamps); bounds
45
+ // from the columnar time axis.
46
+ if (bs.length === 0 ||
47
+ time < bs.x[0] ||
48
+ time > bs.x[bs.length - 1]) {
49
+ return [];
50
+ }
51
+ const e = series.nearest(time);
52
+ if (e === undefined)
53
+ return [];
54
+ // get() wants a literal key; lower/upper are runtime strings. Cast the
55
+ // *event* (not the method — detaching `this` breaks `get`).
56
+ const ev = e;
57
+ const lo = ev.get(lower);
58
+ const hi = ev.get(upper);
59
+ if (typeof lo !== 'number' ||
60
+ !Number.isFinite(lo) ||
61
+ typeof hi !== 'number' ||
62
+ !Number.isFinite(hi)) {
63
+ return [];
64
+ }
65
+ // Both edges, labelled by their column (e.g. p25 / p75), in the band's
66
+ // fill colour. A gap on either edge yields no readout (like the fill).
67
+ return [
68
+ { x: e.begin(), value: lo, color: style.fill, label: lower },
69
+ { x: e.begin(), value: hi, color: style.fill, label: upper },
70
+ ];
71
+ },
72
+ draw: (ctx, xScale, yScale) => drawBand(ctx, bs, xScale, yScale, style, curveFactory),
73
+ },
74
+ axisId: axis,
75
+ index,
76
+ }), [bs, series, lower, upper, style, curveFactory, axis, index]);
77
+ // Stable per-instance slot (see useSlotKey): keeps this band's z-position +
78
+ // identity across prop updates; the injected index drives the sort.
79
+ const slot = useSlotKey();
80
+ useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
81
+ useEffect(() => {
82
+ layers.registerLayer(slot, entry);
83
+ }, [layers, slot, entry]);
84
+ // Also a tracker source: the container fans in the band edges at the cursor
85
+ // for the (outside-the-chart) readout.
86
+ const { registerTrackerSource, unregisterTrackerSource } = container;
87
+ useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
88
+ useEffect(() => {
89
+ registerTrackerSource(slot, entry.layer);
90
+ }, [registerTrackerSource, slot, entry.layer]);
91
+ return null;
92
+ }
93
+ //# sourceMappingURL=BandChart.js.map