Alignment
align(seq, opts) resamples a TimeSeries onto a
Sequence grid, producing
one Interval-keyed event per grid point. The value at each grid
point is computed by sampling the source — either holding the most
recent source value or interpolating between the two nearest.
This is the right operator when the question is "what was the value at this grid point?" — chart sampling, fixed-cadence exports, regularising irregular telemetry. For "what's the rollup of every event inside this bucket?", reach for Aggregation instead.
Mental model
The grid splits time into steps — the regions between the orange lines. sample picks where in each step to read: its begin, center, or end. At that read-line, hold takes the previous sample (to its left); linear interpolates the two samples around it. The teal dots are the real align output there; the faint lines are the construction. (See @pond-ts/charts for how the chart is built.)
For each grid point, align picks a single source value through
two settings: sample (where inside the grid step to read) and
method (how to derive the value at that point).
- TypeScript
- Rust
import { Sequence, TimeSeries } from 'pond-ts';
const cpu = TimeSeries.fromJSON({
name: 'cpu',
schema: [
{ name: 'time', kind: 'time' },
{ name: 'cpu', kind: 'number' },
] as const,
rows: [
['2025-01-01T00:00:00Z', 0.31],
['2025-01-01T00:00:30Z', 0.44],
['2025-01-01T00:01:30Z', 0.48],
],
});
const aligned = cpu.align(Sequence.every('1m'), {
method: 'hold',
sample: 'begin',
});
// one row per minute on the source's range, value held from
// the most recent source ≤ the grid point.
use pond_rs::{Align, AlignMethod, At, Column, Sequence, TimeSeries, parse_time};
fn main() -> Result<(), pond_rs::PondError> {
let times = [
"2025-01-01T00:00:00Z",
"2025-01-01T00:00:30Z",
"2025-01-01T00:01:30Z",
]
.iter()
.map(|t| parse_time(t))
.collect::<Result<Vec<_>, _>>()?;
let cpu = TimeSeries::new(
"cpu",
times,
vec![Column::required("cpu", vec![0.31, 0.44, 0.48])],
)?;
let aligned = cpu.align(
&Sequence::every("1m")?,
Align {
method: AlignMethod::Hold,
sample: At::Begin,
range: None, // the series' own range
},
)?;
// One row per minute, holding the latest reading at or before it.
let got: Vec<_> = aligned.column("cpu")?.numbers()?.iter().collect();
assert_eq!(got, [Some(0.31), Some(0.44)]);
// Linear: a straight line between the readings either side.
let linear = Align {
method: AlignMethod::Linear,
..Align::default()
};
let lined = cpu.align(&Sequence::every("1m")?, linear)?;
let got: Vec<_> = lined.column("cpu")?.numbers()?.iter().collect();
assert_eq!(got, [Some(0.31), Some(0.45999999999999996)]);
Ok(())
}
Align's Default is pond-ts's default (hold, sampled at the start, over the series' own range), so set only what you change: Align { method: AlignMethod::Linear, ..Align::default() }.
Parameters
| Option | Type | Default | Effect |
|---|---|---|---|
method | 'hold' | 'linear' | 'hold' | How to derive the value at the sample point |
sample | 'begin' | 'center' | 'end' | 'begin' | Where inside each grid step to read |
range | TimeRange | source extent | Pin the output range explicitly |
method
'hold'— carry the most recent source value at or before the sample point. Output isundefinedfor grid points before the first source event. Works on any column kind.'linear'— interpolate between the two source values surrounding the sample point. Numeric columns only; throws onTimeRange- orInterval-keyed sources, falls back to'hold'for non-numeric columns.
sample
'begin'— read at the grid step's start (the interval's inclusive begin boundary).'center'— read at the grid step's midpoint.'end'— read at the grid step's exclusive end (= the start of the next step).
The sample point determines which source events are "before" or
"after" the grid point — hold reads the most recent source ≤ the
sample point; linear interpolates across it.
range
- TypeScript
- Rust
cpu.align(Sequence.every('5s'), {
method: 'hold',
sample: 'begin',
range: new TimeRange({ start: 0, end: 60_000 }),
});
use pond_rs::{Align, Column, Sequence, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let cpu = TimeSeries::new(
"cpu",
vec![10_000, 22_000],
vec![Column::required("cpu", vec![1.0, 2.0])],
)?;
// Every 5 s from 0 to 60 s, both ends included.
let padded = cpu.align(
&Sequence::every("5s")?,
Align {
range: Some((0, 60_000)),
..Align::default()
},
)?;
assert_eq!(padded.len(), 13);
let cpu = padded.column("cpu")?.numbers()?;
assert_eq!(cpu.get(0), None); // before the first reading
assert_eq!(cpu.get(2), Some(1.0)); // 10 s
assert_eq!(cpu.get(12), Some(2.0)); // 60 s, held from 22 s
Ok(())
}
range is a pair of epoch milliseconds, Some((begin, end)), rather than a TimeRange. A grid point counts when its sample point falls in it, both ends included.
Pins the output range explicitly. Default is the source's own
extent. Use range when you want a uniform grid over a specific
window even if the source doesn't fill it — the output schema is
the same, just padded with undefineds outside the source range
(or the most recent value, depending on method).
Output schema
The output is an Interval-keyed TimeSeries<S> — same column
names and types as the source. Each row is one grid point.
- TypeScript
- Rust
aligned.schema[0].kind; // 'interval'
aligned.at(0)?.key(); // Interval — start = grid begin, end = grid end
aligned.at(0)?.get('cpu'); // number | undefined
use pond_rs::{Align, Column, Label, Sequence, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let cpu = TimeSeries::new(
"cpu",
vec![0, 30_000, 90_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48])],
)?;
let aligned = cpu.align(&Sequence::every("1m")?, Align::default())?;
assert_eq!(aligned.key_name(), "interval");
let key = aligned.at(0).unwrap().key(); // start = grid begin, end = grid end
assert_eq!((key.begin(), key.end()), (0, 60_000));
assert_eq!(key.label(), Some(&Label::from(0.0))); // labelled by its start
assert_eq!(aligned.at(0).unwrap().number("cpu")?, Some(0.31));
Ok(())
}
The grid is half-open [begin, end) — events at exactly the
boundary belong to the later grid step.
Time-keyed output for chart libraries
align returns interval-keyed events (consistent with
aggregate). Most chart libraries want point-keyed output —
chain .asTime({ at }) to convert:
- TypeScript
- Rust
const points = cpu
.align(Sequence.every('5m'), { method: 'hold', sample: 'end' })
.asTime({ at: 'end' });
// points.at(0)?.key() is now a Time at the interval's end.
use pond_rs::{Align, At, Column, Sequence, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let min = 60_000;
let cpu = TimeSeries::new(
"cpu",
vec![0, 4 * min, 7 * min, 12 * min, 16 * min],
vec![Column::required("cpu", vec![0.1, 0.2, 0.3, 0.4, 0.5])],
)?;
let points = cpu
.align(
&Sequence::every("5m")?,
Align {
sample: At::End,
..Align::default()
},
)?
.as_time(At::End)?;
assert_eq!(points.key_name(), "time");
assert_eq!(points.key().begin(), &[5 * min, 10 * min, 15 * min]);
let got: Vec<_> = points.column("cpu")?.numbers()?.iter().collect();
assert_eq!(got, [Some(0.2), Some(0.3), Some(0.4)]);
Ok(())
}
as_time takes At::Begin, At::Center or At::End.
asTime's at option ('begin' | 'center' | 'end') picks where
on each interval the resulting Time lands.
Compared to aggregate
Same inputs (a sequence and an optional range; range goes in align's
options object but is aggregate's third argument), same output key shape
(Interval-keyed grid), different reduction.
| Question | Operator |
|---|---|
| "What was the value at this grid point?" | align(seq, { method, sample }) |
| "What's the rollup of every event in this bucket?" | aggregate(seq, mapping) |
- TypeScript
- Rust
// align — one row per minute, value sampled from the source.
cpu.align(Sequence.every('1m'), { method: 'hold' });
// aggregate — one row per minute, value reduced from sources inside.
cpu.aggregate(Sequence.every('1m'), { cpu: 'avg' });
use pond_rs::{Agg, Align, Column, Reducer, Sequence, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let cpu = TimeSeries::new(
"cpu",
vec![0, 30_000, 90_000],
vec![Column::required("cpu", vec![0.2, 0.4, 0.6])],
)?;
let minute = Sequence::every("1m")?;
// align: one row per minute, value sampled at each minute's start.
let held = cpu.align(&minute, Align::default())?;
let got: Vec<_> = held.column("cpu")?.numbers()?.iter().collect();
assert_eq!(got, [Some(0.2), Some(0.4)]);
// aggregate: one row per minute, value reduced from the rows inside.
let avg = cpu.aggregate(&minute, &[Agg::of("cpu", Reducer::Avg)])?;
let got: Vec<_> = avg.column("cpu")?.numbers()?.iter().collect();
assert_eq!(got, [Some(0.30000000000000004), Some(0.6)]);
Ok(())
}
For a chart at one-minute resolution where every grid point should
have a value, align is right. For "average CPU per minute,"
aggregate is right. The two are complementary.
See Aggregation for the bucketing operator.
See also
- Aggregation — bucket-rollup counterpart
- Sequences — grid definitions
- Cleaning data — handle gaps left by
holdorlinear(e.g. earlyundefineds before the first source) - Reducer reference — empty-bucket and
rolling-window behaviour for every reducer (relevant for the
aggregatecounterpart)