@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.
- package/CHANGELOG.md +3254 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/AreaChart.d.ts +85 -0
- package/dist/AreaChart.js +119 -0
- package/dist/BandChart.d.ts +55 -0
- package/dist/BandChart.js +93 -0
- package/dist/BarChart.d.ts +72 -0
- package/dist/BarChart.js +137 -0
- package/dist/BoxPlot.d.ts +77 -0
- package/dist/BoxPlot.js +137 -0
- package/dist/Canvas.d.ts +37 -0
- package/dist/Canvas.js +39 -0
- package/dist/ChartContainer.d.ts +106 -0
- package/dist/ChartContainer.js +306 -0
- package/dist/ChartRow.d.ts +29 -0
- package/dist/ChartRow.js +215 -0
- package/dist/Layers.d.ts +22 -0
- package/dist/Layers.js +399 -0
- package/dist/LineChart.d.ts +60 -0
- package/dist/LineChart.js +105 -0
- package/dist/ScatterChart.d.ts +84 -0
- package/dist/ScatterChart.js +139 -0
- package/dist/TimeAxis.d.ts +9 -0
- package/dist/TimeAxis.js +12 -0
- package/dist/XAxis.d.ts +39 -0
- package/dist/XAxis.js +84 -0
- package/dist/YAxis.d.ts +42 -0
- package/dist/YAxis.js +86 -0
- package/dist/annotations.d.ts +110 -0
- package/dist/annotations.js +459 -0
- package/dist/area.d.ts +54 -0
- package/dist/area.js +186 -0
- package/dist/band.d.ts +31 -0
- package/dist/band.js +57 -0
- package/dist/bars.d.ts +96 -0
- package/dist/bars.js +171 -0
- package/dist/box.d.ts +59 -0
- package/dist/box.js +140 -0
- package/dist/chip.d.ts +23 -0
- package/dist/chip.js +43 -0
- package/dist/cjs-fallback.cjs +16 -0
- package/dist/context.d.ts +362 -0
- package/dist/context.js +5 -0
- package/dist/curve.d.ts +22 -0
- package/dist/curve.js +13 -0
- package/dist/data.d.ts +154 -0
- package/dist/data.js +197 -0
- package/dist/domain.d.ts +19 -0
- package/dist/domain.js +61 -0
- package/dist/encoding.d.ts +89 -0
- package/dist/encoding.js +144 -0
- package/dist/format.d.ts +53 -0
- package/dist/format.js +47 -0
- package/dist/gaps.d.ts +146 -0
- package/dist/gaps.js +209 -0
- package/dist/grid.d.ts +11 -0
- package/dist/grid.js +29 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +34 -0
- package/dist/line.d.ts +46 -0
- package/dist/line.js +88 -0
- package/dist/range.d.ts +15 -0
- package/dist/range.js +27 -0
- package/dist/scatter.d.ts +70 -0
- package/dist/scatter.js +213 -0
- package/dist/select.d.ts +13 -0
- package/dist/select.js +23 -0
- package/dist/slots.d.ts +48 -0
- package/dist/slots.js +64 -0
- package/dist/theme.d.ts +224 -0
- package/dist/theme.js +232 -0
- package/dist/tracker.d.ts +30 -0
- package/dist/tracker.js +47 -0
- package/dist/use-slot-key.d.ts +21 -0
- package/dist/use-slot-key.js +25 -0
- package/dist/viewport.d.ts +20 -0
- package/dist/viewport.js +30 -0
- 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
|