Skip to main content

Mental model

Hold this one picture and the rest of core follows.

A series is a schema-typed, ordered collection of events. Events carry a key plus a typed payload. Operators transform series into series; reducers compress events into values.

S — the schema, threaded through every stepTimeSeries<S>(or LiveSeries<S>)immutable, complete(or mutable, streaming)transforms(preserve the grid)filter, map, select, fill,diff, rate, rolling, smooth,cumulative, pctChange, …TimeSeries<S'>(or accumulator)windowing(changes the grid)aggregate(seq, mapping)rolling(window, mapping)reduce(mapping)Bucketed seriesor scalar record

The schema S carries through every transform — the type system narrows column shapes correctly, without as casts. That single invariant is what makes the algebra closed: because every operator returns a series of a known schema, you keep chaining, and a bucket is just a smaller series (same API, no second primitive to learn).

The three series types

The key's kind picks the type, and the type decides which operators exist:

TypeKeyed byCarries
TimeSeriesa temporal key (batch)the full algebra incl. calendar/clock operators
LiveSeriesa temporal key (streaming)the same operator surface, incremental cost model
ValueSeriesa monotonic non-time axisthe ordering-based operators only — no calendar (by type)

TimeSeries and LiveSeries are peers, not layers: same schema type, same operators, different cost models (whole-collection vs. incremental). ValueSeries drops in when the natural x of your data is a quantity (distance, work) rather than the wall clock.

The vocabulary each operator assumes — temporal keys, sequences, windowing, triggers, partitioning, late data, the value axis — is defined page by page in Concepts. Read those reference-style once the picture above is clear.

Coming from pandas?

pandas's .resample() covers both directions; pond-ts splits them:

  • Downsample (fewer rows out than in) — aggregate(seq, mapping).resample().agg()
  • Upsample / regrid (one row per grid point, hold or interpolate) — align(seq, { method }).asfreq() + .ffill() / .interpolate('time')
  • rolling(window, mapping).rolling().agg()
  • within(range) / overlapping(range) / trim(range).loc[] slicing with three explicit semantics
  • reduce(mapping).agg() on the whole frame
  • groupBy(col).groupby(col)

See Aggregation for the deep dive on bucketing.

Coming from pondjs?

pondjspond-ts
TimeSeries (sorted)TimeSeries (1:1 conceptually; methods differ)
Collection (unordered)TimeSeries — pond-ts only exposes the sorted variant
IndexedEventEvent<Interval>
TimeRangeEventEvent<TimeRange>
Pipeline + processorsmethod chain on TimeSeries
Pond / Aggregatorbatch aggregate() or LiveAggregation

pond-ts deliberately drops the Collection / TimeSeries distinction: a bucket is a smaller TimeSeries, so anything that works on a series works on a bucket — same API, no second primitive.