Skip to main content

pond

Typed time-series primitives, in TypeScript or Rust. Schema-driven events, composable transforms, a live-ingest mode, financial studies, and an optional React integration — all immutable.

pond comes in two languages that give the same answers:

  • pond-ts is the TypeScript library: strict types end to end, runs in the browser and on Node, and carries the React hooks and charts.
  • pond-rs is the same core written in Rust: the same operations and the same results, bit for bit, at native speed and on several cores. See pond-rs (Rust).
  • pond_rs is pond-rs for Python (pip install pond-rs): polars, pandas and pyarrow frames in and out, with the same answers. See Python (pond_rs).
PackageTypeScriptRust
Core: batch seriespond-tspond-rs
Core: live seriespond-tspond-rs (pond_rs::live)
Financial studies@pond-ts/financialpond-rs (pond_rs::financial)
Process (computation graphs)@pond-ts/processTypeScript only
React hooks, charts, fitness@pond-ts/react, /charts, /fitTypeScript only

Examples on these pages show TypeScript and, where pond-rs has the same thing, a Rust tab beside it. Pick one and every tabbed example on the site follows.

npm install pond-ts # core
npm install @pond-ts/react # React hooks (optional)

First example: batch​

Your data is rows with timestamps. Tell pond the schema once; every transform downstream works off it.

import { Sequence, TimeSeries } from 'pond-ts';

// 1. Define the schema. Every downstream transform narrows off of it —
// .get('cpu') returns number, .get('host') returns string, and
// transforms like aggregate preserve that narrowing.
const schema = [
{ name: 'time', kind: 'time' },
{ name: 'cpu', kind: 'number' },
{ name: 'requests', kind: 'number' },
{ name: 'host', kind: 'string' },
] as const;

// 2. Construct a TimeSeries. Rows are positional by schema column;
// time values are ms-since-epoch, ISO strings with an offset, or
// Dates.
const cpu = TimeSeries.fromJSON({
name: 'cpu',
schema,
rows: [
['2025-01-01T00:00:00Z', 0.31, 120, 'host1'],
['2025-01-01T00:01:00Z', 0.44, 135, 'host2'],
['2025-01-01T00:02:00Z', 0.52, 141, 'host1'],
['2025-01-01T00:03:00Z', 0.48, 128, 'host1'],
['2025-01-01T00:04:00Z', 0.63, 166, 'host3'],
],
});

// 3. Compose transforms. Each one returns a new TimeSeries — the
// source is immutable, types narrow per step.
const byMinute = cpu.aggregate(Sequence.every('1m'), {
cpu: 'avg',
requests: 'sum',
host: 'last',
});

const bands = cpu.baseline('cpu', { window: '2m', sigma: 2 });
// ^ appends avg / sd / upper / lower columns in one rolling pass,
// flat-window `sd === 0` handling already correct.

const anomalies = cpu.outliers('cpu', { window: '2m', sigma: 2 });
// ^ schema-preserving filter — same columns, just the spikes.

// 4. Chart-ready output. Feed the series straight to @pond-ts/charts,
// or export wide rows via `toPoints()` for a third-party chart
// package (see pond-ts → Advanced → Using other chart libraries).
byMinute.toPoints(); // [{ ts, cpu, requests, host }, ...]
bands.toPoints(); // [{ ts, cpu, ..., avg, sd, upper, lower }, ...]
anomalies.length; // how many spikes tripped the 2σ band

The whole batch API is composable like this — filter, map, rolling, smooth, groupBy, join, reduce, diff, rate, fill all fit the same "TimeSeries in, TimeSeries out, typed schema preserved" shape.

Second example: live data​

Drop a LiveSeries wherever events arrive incrementally — a WebSocket, a polling loop, a message queue. Push in as they come; snapshot to a regular TimeSeries at any moment for batch analytics.

import { LiveSeries, Sequence } from 'pond-ts';

// 1. Same schema; this is a live append buffer with retention.
const live = new LiveSeries({
name: 'cpu',
schema,
retention: { maxAge: '10m' }, // keep only the last 10 minutes
});

// 2. Push as events arrive. Each push is validated against the schema.
live.push([Date.now(), 0.45, 128, 'api-1']);
// ...driven by whatever your data source is.

// 3. React to events inline. `e.get('cpu')` narrows to number |
// undefined straight from the schema — no `as number` casts.
live.on('event', (e) => {
const cpu = e.get('cpu');
if (cpu !== undefined && cpu > 0.9) {
console.warn(`High CPU on ${e.get('host')}`);
}
});

// 4. Snapshot to a TimeSeries for batch analytics at any time.
const snap = live.toTimeSeries();
snap.aggregate(Sequence.every('1m'), { cpu: 'avg' });

The live side also has its own aggregate / rolling / filter views that stay incremental — good for continuously-updated dashboard numbers without snapshot overhead. See Live (TypeScript) or Live series (Rust).

Third example: React (optional)​

You do not need React to use pond-ts. Everything above is the core package. React is TypeScript only, so this example has no Rust tab. @pond-ts/react is a separate install that wraps the live primitives in hooks — it's the bridge, not the library.

import { useEffect } from 'react';
import { useLiveSeries, useCurrent } from '@pond-ts/react';

function Dashboard() {
// 1. Create and subscribe to a LiveSeries; snap refreshes on push.
const [live, snap] = useLiveSeries({
name: 'cpu',
schema,
retention: { maxAge: '10m' },
});

// 2. Pipe events from wherever they come (WebSocket here).
useEffect(() => {
const ws = new WebSocket('/api/metrics');
ws.onmessage = (m) => {
const { ts, cpu, requests, host } = JSON.parse(m.data);
live.push([ts, cpu, requests, host]);
};
return () => ws.close();
}, [live]);

// 3. Reduce the source to chart-ready scalars — reference-stable
// per-field, so downstream useMemo doesn't invalidate on every
// push.
const { cpu } = useCurrent(live, { cpu: 'avg' });

return (
<>
<Stat label="Avg CPU" value={cpu} />
{snap && <Chart points={snap.select('cpu').toPoints()} />}
</>
);
}

See @pond-ts/react for the hook reference and end-to-end dashboard patterns.

What pond is (and isn't)​

  • Typed — in TypeScript the schema is a readonly tuple and every transform output narrows off it, with no as casts at the call site. In Rust the column kinds are checked when the code runs, with errors that say what to fix.
  • Two languages, one answer — pond-rs is checked against pond-ts's own output on a shared set of test cases, so a result does not depend on which language computed it.
  • Immutable — Event and TimeSeries are frozen; transforms return new instances.
  • Batch and streaming as peers — TimeSeries is in-memory transforms over a complete dataset; LiveSeries is a bounded buffer with moderate reordering tolerance at ingest. Same schema type, same operator surface, different cost models. Not a full streaming engine like Beam or Flink (see Live Transforms → Late-event scope for what that does and doesn't cover).
  • Framework-agnostic — the core has no React dependency; @pond-ts/react is strictly optional.

Not a database. Not a query engine. The core is not a chart library either — @pond-ts/charts is the first-party chart layer on top of it, and toPoints() is the one-line bridge to every other charting option.

In this docs site​

  • Start here — install, first workflow, the mental model.
  • pond-ts (core) — TimeSeries, LiveSeries, advanced (array columns, bridging to other chart libraries).
  • pond-rs (Rust) — the core and the financial studies in Rust: getting started, what differs from pond-ts, and the benchmarks.
  • @pond-ts/react — React hooks for subscribing to live sources.
  • @pond-ts/fit — activity/fitness analytics on top of core.
  • @pond-ts/charts — first-party canvas charts consuming a TimeSeries directly.
  • How-to guides — long-form, end-to-end scenario walkthroughs.
  • Recipes — single-technique, code-forward pages.
  • Reference — benchmarks and cross-package reference material.

The full TypeScript API reference lives in-site at /docs/api; the Rust one is on docs.rs.