@pond-ts/charts
Declarative, canvas-rendered time-series charts for React. Compose a chart
from components the way you'd compose any other UI. It draws to a canvas rather
than the DOM, so point count is a rendering cost and not a node count. And it
reads a pond TimeSeries directly, so rolling, aggregation, alignment and gap
handling all run right up to the plot — no adapter layer, and no second data
model to keep in step.
Two days of household electricity demand at one-minute resolution — 2,880 samples, every one of them a point on the canvas. Quiet overnight, a sharp breakfast peak, a long cooking-hours evening. Move the pointer and a staff rises to a flag at each layer's value there; drag to pan, wheel to zoom, both stopped at the ends of the record. It's live, not a screenshot.
One column is doing all of that. kw is the only data. Demand at this
resolution isn't a smooth signal with noise on it — it's a sum of
rectangles, because a kettle is 2.6 kW for three minutes and nothing in
between. So the raw minutes get two layers: a scatter for where the samples
actually are, and a faint line joining them. The vertical strokes a per-minute
polyline is mostly made of are the reading here — each one is something
switching on, and the flats between them are the rectangles it draws. Over the
top, the pale envelope is each hour's min–max and the line through it is the
hour's mean. Neither is a second dataset prepared somewhere else; they're a
single pond rolling() call, emitting three columns from the one source column
and handing them to two more draw layers:
const rolled = demand.rolling(
'1h',
{
mean: { from: 'kw', using: 'avg' },
lo: { from: 'kw', using: 'min' },
hi: { from: 'kw', using: 'max' },
},
{ alignment: 'centered' },
);
That's the entire pitch: a chart consumes a pond TimeSeries directly, the
transform pipeline runs right up to the plot, you compose a container, rows,
and draw layers as React components, and the canvas renderer does the drawing.
No chart-library data model to adapt to; no imperative escape hatch for
interaction.
The break on the first afternoon is a ninety-minute recorder outage, carried as
undefined rather than zero. The scatter has no points there and every line
breaks — a connecting line is exactly the layer that would otherwise rule a
confident straight edge across an hour and a half nobody measured, and zero
would draw a switched-off house that never happened. That distinction is the
whole of Missing data & gaps.
The demand is modelled, not measured. It's shaped after the UCI Individual household electric power consumption set (CC BY 4.0) — one house, one-minute active power in kW, 2,075,259 rows, ~1.25% of them missing — but none of its rows are redistributed here. The numbers are generated from how domestic load forms: a standby floor, a fridge duty-cycling against it, and discrete appliance runs on top. See the fixture.
Install — npm install @pond-ts/charts pond-ts @pond-ts/react react react-dom.
Peer-depends on pond-ts, @pond-ts/react, and react; the pond packages
release together, so keep their ranges in step. See
Using @pond-ts/charts for the two integration
gotchas (Storybook react-docgen, the repaint contract).
The layout model
Charts are composed, not configured — the same nesting react-timeseries-charts users will recognise:
<ChartContainer>— owns the shared x axis (time or value, inferred from the data), the viewrange, overallwidth,theme, and cursor mode.<ChartRow>— one stacked plot band with its own y axis / height. A container holds one or more rows sharing the x axis.<Layers>— the mandatory z-stack inside a row; children draw back-to-front in declaration order.<YAxis>/<XAxis>/<TimeAxis>— placeable axes.idlinks a draw layer'saxisto a scale;sideplaces it;format(a d3 specifier or a function) formats ticks and the matching cursor readout.
The minimal API set — compact cut
The primitives you compose a chart from. The API reference has every prop, type, and default; this is the map.
Layout & axes
| Export | What it is |
|---|---|
ChartContainer | Root: shared x axis, range, width, theme, cursor. |
ChartRow | A stacked plot band with its own y axis and height. |
Layers | The z-stack boundary inside a row. |
YAxis | A y axis: id, side, auto-fit or explicit domain, format. |
XAxis | A placeable x axis (time or value), custom ticks. |
TimeAxis | A thin <XAxis> preset for the time axis. |
CategoryAxis | A thin <XAxis> preset for the ordinal category axis. |
Canvas | The low-level DPR-aware canvas primitive rows draw on. |
Draw layers
| Export | What it draws |
|---|---|
LineChart | A gap-aware line. |
AreaChart | A filled area. |
BandChart | A filled lower/upper envelope (variance band). |
ScatterChart | Points, with data-driven radius / colour encoding. |
BarChart | Bars, histograms — stacked (columns / a Map / bins) and horizontal (guide) — and categorical bars (categories, a bar per category on an ordinal axis; guide). |
BoxPlot | Box-and-whisker per bucket (shape: whisker / solid / none) from five pre-computed quantile columns. |
Candlestick | First-class OHLC candles (variant: candle / bar / hollow; colorBy; optional showOHLC axis pills). Pairs with the trading-time axis (calendar on ChartContainer). |
Every draw layer takes a pond series + a column, an as style
identifier (theme lookup), and an axis scale id — style and scale are
separate channels by design (no per-component colour/width props).
Row lists (standalone — no ChartContainer)
| Export | What it is |
|---|---|
BarList | A ranked row table — one proportional bar line per column per entity, data cells, sort, expander (guide). |
BoxList | Its distribution sister — a five-number box per line plus a current-value tick (the traffic-by-interface table; guide). |
One row per entity rather than per bucket. A per-event series feeds
directly (<BarList series={splits} label={…}> — one row per event, no
shaping step), and partition facts (partitionBy + reduce) spread straight
into a row's values — see
the acquisition recipes.
Annotations & indicators
| Export | What it is |
|---|---|
Region | A shaded x-range (annotation register — never a data hue). |
Baseline | A horizontal value line, optional axis-pill indicator. |
Marker | A vertical x line, optional axis-pill indicator. |
YAxisIndicator | A live value pill pinned to a y-axis edge. |
createLiveValue | High-frequency pill updates with an isolated repaint. |
View builders & theming
With a pond series, the components above are the whole data contract — the
from* exports below expose the chart-ready view shapes for consumers
writing custom draw code; no shipped layer needs them.
| Export | What it is |
|---|---|
fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries | Build the chart-ready view shapes for custom draw code (no shipped layer needs them). |
stacksFromGroups, stacksFromColumns, stacksFromBins, categoryStack, transposeRow | Stacked / histogram / categorical view builders (bins and categories inputs also accept their sources directly). |
defaultTheme | The neutral built-in ChartTheme. |
cssVarTheme, useChartTheme | Build a theme from CSS custom properties and follow a dark/light toggle. |
Why @pond-ts/charts — and when not to use it
The pitch is narrow on purpose: canvas rendering, streaming-first
(a LiveSeries re-render is the same code path as a batch one — no separate
"live mode" API), and the pond transform pipeline right up to the plot
(rolling, aggregation, gap-fill all feed the same layer props a batch chart
uses). If your charts are static SVG dashboards over plain arrays, that's
real machinery you don't need — the bridge page
covers exporting pond data to Recharts instead.
Some honest constraints, before you commit: you adopt the pond TimeSeries
data model end to end (not a drop-in over arbitrary JSON); width is an
explicit pixel number, not "100%" — the
responsive-width recipe is one
ResizeObserver away, and it's what the Gallery's own cards run on;
interaction is mouse-only (no keyboard/touch cursor or selection yet); and
the package is pre-1.0 — the API is stabilizing, not frozen. None of that
is hidden: the Storybook and the Gallery show
exactly what ships today, working, not a mockup.
Used in production for activity dashboards and ops telemetry today, with a financial-charts build actively underway. See the CHANGELOG for what's shipped in each release — the whole monorepo tags and publishes together.
Where to go next
- Learn charts — new to the library? Start here: a nine-chapter tutorial track, one running example, a live chart in every chapter.
- Gallery — eight live charts spanning every draw layer, each linking its full Storybook coverage. Touch one before you read another word of API reference.
- Value axis — the chart-level reference for plotting
against something other than time: axis-kind inference,
byColumnhistograms, category axes, and dual x-axes. - Storybook — every component and interaction mode, one story per prop/state: the systematic, API-adjacent knob walk.
- API reference — the in-site, full-width reference: every component, prop, and type.
- Using @pond-ts/charts — install notes and integration gotchas.
- Theming — the
ChartThememodel and the CSS-token bridge. - Resizable multi-panel layout — a full chart layout worked end to end.
The docs here are an introduction plus the generated type reference. A per-chart-type reference section, a dedicated interaction/annotations reference (deeper than the Learn track's tour), and the financial-charts flagship guide are on the roadmap — see PLAN.md's "Docs site wave" for what's shipped and what's next.