Using other chart libraries
The charting answer to pond's data is
@pond-ts/charts. It consumes a
TimeSeries directly — no point-array conversion — and ships the
things you'd otherwise rebuild by hand: cursors and readouts,
selection, annotations, theming, gap-aware rendering, live updates.
If you're choosing how to chart pond data, start there; the rest of
this site assumes it.
This page covers the other case — and it is the one place in
these docs that does: getting data out of pond to feed a
third-party chart package (Recharts, Observable Plot, visx, raw d3).
The bridge is deliberately short: shape your TimeSeries with pond's
transforms, then export flat rows via toPoints() (or dense typed
arrays via the column API for large data). If your data is plain
arrays to begin with and you don't need the transform pipeline, a
general-purpose chart library alone may genuinely be the better fit —
pond earns its keep when the data needs shaping first.
toPoints() — the bridge
Wide-row export: every event becomes one row with ts plus every
value column from the schema as a top-level key.
const data = cpu.toPoints();
// [{ ts: number, cpu: number | undefined, host: string | undefined, ... }]
ts is event.begin() — for Time keys this is the timestamp; for
TimeRange / Interval keys this is the interval start. Missing
values stay undefined; chart libraries render those as gaps with
connectNulls={false} or equivalent.
The shape — flat { ts, ...cols }[] — is what every mainstream chart
library accepts directly. For a single-series chart, compose with
select to drop the columns you don't want:
const cpuPoints = cpu.select('cpu').toPoints();
// [{ ts: number, cpu: number | undefined }, ...]
Worked example — the rows drop straight into a Recharts line chart
(the same shape works for Observable Plot's data or a d3 scale
domain):
import { LineChart, Line, XAxis, YAxis } from 'recharts';
function CpuChart({ series }: { series: typeof cpu }) {
const data = series.select('cpu').toPoints();
return (
<LineChart width={560} height={220} data={data}>
<XAxis
dataKey="ts"
type="number"
domain={['dataMin', 'dataMax']}
tickFormatter={(ts) => new Date(ts).toLocaleTimeString()}
/>
<YAxis />
<Line dataKey="cpu" dot={false} connectNulls={false} />
</LineChart>
);
}
connectNulls={false} keeps pond's gaps honest — an undefined cell
renders as a break, not an interpolated bridge.
fromPoints(points, { schema, name? }) — the inverse
Construct a TimeSeries from a flat array of wide-row points. Each
point carries ts plus one key per value column from the schema:
const ts = TimeSeries.fromPoints(
[
{ ts: 0, cpu: 0.31, host: 'api-1' },
{ ts: 60_000, cpu: 0.44, host: 'api-1' },
],
{
schema: [
{ name: 'time', kind: 'time' },
{ name: 'cpu', kind: 'number' },
{ name: 'host', kind: 'string' },
] as const,
name: 'reconstructed',
},
);
The schema's first column must be kind: 'time' (ts is a single
timestamp; it can't reconstruct a TimeRange or Interval extent).
Missing keys on a row become undefined on the resulting Event.
Per-group wide rows (one source, categorical column)
The most common dashboard shape: one source where events carry a
categorical column (host, region, service, …), and you want one
chart line per category. The events arrive in long form; most chart
libraries want wide form — one row per timestamp, one column per
category. pivotByGroup is the reshape:
const wide = long.pivotByGroup('host', 'cpu');
// schema: [time, "api-1_cpu", "api-2_cpu"]
wide.toPoints();
// [{ ts: 0, "api-1_cpu": 0.31, "api-2_cpu": 0.52 }, ...]
pivotByGroup is the simpler primitive when there's one value column
per group; groupBy + joinMany is the heavier workflow when each
group spawns multiple derived columns (per-host baselines). Full
reference: Reshaping → pivotByGroup.
Overlaying multiple TimeSeries
Two series to overlay (this-week vs last-week) join before the chart
layer: series.join(other) or
TimeSeries.joinMany([...]), then
one toPoints() on the result.
Large datasets: the column API
Past tens of thousands of points, per-row objects from toPoints()
become the bottleneck. The column API hands you dense
Float64Arrays (series.column('cpu').toFloat64Array()) and
bin/binBy for decimation without per-event allocation — draw
straight to canvas from those. (This is the same machinery
@pond-ts/charts uses internally.)
Live-update gotchas
Two recurring snags when an external chart re-renders on live snapshots:
- Disable animation on snapshot ticks — a chart that tweens every
update smears a 100 ms snapshot cadence into continuous motion
(
isAnimationActive={false}in Recharts, or your library's equivalent). - Branch on the empty snapshot for fixed domains — before the
first event arrives,
toPoints()is[]; a chart with an explicit y-domain will happily render empty axes over no data. Render an empty-state instead of the chart until the snapshot has rows.
Full worked example
The dashboard how-to guide builds a complete live Recharts dashboard on pond data — snapshots, baselines, gaps, and the animation gotcha above worked in context — and is the maintained end-to-end reference for this bridge.