Skip to main content

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.