View over a TimeSeries that scopes stateful transforms to within
each partition. Created by TimeSeries.partitionBy(by).
Most pond-ts stateful operators read from neighboring events when
computing each output. On a multi-entity series (events for many
hosts interleaved by time), those neighbors silently cross entity
boundaries: a fill('linear') for host-A would interpolate using
host-B's value as a "neighbor"; a rolling('5m', { cpu: 'avg' })
would average across all hosts in the window.
partitionBy runs the transform independently on each partition's
events. The view is persistent across chains — each sugar method
returns another PartitionedTimeSeries carrying the same partition
columns, so multi-step per-partition workflows compose cleanly:
const cleaned = ts
.partitionBy('host')
.dedupe({ keep: 'last' }) // per-host
.fill({ cpu: 'linear' }) // per-host
.rolling('5m', { cpu: 'avg' }) // per-host
.collect(); // back to TimeSeries<S>
Call .collect() (or .apply(fn) for arbitrary transforms) to
materialize back to a regular TimeSeries. Without .collect(),
the chain stays in partition view.
Constructor
new PartitionedTimeSeries(source: TimeSeries<S>, by: keyof EventDataForSchema<S> & string | readonly (keyof EventDataForSchema<S> & string)[], options?: { groups?: readonly K[] })
Properties
byreadonly (keyof EventDataForSchemaEventDataForSchematypepond-ts{ [C in DataColumnsForSchema<S>[number] as C['name']]: DataValueForColumn<C> }<S> & string)[]readonlyDeclared partition values when partitionBy(col, { groups }) was
used. When set, toMap iterates in declared order (not insertion
order), empty declared groups still appear as empty TimeSeries
entries, and unknown partition values throw at construction time.
sourceTimeSeriesTimeSeriesclasspond-tsAn immutable, schema-typed, ordered collection of events — the batch
layer's core primitive. A series is constructed whole from complete data
and never mutated: every transform (filter, align, rollup, …)
returns a new TimeSeries, so the full analytical surface can sort,
scan, or index freely. Example:
new TimeSeries({ name, schema, rows }).
<S>readonlyMethods
aggregate
aggregate(sequence: SequenceLike, mapping: Mapping, options?: { range?: TemporalLike }): PartitionedTimeSeries<readonly [ColumnDef<'interval', 'interval'>, AggregateColumns<ValueColumnsForSchema<S>, Mapping>], K>
Per-partition aggregate. See TimeSeries.aggregate.
align
align(sequence: SequenceLike, options?: { method?: AlignMethod; range?: TemporalLike; sample?: AlignSample }): PartitionedTimeSeries<readonly [ColumnDef<'interval', 'interval'>, OptionalizeColumns<ValueColumnsForSchema<S>>], K>
Per-partition align. See TimeSeries.align.
apply
apply(fn: (group: TimeSeries<S>) => TimeSeries<R>): TimeSeries<R>
Run a transform fn independently on each partition and return a
TimeSeries<R> directly (terminal — does not stay in the
partitioned view). The escape hatch for compositions or operators
not exposed as sugar.
To keep the partition after a custom transform, use the sugar
methods (which preserve partition state) or call .partitionBy(...)
again on the result.
```ts
// chain two stateful ops within each partition (one shot)
const out = series.partitionBy('host').apply(g =>
g.fill({ cpu: 'linear' }).rolling('5m', { cpu: 'avg' }),
);
```
baseline
baseline(col: Col, options: { alignment?: RollingAlignment; minSamples?: number; names?: { avg?: AvgName; lower?: LowerName; sd?: SdName; upper?: UpperName }; sigma: number; window: DurationInput }): PartitionedTimeSeries<readonly [S[0], ValueColumnsForSchema<S>, OptionalNumberColumn<AvgName>, OptionalNumberColumn<SdName>, OptionalNumberColumn<UpperName>, OptionalNumberColumn<LowerName>]>
Per-partition baseline. See TimeSeries.baseline.
collect
collect(): TimeSeries<S>
Materialize the partitioned view back into a regular TimeSeries.
Terminal operation — call this at the end of a chain to "collect"
the per-partition results. Equivalent to .apply(g => g) but
cheaper (no fn dispatch, just returns the source as-is).
```ts
const cleaned = ts
.partitionBy('host')
.fill({ cpu: 'linear' })
.rolling('5m', { cpu: 'avg' })
.collect(); // <- TimeSeries<S>
```
cumulative
cumulative(spec: { [K in string]: 'sum' | 'min' | 'max' | 'count' | (acc: number, value: number) => number }): PartitionedTimeSeries<readonly [S[0], ReplaceSmoothedColumn<ValueColumnsForSchema<S>, Targets>], K>
Per-partition cumulative. See TimeSeries.cumulative.
dedupe
dedupe(options?: { keep?: DedupeKeep<S> }): PartitionedTimeSeries<S, K>
Per-partition dedupe. The duplicate key becomes "same partition
columns AND same timestamp" — partitionBy provides the partition
segregation, dedupe handles the within-partition timestamp
collapse. The most common dedupe shape for multi-entity ingest.
See TimeSeries.dedupe.
diff
diff(columns: Target | readonly Target[], options?: { drop?: boolean }): PartitionedTimeSeries<readonly [S[0], ReplaceSmoothedColumn<ValueColumnsForSchema<S>, Target>], K>
Per-partition diff. See TimeSeries.diff.
fill
fill(strategy: FillStrategy | FillMapping<S>, options?: { limit?: number; maxGap?: DurationInput }): PartitionedTimeSeries<S, K>
Per-partition fill. See TimeSeries.fill.
materialize
materialize(sequence: SequenceLike, options?: { range?: TemporalLike; sample?: AlignSample; select?: 'first' | 'last' | 'nearest' }): PartitionedTimeSeries<readonly [ColumnDef<'time', 'time'>, OptionalizeColumns<ValueColumnsForSchema<S>>], K>
Per-partition materialize. See TimeSeries.materialize.
Bonus over the bare TimeSeries.materialize call: every
output row, including empty-bucket rows, gets the partition
columns auto-populated from the partition's known key values.
Without this, empty buckets would emit rows with undefined
partition columns — forcing a follow-up
.fill({ host: 'hold' }) step that fails for partitions where
every event sits in a long-outage gap.
outliers
outliers(col: Col, options: { alignment?: RollingAlignment; minSamples?: number; sigma: number; window: DurationInput }): PartitionedTimeSeries<S, K>
Per-partition outliers. See TimeSeries.outliers.
pctChange
pctChange(columns: Target | readonly Target[], options?: { drop?: boolean }): PartitionedTimeSeries<readonly [S[0], ReplaceSmoothedColumn<ValueColumnsForSchema<S>, Target>], K>
Per-partition pctChange. See TimeSeries.pctChange.
rate
rate(columns: Target | readonly Target[], options?: { drop?: boolean }): PartitionedTimeSeries<readonly [S[0], ReplaceSmoothedColumn<ValueColumnsForSchema<S>, Target>], K>
Per-partition rate. See TimeSeries.rate.
rolling
rolling(window: DurationInput, mapping: Mapping, options?: { alignment?: RollingAlignment; minSamples?: number }): PartitionedTimeSeries<readonly [S[0], AggregateColumns<ValueColumnsForSchema<S>, Mapping>], K>
Per-partition rolling. See TimeSeries.rolling.
rolling(sequence: SequenceLike, window: DurationInput, mapping: Mapping, options?: { alignment?: RollingAlignment; minSamples?: number; range?: TemporalLike; sample?: AlignSample }): PartitionedTimeSeries<readonly [ColumnDef<'interval', 'interval'>, AggregateColumns<ValueColumnsForSchema<S>, Mapping>], K>
Per-partition rolling. See TimeSeries.rolling.
sample
sample(strategy: BatchSampleStrategy): PartitionedTimeSeries<S, K>
Per-partition sample. Each partition gets its own independent
sample state — separate stride counter or its own K-event
reservoir. Safe by construction; no unsafeGlobal: true token.
See TimeSeries.sample.
scan
scan(source: Source, step: ScanStep<A>, init: A): PartitionedTimeSeries<readonly [S[0], ReplaceSmoothedColumn<ValueColumnsForSchema<S>, Source>], K>
Per-partition scan. See TimeSeries.scan.
scan(source: Source, step: ScanStep<A>, init: A, options: { output: Name }): PartitionedTimeSeries<readonly [S[0], ValueColumnsForSchema<S>, ColumnDef<Name, 'number'>], K>
Per-partition scan. See TimeSeries.scan.
shift
shift(columns: Target | readonly Target[], n: number): PartitionedTimeSeries<readonly [S[0], ReplaceSmoothedColumn<ValueColumnsForSchema<S>, Target>], K>
Per-partition shift. See TimeSeries.shift.
smooth
smooth(column: Target, method: SmoothMethod, options: { alpha: number; output?: Output; warmup?: number } | { alignment?: RollingAlignment; output?: Output; window: DurationInput } | { output?: Output; span: number }): PartitionedTimeSeries<Output extends string ? readonly [S[0], ValueColumnsForSchema<S>, OptionalNumberColumn<Output>] : readonly [S[0], ReplaceSmoothedColumn<ValueColumnsForSchema<S>, Target>]>
Per-partition smooth. See TimeSeries.smooth.
toMap
toMap(): Map<K, TimeSeries<S>>
Materialize the partitioned view as a Map<key, TimeSeries<S>>,
one entry per partition. Terminal — exits the partition view.
Use this when downstream code needs to iterate or look up per
partition (typical in dashboards: one chart line per host, one
tooltip per region). Without this, the equivalent dance was
.collect().groupBy(col, fn) — two operators where one would do.
The map key is the stringified partition value for single-column
partitions, or a JSON.stringify'd array of values for composite
partitions. The single-column form preserves the value's natural
string representation (a host column with values 'api-1'
yields keys 'api-1'); composite keys produce JSON like
'["api-1","eu"]'. Map iteration order matches the order each
partition was first encountered in the source events.
undefined partition values become the literal ' undefined'
with a leading space — this avoids colliding with a string
column whose value happens to be the literal text 'undefined'.
The two are distinct buckets:
series // events with host=undefined and host='undefined'
.partitionBy('host')
.toMap();
// → 2 entries: ' undefined' (missing) vs 'undefined' (string literal)
Divergence from series.groupBy(col): groupBy uses bare
'undefined' (no leading space) for missing values, so it
collapses these two cases. toMap's leading-space sentinel is
an intentional improvement — the older groupBy shape silently
loses the distinction between "missing" and "the string
'undefined'". Migrating from groupBy to toMap will produce
different keys for partitions with undefined values; lookup
code that previously did .get('undefined') should change to
.get(' undefined') (note the leading space) to find the
missing-value bucket.
Composite encoder. For composite partitions, JSON.stringify
with a ?? null fallback emits both null and undefined as
JSON null. In practice this only matters if event data
contains explicit null values, which the standard
validation/ingest paths convert to undefined upfront — so the
single-column-vs-composite asymmetry is unreachable through the
normal API.
```ts
// Per-host event lookup
const byHost = events.partitionBy('host').toMap();
const apiEvents = byHost.get('api-1');
// With a transform — one-shot per-partition shape change
const points = events.partitionBy('host').toMap((g) => g.toPoints());
for (const [host, rows] of points) {
chart.addSeries(host, rows);
}
// Composite partition
const byHostRegion = events
.partitionBy(['host', 'region'])
.toMap();
const apiEu = byHostRegion.get('["api-1","eu"]');
```
toMap(transform: (group: TimeSeries<S>) => TimeSeries<R>): Map<K, TimeSeries<R>>
Materialize the partitioned view as a Map<key, TimeSeries<S>>,
one entry per partition. Terminal — exits the partition view.
Use this when downstream code needs to iterate or look up per
partition (typical in dashboards: one chart line per host, one
tooltip per region). Without this, the equivalent dance was
.collect().groupBy(col, fn) — two operators where one would do.
The map key is the stringified partition value for single-column
partitions, or a JSON.stringify'd array of values for composite
partitions. The single-column form preserves the value's natural
string representation (a host column with values 'api-1'
yields keys 'api-1'); composite keys produce JSON like
'["api-1","eu"]'. Map iteration order matches the order each
partition was first encountered in the source events.
undefined partition values become the literal ' undefined'
with a leading space — this avoids colliding with a string
column whose value happens to be the literal text 'undefined'.
The two are distinct buckets:
series // events with host=undefined and host='undefined'
.partitionBy('host')
.toMap();
// → 2 entries: ' undefined' (missing) vs 'undefined' (string literal)
Divergence from series.groupBy(col): groupBy uses bare
'undefined' (no leading space) for missing values, so it
collapses these two cases. toMap's leading-space sentinel is
an intentional improvement — the older groupBy shape silently
loses the distinction between "missing" and "the string
'undefined'". Migrating from groupBy to toMap will produce
different keys for partitions with undefined values; lookup
code that previously did .get('undefined') should change to
.get(' undefined') (note the leading space) to find the
missing-value bucket.
Composite encoder. For composite partitions, JSON.stringify
with a ?? null fallback emits both null and undefined as
JSON null. In practice this only matters if event data
contains explicit null values, which the standard
validation/ingest paths convert to undefined upfront — so the
single-column-vs-composite asymmetry is unreachable through the
normal API.
```ts
// Per-host event lookup
const byHost = events.partitionBy('host').toMap();
const apiEvents = byHost.get('api-1');
// With a transform — one-shot per-partition shape change
const points = events.partitionBy('host').toMap((g) => g.toPoints());
for (const [host, rows] of points) {
chart.addSeries(host, rows);
}
// Composite partition
const byHostRegion = events
.partitionBy(['host', 'region'])
.toMap();
const apiEu = byHostRegion.get('["api-1","eu"]');
```
toMap(transform: (group: TimeSeries<S>) => R): Map<K, R>
Materialize the partitioned view as a Map<key, TimeSeries<S>>,
one entry per partition. Terminal — exits the partition view.
Use this when downstream code needs to iterate or look up per
partition (typical in dashboards: one chart line per host, one
tooltip per region). Without this, the equivalent dance was
.collect().groupBy(col, fn) — two operators where one would do.
The map key is the stringified partition value for single-column
partitions, or a JSON.stringify'd array of values for composite
partitions. The single-column form preserves the value's natural
string representation (a host column with values 'api-1'
yields keys 'api-1'); composite keys produce JSON like
'["api-1","eu"]'. Map iteration order matches the order each
partition was first encountered in the source events.
undefined partition values become the literal ' undefined'
with a leading space — this avoids colliding with a string
column whose value happens to be the literal text 'undefined'.
The two are distinct buckets:
series // events with host=undefined and host='undefined'
.partitionBy('host')
.toMap();
// → 2 entries: ' undefined' (missing) vs 'undefined' (string literal)
Divergence from series.groupBy(col): groupBy uses bare
'undefined' (no leading space) for missing values, so it
collapses these two cases. toMap's leading-space sentinel is
an intentional improvement — the older groupBy shape silently
loses the distinction between "missing" and "the string
'undefined'". Migrating from groupBy to toMap will produce
different keys for partitions with undefined values; lookup
code that previously did .get('undefined') should change to
.get(' undefined') (note the leading space) to find the
missing-value bucket.
Composite encoder. For composite partitions, JSON.stringify
with a ?? null fallback emits both null and undefined as
JSON null. In practice this only matters if event data
contains explicit null values, which the standard
validation/ingest paths convert to undefined upfront — so the
single-column-vs-composite asymmetry is unreachable through the
normal API.
```ts
// Per-host event lookup
const byHost = events.partitionBy('host').toMap();
const apiEvents = byHost.get('api-1');
// With a transform — one-shot per-partition shape change
const points = events.partitionBy('host').toMap((g) => g.toPoints());
for (const [host, rows] of points) {
chart.addSeries(host, rows);
}
// Composite partition
const byHostRegion = events
.partitionBy(['host', 'region'])
.toMap();
const apiEu = byHostRegion.get('["api-1","eu"]');
```