Transformations
The simplest shape: events in, events out. One event per source event, same temporal key, sometimes a different schema. These are the operators that preserve the event grid — they don't bucket, window, or aggregate.
For operations that change the event grid (rolling windows, fixed-bucket aggregation, alignment), see Concepts → Windowing and the Aggregation, Alignment, and Rolling Windows operator pages.
Two flavors:
- Per-event-only — operate on one event at a time, no memory.
map,select,rename,collapse,filter, plusset/mergeon individual events. - Per-event-with-tiny-state — look at the previous event (or a
small accumulator) to compute each output.
diff,rate,pctChange,cumulative,scan,shift.
Coming from pandas: this section covers .assign(), .rename(),
[cols], .where() (≈ filter), .diff(), .pct_change(),
.cumsum(), .shift(). Coming from JavaScript arrays: map, filter
do exactly what you'd expect; the rest are time-series-aware.
Per-event-only
map(nextSchema, fn) — transform each event
The most general. Pass the output schema (so the result type is narrow) and a function that takes a source event and returns the new one. The schema argument is mandatory even when the shape doesn't change — pond-ts always wants the next schema explicit.
- TypeScript
- Rust
// Same schema; just transform the cpu value.
const scaled = cpu.map(cpu.schema, (e) => {
const value = e.get('cpu');
return value === undefined ? e : e.set('cpu', value * 100);
});
// New schema — adds a "flagged" column.
const flaggedSchema = [
...cpu.schema,
{ name: 'flagged', kind: 'boolean' },
] as const;
const flagged = cpu.map(flaggedSchema, (e) =>
e.merge({ flagged: (e.get('cpu') ?? 0) > 0.9 }),
);
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let cpu = TimeSeries::new(
"cpu",
vec![0, 1_000, 2_000, 3_000],
vec![Column::optional(
"cpu",
vec![Some(0.5), None, Some(0.95), Some(0.4)],
)],
)?;
// Same columns; transform the cpu values. Gaps are not passed in.
let scaled = cpu.map_numbers("cpu", |v| v * 100.0)?;
let got: Vec<_> = scaled.column("cpu")?.numbers()?.iter().collect();
assert_eq!(got, [Some(50.0), None, Some(95.0), Some(40.0)]);
// A new "flagged" column, worked out from cpu.
let values = cpu.column("cpu")?.numbers()?;
let flags: Vec<bool> = values.iter().map(|v| v.unwrap_or(0.0) > 0.9).collect();
let flagged = cpu.with_column(Column::required("flagged", flags))?;
let got: Vec<_> = flagged.column("flagged")?.booleans()?.iter().collect();
assert_eq!(got, [Some(false), Some(false), Some(true), Some(false)]);
Ok(())
}
pond-rs has no general map over whole events. Change one column's values with map_numbers (or map_strings, map_booleans, map_decimals), and add a column with with_column.
Event instances are immutable; set returns a new Event with the
field updated. merge(patch) works the same way for multi-field
updates and can introduce new columns.
filter(predicate) — drop events
Predicate gets an event and an index, returns true to keep:
- TypeScript
- Rust
const active = cpu.filter((e) => e.get('cpu') !== undefined);
const apiOnly = events.filter((e) => e.get('host')?.startsWith('api'));
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let events = TimeSeries::new(
"requests",
vec![0, 1_000, 2_000, 3_000],
vec![
Column::optional("cpu", vec![Some(0.5), None, Some(0.95), Some(0.4)]),
Column::required("host", vec!["api-1", "db-1", "api-2", "api-1"]),
],
)?;
let active = events.filter(|e| Ok(e.number("cpu")?.is_some()))?;
let api_only =
events.filter(|e| Ok(e.string("host")?.is_some_and(|h| h.starts_with("api"))))?;
assert_eq!(active.len(), 3);
assert_eq!(api_only.key().begin(), &[0, 2_000, 3_000]);
Ok(())
}
The closure gets a Row and returns a Result, so lookups inside it can use ?.
Same schema in, same schema out; just fewer rows. See Cleaning data for filter as a "remove bad rows" pattern.
filter also takes an expression in place of the
function, such as cpu.filter(col('cpu').gt(0.5)). It runs over whole
columns and is usually several times faster.
select(...keys) — keep only some columns
Schema-narrowing — type-level too:
- TypeScript
- Rust
const just = full.select('cpu', 'host');
// just.schema === [time, cpu, host]; the other columns are gone.
just.at(0)?.get('cpu'); // still narrows to number | undefined
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let full = TimeSeries::new(
"metrics",
vec![0, 1_000],
vec![
Column::required("cpu", vec![0.5, 0.7]),
Column::required("requests", vec![10.0, 20.0]),
Column::required("host", vec!["api-1", "api-2"]),
],
)?;
let just = full.select(&["host", "cpu"])?;
let names: Vec<&str> = just.columns().iter().map(|c| c.name()).collect();
assert_eq!(names, ["cpu", "host"]); // schema order, not the order asked
assert_eq!(just.column("cpu")?.numbers()?.get(0), Some(0.5));
assert!(full.select(&["ghost"]).is_err()); // not a column
Ok(())
}
select takes a slice of names. An unknown name is an error.
Pass the names as separate arguments. A name that isn't a payload
column throws a RangeError listing the valid columns, and an array
argument (select(['cpu', 'host'])) throws a TypeError — spread it:
select(...names). Output columns keep schema order; a repeated name
selects its column once.
rename(mapping) — rename payload columns
- TypeScript
- Rust
const renamed = cpu.rename({ cpu: 'usage' });
renamed.at(0)?.get('usage'); // number | undefined
// renamed.schema has 'usage' where 'cpu' was.
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let cpu = TimeSeries::new(
"cpu",
vec![0, 1_000],
vec![
Column::required("cpu", vec![0.5, 0.7]),
Column::required("host", vec!["a", "b"]),
],
)?;
let renamed = cpu.rename(&[("cpu", "usage")])?;
let names: Vec<&str> = renamed.columns().iter().map(|c| c.name()).collect();
assert_eq!(names, ["usage", "host"]); // 'usage' where 'cpu' was
assert_eq!(renamed.column("usage")?.numbers()?.get(0), Some(0.5));
Ok(())
}
rename takes (from, to) pairs.
The temporal key column can't be renamed.
collapse(keys, output, reducer) — combine columns into one
Reducer receives a typed record of just the named columns, returns a single value:
- TypeScript
- Rust
// Average of cpu and requests into a "load" column.
const summary = metrics.collapse(
['cpu', 'requests'],
'load',
({ cpu, requests }) => ((cpu ?? 0) + (requests ?? 0)) / 2,
);
// schema: [time, ...other source columns, load]
use pond_rs::{Column, Scalar, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let metrics = TimeSeries::new(
"metrics",
vec![0, 1_000, 2_000],
vec![
Column::optional("cpu", vec![Some(0.5), None, Some(0.95)]),
Column::required("requests", vec![10.0, 20.0, 40.0]),
Column::required("host", vec!["api-1", "db-1", "api-2"]),
],
)?;
// Average of cpu and requests into a "load" column.
let load = |r: &pond_rs::Row| {
let cpu = r.number("cpu")?.unwrap_or(0.0);
let requests = r.number("requests")?.unwrap_or(0.0);
Ok(Some(Scalar::Number((cpu + requests) / 2.0)))
};
let summary = metrics.collapse(&["cpu", "requests"], "load", load)?;
let names: Vec<&str> = summary.columns().iter().map(|c| c.name()).collect();
assert_eq!(names, ["host", "load"]);
let got: Vec<_> = summary.column("load")?.numbers()?.iter().collect();
assert_eq!(got, [Some(5.25), Some(10.0), Some(20.475)]);
// collapse_keep is pond-ts's { append: true }.
let kept = metrics.collapse_keep(&["cpu", "requests"], "load", load)?;
assert_eq!(kept.columns().len(), 4);
Ok(())
}
The closure gets the whole row and returns Ok(Some(Scalar)), or Ok(None) for a gap. The output column comes after the columns that are kept.
The named columns are removed by default; { append: true } keeps
them and adds the new one alongside.
Event.set(field, value) and Event.merge(patch)
Work directly on an Event. You'll use them inside map callbacks:
events.map((e) => e.set('cpuPct', (e.get('cpu') ?? 0) * 100));
events.map((e) =>
e.merge({
cpu: (e.get('cpu') ?? 0) * 100,
flagged: (e.get('cpu') ?? 0) > 0.9,
}),
);
set returns the same key; merge does too. Both are immutable.
pond-rs has no Event objects to set or merge: a Row is a read-only
view. Use map_numbers, with_column or collapse instead.
Per-event-with-tiny-state
These look at one event plus the previous (or an accumulator) to
compute each output. Length is preserved; the first event has no
predecessor, so target columns are undefined (or you can drop the
first event with { drop: true }).
diff(cols) — per-event delta
- TypeScript
- Rust
const deltas = series.diff('requests');
// requests = curr - prev. First event: undefined.
const both = series.diff(['cpu', 'mem']);
const dropped = series.diff('requests', { drop: true });
use pond_rs::{Change, Column, FirstRow, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"web",
vec![0, 1_000, 2_000, 3_000],
vec![
Column::required("requests", vec![10.0, 20.0, 40.0, 30.0]),
Column::required("cpu", vec![0.5, 0.6, 0.95, 0.4]),
Column::required("mem", vec![1.0, 1.5, 1.5, 2.0]),
],
)?;
let deltas = series.diff(&["requests"])?;
let got: Vec<_> = deltas.column("requests")?.numbers()?.iter().collect();
assert_eq!(got, [None, Some(10.0), Some(20.0), Some(-10.0)]); // first row: a gap
let both = series.diff(&["cpu", "mem"])?;
assert_eq!(both.column("mem")?.numbers()?.get(1), Some(0.5));
let dropped = series.diff_with(&["requests"], Change::default().first(FirstRow::Drop))?;
assert_eq!(dropped.len(), 3);
// Keep the source and add the change beside it.
let with_change = series.diff_with(&["requests"], Change::default().output("requestsDelta"))?;
assert_eq!(
with_change.column("requests")?.numbers()?.get(1),
Some(20.0)
);
assert_eq!(
with_change.column("requestsDelta")?.numbers()?.get(1),
Some(10.0)
);
Ok(())
}
diff_with takes a Change: .first(FirstRow::Drop) is { drop: true } and .output(name) is { output }. This covers the output example below too.
diff, rate and pctChange replace each named column with its change.
To keep the source and add the change beside it, name an output (one
source column only; the name must be new):
const withChange = series.diff('requests', { output: 'requestsDelta' });
// requests unchanged; requestsDelta = curr - prev (optional number)
rate(cols) — delta per second
diff divided by the time gap (in seconds). When two consecutive
events share a timestamp (dt = 0), rate returns undefined
rather than dividing by zero — filter per-producer first if you have
concurrent emissions.
- TypeScript
- Rust
const rps = series.rate('requests');
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
// A request counter read at 0 s, 1 s, 3 s, and twice at 3 s.
let series = TimeSeries::new(
"web",
vec![0, 1_000, 3_000, 3_000],
vec![Column::required(
"requests",
vec![100.0, 110.0, 150.0, 160.0],
)],
)?;
let rps = series.rate(&["requests"])?;
let got: Vec<_> = rps.column("requests")?.numbers()?.iter().collect();
// 10 in 1 s, 40 in 2 s, then no time between the last two rows.
assert_eq!(got, [None, Some(10.0), Some(20.0), None]);
Ok(())
}
pctChange(cols) — relative change
(curr - prev) / prev. Time-gap-independent. Returns undefined
when the previous value is 0.
- TypeScript
- Rust
const rets = prices.pctChange('price');
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let prices = TimeSeries::new(
"prices",
vec![0, 1_000, 2_000, 3_000],
vec![Column::required("price", vec![10.0, 20.0, 0.0, 5.0])],
)?;
let rets = prices.pct_change(&["price"])?;
let got: Vec<_> = rets.column("price")?.numbers()?.iter().collect();
assert_eq!(got, [None, Some(1.0), Some(-1.0), None]); // previous 0: a gap
Ok(())
}
cumulative(spec) — running total / max / min / count / rises / falls
- TypeScript
- Rust
const running = series.cumulative({ requests: 'sum', cpu: 'max' });
const errors = series.cumulative({ errors: 'count' });
const product = series.cumulative({ factor: (acc, v) => acc * v });
use pond_rs::{Column, Reducer, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"web",
vec![0, 1_000, 2_000, 3_000],
vec![
Column::required("requests", vec![10.0, 20.0, 40.0, 30.0]),
Column::optional("cpu", vec![Some(0.5), None, Some(0.95), Some(0.4)]),
],
)?;
let running = series.cumulative(&[("requests", Reducer::Sum), ("cpu", Reducer::Max)])?;
let sums: Vec<_> = running.column("requests")?.numbers()?.iter().collect();
let peaks: Vec<_> = running.column("cpu")?.numbers()?.iter().collect();
assert_eq!(sums, [Some(10.0), Some(30.0), Some(70.0), Some(100.0)]);
assert_eq!(peaks, [Some(0.5), Some(0.5), Some(0.95), Some(0.95)]); // the gap carries
let counts = series.cumulative(&[("cpu", Reducer::Count)])?;
let got: Vec<_> = counts.column("cpu")?.numbers()?.iter().collect();
assert_eq!(got, [Some(1.0), Some(1.0), Some(2.0), Some(3.0)]);
// A running fold of your own.
let product = series.cumulative_with("requests", |acc, v| acc * v)?;
assert_eq!(
product.column("requests")?.numbers()?.get(3),
Some(240_000.0)
);
Ok(())
}
A fold of your own is cumulative_with, one column at a time.
When a source value is undefined, the accumulator carries forward
its previous value — no NaN propagation.
Rises and falls: 'increase' / 'decrease'
'increase' is the running total of rises between values, 'decrease'
the running total of falls (a positive number). Give a threshold to
ignore small wiggles: a change counts only once the value has moved at
least that far from the last value that counted, in either direction, so
slow drift still adds up. That is elevation gain with a dead band, or the
use of a noisy meter or level.
- TypeScript
- Rust
// ele: 100, 101, 104, (gap), 102, 99, 103, 108
const gain = track.cumulative({ ele: { using: 'increase', threshold: 3 } });
// → 0, 0, 4, 4, 4, 4, 8, 13
const loss = track.cumulative({ ele: { using: 'decrease', threshold: 3 } });
// → 0, 0, 0, 0, 0, 5, 5, 5
const everyRise = track.cumulative({ ele: 'increase' });
use pond_rs::{Column, Cumulative, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let track = TimeSeries::new(
"track",
(0..8).map(|i| i * 1_000).collect(),
vec![Column::optional(
"ele",
vec![
Some(100.0),
Some(101.0),
Some(104.0),
None,
Some(102.0),
Some(99.0),
Some(103.0),
Some(108.0),
],
)],
)?;
let gain = track.cumulative(&[("ele", Cumulative::increase().threshold(3.0))])?;
let got: Vec<_> = gain.column("ele")?.numbers()?.iter().collect();
let want = [0.0, 0.0, 4.0, 4.0, 4.0, 4.0, 8.0, 13.0];
assert_eq!(got, want.map(Some));
let loss = track.cumulative(&[("ele", Cumulative::decrease().threshold(3.0))])?;
let got: Vec<_> = loss.column("ele")?.numbers()?.iter().collect();
let want = [0.0, 0.0, 0.0, 0.0, 0.0, 5.0, 5.0, 5.0];
assert_eq!(got, want.map(Some));
Ok(())
}
A Reducer mixes in with .into():
&[("ele", Cumulative::increase()), ("work", Reducer::Sum.into())].
The first value starts the total at 0, and both directions share one
reference point. Because it is a named accumulator rather than a step
function, @pond-ts/rs runs it without calling back into JavaScript.
scan(source, step, init, options?) — typed-accumulator running fold
scan is the general form of cumulative — the classic mapAccumL.
cumulative locks three things together (the accumulator is the
output is a number, written in place); scan decouples them:
the accumulator A can be any value, the numeric output is separate,
and you choose whether to replace the source or append a new column.
- TypeScript
- Rust
// running sum — the cumulative special case, replacing in place:
series.scan('work', (acc, v) => [acc + v, acc + v], 0);
// ≡ series.cumulative({ work: 'sum' })
// typed accumulator into a NEW column: hysteresis elevation gain
// carries (ref, gain) but emits only the gain. (This exact rule is built
// in as cumulative({ ele: { using: 'increase', threshold: 3 } }); the scan
// form shows how to write your own.)
const T = 3; // ±3 m deadband
const withGain = track.scan<'cumGain', { ref: number | null; gain: number }>(
'ele',
(acc, ele) => {
if (acc.ref === null) return [{ ref: ele, gain: 0 }, 0];
const d = ele - acc.ref;
if (d >= T) return [{ ref: ele, gain: acc.gain + d }, acc.gain + d];
if (d <= -T) return [{ ref: ele, gain: acc.gain }, acc.gain];
return [acc, acc.gain]; // within deadband — carry
},
{ ref: null, gain: 0 },
{ output: 'cumGain' },
);
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let track = TimeSeries::new(
"track",
(0..8).map(|i| i * 1_000).collect(),
vec![Column::optional(
"ele",
vec![
Some(100.0),
Some(101.0),
Some(104.0),
None,
Some(102.0),
Some(99.0),
Some(103.0),
Some(108.0),
],
)],
)?;
// Running sum, replacing the column: the same as cumulative's Sum.
let sum = track.scan("ele", 0.0, |acc, v, _row| (acc + v, acc + v))?;
assert_eq!(sum.column("ele")?.numbers()?.get(7), Some(717.0));
// Hysteresis elevation gain into a new column. The state carries
// (ref, gain) but each row gets only the gain.
const T: f64 = 3.0; // ±3 m deadband
let with_gain = track.scan_to(
"ele",
"cumGain",
(None::<f64>, 0.0),
|(reference, gain), ele, _row| match reference {
None => ((Some(ele), 0.0), 0.0),
Some(r) if ele - r >= T => ((Some(ele), gain + ele - r), gain + ele - r),
Some(r) if ele - r <= -T => ((Some(ele), gain), gain),
Some(r) => ((Some(r), gain), gain), // within deadband: carry
},
)?;
let got: Vec<_> = with_gain.column("cumGain")?.numbers()?.iter().collect();
let want = [0.0, 0.0, 4.0, 4.0, 4.0, 4.0, 8.0, 13.0];
assert_eq!(got, want.map(Some)); // the gap at row 3 repeats the last output
Ok(())
}
scan(column, init, step) takes the start value before the step, and scan_to(column, output, init, step) writes a new column and keeps the source. The state's type comes from init. The step also gets the row number.
step(acc, value, i) returns [nextAcc, output]. The accumulator A
is inferred from init. With no options.output the source column is
replaced (widened to optional number, as cumulative does); with
options.output a new column of that name is appended and the
source is left intact (the name must not already exist).
Missing cells inherit cumulative's behavior exactly: a missing /
undefined source cell does not call step — the accumulator is held
and the row re-emits the last output (so it holds flat across a gap),
undefined only until the first defined value. A stored NaN is a
defined number and is passed to step. On a multi-entity series the
accumulator interleaves across entities; scope it per entity with
partitionBy, run scan inside each partition, and fan back in with
collect:
- TypeScript
- Rust
// One running total per host, not a single interleaved accumulator.
const perHost = series
.partitionBy('host')
.scan('requests', (acc, v) => [acc + v, acc + v], 0)
.collect();
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"web",
vec![0, 0, 1_000, 1_000, 2_000, 2_000],
vec![
Column::required("host", vec!["a", "b", "a", "b", "a", "b"]),
Column::required("requests", vec![1.0, 10.0, 2.0, 20.0, 3.0, 30.0]),
],
)?;
// One running total per host, not a single interleaved one.
let per_host = series
.partition_by(&["host"])?
.scan("requests", 0.0, |acc, v, _row| (acc + v, acc + v))?
.collect()?;
let got: Vec<_> = per_host.column("requests")?.numbers()?.iter().collect();
let want = [1.0, 10.0, 3.0, 30.0, 6.0, 60.0];
assert_eq!(got, want.map(Some));
Ok(())
}
Each partition starts from its own copy of the start value.
split = scan + byColumn. The reason scan exists: it isolates the
order-dependent state into a column so value-axis aggregation
(byColumn) can stay pure and order-free.
Materialize the carried state with scan, then segment statelessly —
e.g. per-kilometre split gain is last − first of the scanned cumGain
in each distance bin. See
Value axis for the mental model
behind this composition.
tail(duration) — keep the trailing window
tail is technically a temporal selection rather than a per-event
op, but it's tiny and lives next to these in practice — keep events
whose begin() is greater than lastEvent.begin() - duration. Same
schema, fewer rows.
- TypeScript
- Rust
const recent = series.tail('30s');
const all = series.tail(); // identity (whole series) when no duration
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 1_000, 2_000, 3_000],
vec![Column::required("cpu", vec![0.5, 0.6, 0.95, 0.4])],
)?;
// Rows that start after 3 s - 2 s = 1 s.
let recent = series.tail("2s")?;
assert_eq!(recent.key().begin(), &[2_000, 3_000]);
Ok(())
}
tail needs a duration; there is no form with no argument.
Often paired with reduce for "current state" readouts — see
Rolling windows → tail for the composition.
shift(cols, n) — lag / lead
Move column values forward (positive n = lag) or backward
(negative n = lead). Vacated slots become undefined. Keys are
unchanged.
- TypeScript
- Rust
const lagged = series.shift('value', 1);
const lead = series.shift('value', -1);
const multi = series.shift(['cpu', 'mem'], 2);
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"s",
vec![0, 1_000, 2_000, 3_000],
vec![
Column::required("value", vec![10.0, 20.0, 40.0, 30.0]),
Column::required("cpu", vec![0.5, 0.6, 0.95, 0.4]),
Column::required("mem", vec![1.0, 1.5, 1.5, 2.0]),
],
)?;
let lagged = series.shift(&["value"], 1)?;
let lead = series.shift(&["value"], -1)?;
let multi = series.shift(&["cpu", "mem"], 2)?;
let got: Vec<_> = lagged.column("value")?.numbers()?.iter().collect();
assert_eq!(got, [None, Some(10.0), Some(20.0), Some(40.0)]);
let got: Vec<_> = lead.column("value")?.numbers()?.iter().collect();
assert_eq!(got, [Some(20.0), Some(40.0), Some(30.0), None]);
assert_eq!(multi.column("cpu")?.numbers()?.get(2), Some(0.5));
Ok(())
}
pond-rs's shift works on every column kind, not only numbers.
For time-based shifts on irregular data, align to a regular grid first (Alignment), then shift by the corresponding number of events.
offsetTime(by) — move the keys in time
Moves every key by a constant — milliseconds or a duration string,
negative for earlier. A duration is a fixed length: '1d' is 24 hours, not
a calendar day, so across a daylight-saving change an overlay is an hour
off. Values stay on their rows; a timeRange /
interval key moves both edges and keeps its labels. Value columns
are shared, not copied, so this costs one pass over the key.
- TypeScript
- Rust
// Overlay yesterday on today: yesterday's rows, keyed a day later.
const yesterdayOnToday = yesterday.offsetTime('1d');
const skewFixed = sensor.offsetTime(-250); // clock ran 250 ms fast
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let day = 86_400_000;
let yesterday = TimeSeries::new(
"load",
vec![0, 3_600_000],
vec![Column::required("v", vec![1.0, 2.0])],
)?;
// Overlay yesterday on today: yesterday's rows, keyed a day later.
let yesterday_on_today = yesterday.offset_time("1d")?;
assert_eq!(yesterday_on_today.key().begin(), &[day, day + 3_600_000]);
let skew_fixed = yesterday.offset_time_ms(-250)?; // clock ran 250 ms fast
assert_eq!(skew_fixed.key().begin(), &[-250, 3_599_750]);
Ok(())
}
offset_time takes a duration and offset_time_ms takes milliseconds.
shift moves values by rows and leaves keys alone; offsetTime
moves keys by time and leaves values alone. If you only want a
chart to draw a column somewhere else — a 1-minute bar's close at
the bar's end — use the layer's xOffset prop in
@pond-ts/charts instead: the series stays as it is.
Method comparison
| Method | Output |
|---|---|
map(nextSchema, fn) | New schema possible; one event in, one out |
filter(pred) | Same schema; subset of rows |
select(...keys) | Schema narrowed to chosen columns |
rename(mapping) | Schema with renamed columns |
collapse(cols, ...) | Schema replaces several columns with one |
diff(cols) | Same schema; per-event delta column |
rate(cols) | Same schema; delta-per-second column |
pctChange(cols) | Same schema; relative-change column |
cumulative(spec) | Same schema; running aggregate column |
scan(src, …) | Replace source, or append output column |
tail(duration) | Same schema; trailing temporal slice |
shift(cols, n) | Same schema; column values shifted |
offsetTime(by) | Same schema; every key moved by by |
Event.set(field, v) | Single field on one event |
Event.merge(patch) | Multi-field patch on one event |