Queries
Methods that interrogate a TimeSeries without changing it. None
of these mutate; none of them return a new TimeSeries (except the
subseries selectors at the bottom, which still leave the source
untouched). Reach for these when you need to ask the series for
something rather than transform it.
- TypeScript
- Rust
const cpu = TimeSeries.fromJSON({ name, schema, rows });
cpu.length; // 5
cpu.first()?.get('cpu'); // 0.31 — one row, via an Event
cpu.column('cpu').mean(); // 0.476 — a whole column, no per-row Events
cpu.timeRange(); // TimeRange — .begin() / .end() in epoch ms
cpu.tail('30s').reduce({ cpu: 'avg' }); // current state
use pond_rs::{Agg, Column, EventKey, Reducer, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let cpu = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000, 30_000, 40_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48, 0.62, 0.53])],
)?;
assert_eq!(cpu.len(), 5);
assert_eq!(cpu.first().unwrap().number("cpu")?, Some(0.31)); // one row
assert_eq!(cpu.reduce_number("cpu", Reducer::Avg)?, Some(0.476)); // a whole column
assert_eq!(cpu.time_range(), Some(EventKey::time_range(0, 40_000)?));
let now = cpu.tail("30s")?.reduce(&[Agg::of("cpu", Reducer::Avg)])?; // current state
assert_eq!(now.number("cpu")?, Some(0.5433333333333333));
Ok(())
}
pond-rs columns have no mean(); a whole-column answer comes from reduce_number, as below under Reading columns.
Two ways to read: the row API (Events — first(), at(i),
get('cpu'), iteration) for per-event logic, and the column API
(column('cpu') — reductions and typed-array access) for throughput.
The columnar backing (added in v0.18.0) made the column path first-class;
the row API is unchanged. This page covers reading via rows; the column
path is its own page (Columns), summarized under
Reading columns below.
Counts and bounds
length
Number of events. Plain property.
series.length; // number
timeRange()
Overall extent as a TimeRange, or undefined for an empty series.
It's a TimeRange instance — .begin() / .end() return the bounds in
epoch milliseconds (not .start / .end fields):
- TypeScript
- Rust
const r = series.timeRange(); // TimeRange | undefined
r?.begin(); // number (epoch ms)
r?.end();
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48])],
)?;
assert_eq!(series.len(), 3);
// The overall span, as an `EventKey`; `None` for an empty series.
let r = series.time_range().unwrap();
assert_eq!((r.begin(), r.end()), (0, 20_000));
// `rows()` walks the rows; each `Row` is a view, not a copy.
let times: Vec<i64> = series.rows().map(|row| row.begin()).collect();
assert_eq!(times, [0, 10_000, 20_000]);
Ok(())
}
time_range() gives an EventKey. There is no events array; rows() walks the rows instead.
events
The rows as an array of Event objects — for when you need an
Array.map / Array.find shape rather than the TimeSeries API:
series.events; // ReadonlyArray<EventForSchema<S>>
A TimeSeries is backed by typed columns, not Event objects, so
events materializes one Event per row on first access (lazily,
then memoized). That's fine for row-shaped logic, but for a scan or a
reduction over a single field, read the column instead — see
Reading columns.
Single events
at(index)
Event at a positional index, or undefined if out of bounds.
Negative indices count from the end (-1 is the last event).
- TypeScript
- Rust
series.at(0); // first event, or undefined
series.at(-1); // last event, same as series.last()
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000, 30_000],
vec![Column::required("cpu", vec![0.31, 0.95, 0.48, 0.62])],
)?;
assert_eq!(series.at(0).unwrap().begin(), 0);
assert_eq!(series.at(-1).unwrap().begin(), 30_000); // same as last()
assert!(series.at(4).is_none()); // past the end
assert_eq!(series.first().unwrap().number("cpu")?, Some(0.31));
assert_eq!(series.last().unwrap().number("cpu")?, Some(0.62));
// The closure returns a Result, so lookups inside it can use `?`.
let spike = series.find(|r| Ok(r.number("cpu")?.unwrap_or(0.0) > 0.9))?;
assert_eq!(spike.unwrap().begin(), 10_000);
Ok(())
}
This covers first(), last() and find() below as well.
first() / last()
series.first(); // EventForSchema<S> | undefined
series.last(); // EventForSchema<S> | undefined
find(predicate)
First event matching a predicate, like Array.prototype.find. It also takes an
expression, series.find(col('cpu').gt(0.9)), and still
stops at the first match.
const spike = series.find((e) => (e.get('cpu') ?? 0) > 0.9);
Reading columns
To read values rather than events, go through a column instead of
materializing an Event per row. series.column(name) returns a typed,
read-only view of one field, narrowed by the schema:
- TypeScript
- Rust
const cpu = series.column('cpu'); // Float64Column (number kind)
cpu.mean(); // number | undefined — one validity-aware pass
cpu.minMax(); // [min, max] | undefined — the Y-extent, one scan
cpu.at(5); // number | undefined — the value at row 5
cpu.toFloat64Array(); // dense Float64Array for a canvas / WebGL loop
use pond_rs::{Column, Reducer, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000, 30_000, 40_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48, 0.62, 0.53])],
)?;
let cpu = series.column("cpu")?.numbers()?; // fails if not a number column
assert_eq!(cpu.get(3), Some(0.62)); // the value at row 3
assert_eq!(cpu.values(), &[0.31, 0.44, 0.48, 0.62, 0.53]); // a plain &[f64]
// Reductions go through the series.
assert_eq!(series.reduce_number("cpu", Reducer::Avg)?, Some(0.476));
assert_eq!(series.reduce_number("cpu", Reducer::Min)?, Some(0.31));
assert_eq!(series.reduce_number("cpu", Reducer::Max)?, Some(0.62));
Ok(())
}
A pond-rs column holds the values but has no reductions (mean, minMax, uniqueCount, all, …); reduce through the series with reduce_number. values() is the raw buffer, where a gap is a non-finite number; iter() gives Option<f64>s, and get(i) panics past the end rather than giving None.
Numeric columns carry the scalar reductions (min / max / sum /
mean / stdev / median / percentile / count / minMax); string
columns have uniqueCount(), boolean columns all() / any() /
none(). series.keyColumn() is the same idea for the time axis. The
column API (added with the columnar backing in v0.18.0) is the
throughput path — no per-row Event — while the row methods above stay
the default for per-event logic.
This is the short version; the full column surface (missing-value
handling, bin, scan, chunked columns) is on the
Columns page.
Key-based lookups
For series with sorted keys (which is every TimeSeries), these are
binary-search lookups in O(log N). Pass a Time, Interval,
TimeRange, or raw timestamp.
includesKey(key)
Does any event match this key? true / false.
series.includesKey(new Time(0)); // boolean
series.includesKey(60_000); // also accepts a raw timestamp
bisect(key)
The index where this key would be inserted to keep order. Useful for "find the position of a timestamp without requiring an exact match":
const i = series.bisect(targetTimestamp);
// 0 ≤ i ≤ series.length
atOrBefore(key) / atOrAfter(key)
The nearest event that doesn't overshoot the key. Common pattern:
"the most recent reading at or before t":
- TypeScript
- Rust
const previous = series.atOrBefore(Date.now());
const next = series.atOrAfter(Date.now());
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000, 30_000, 40_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48, 0.62, 0.53])],
)?;
assert!(series.includes_key(10_000));
assert!(!series.includes_key(15_000));
assert_eq!(series.bisect(15_000), 2);
assert_eq!(series.at_or_before(15_000).unwrap().begin(), 10_000);
assert_eq!(series.at_or_after(15_000).unwrap().begin(), 20_000);
assert_eq!(series.nearest(26_000).unwrap().begin(), 30_000);
assert_eq!(series.nearest(25_000).unwrap().begin(), 20_000); // a tie: the earlier row
Ok(())
}
This covers includes_key, bisect and nearest too. A plain i64 time works as a key; build other keys with EventKey::time_range or EventKey::interval.
nearest(key)
The single event closest to the key by begin() distance, either side
(vs atOrBefore/atOrAfter, which pick a direction). undefined for
an empty series:
series.nearest(Date.now()); // the closest reading to now
Predicates over the series
some(predicate) / every(predicate)
Array-style predicate scans. Like find, both also take an
expression such as col('cpu').gt(0.9), which runs over
whole columns:
- TypeScript
- Rust
series.some((e) => (e.get('cpu') ?? 0) > 0.9); // any spike?
series.every((e) => e.get('host') !== undefined); // all hosts known?
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000],
vec![
Column::required("cpu", vec![0.31, 0.95, 0.48]),
Column::required("host", vec!["a", "b", "a"]),
],
)?;
assert!(series.some(|r| Ok(r.number("cpu")?.unwrap_or(0.0) > 0.9))?); // any spike?
assert!(series.every(|r| Ok(r.string("host")?.is_some()))?); // all hosts known?
Ok(())
}
Subseries selection
These return a new TimeSeries<S> with the same schema and a
subset of events. The source is untouched.
slice(begin?, end?)
Positional, like Array.slice. Negative indices supported.
- TypeScript
- Rust
const head = series.slice(0, 100); // first 100 events
const tail = series.slice(-10); // last 10 events
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
(0..200).map(|i| i * 1_000).collect(),
vec![Column::required("cpu", vec![0.5; 200])],
)?;
let head = series.slice(0..100); // first 100 rows
let tail = series.slice(-10..); // last 10 rows
assert_eq!(head.len(), 100);
assert_eq!(tail.key().begin()[0], 190_000);
Ok(())
}
slice takes a Rust range; negative ends count from the end, as in JavaScript.
tail(duration)
Trailing time window measured from the last event's begin().
"The last 30 seconds":
- TypeScript
- Rust
series.tail('30s'); // TimeSeries<S>
series.tail('5m').reduce({ cpu: 'avg' }); // current state
use pond_rs::{Agg, Column, Reducer, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000, 30_000, 40_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48, 0.62, 0.53])],
)?;
// Rows that start after 40 s - 30 s = 10 s.
let last_30s = series.tail("30s")?;
assert_eq!(last_30s.key().begin(), &[20_000, 30_000, 40_000]);
let now = series
.tail("30s")?
.reduce(&[Agg::of("cpu", Reducer::Avg)])?;
assert_eq!(now.number("cpu")?, Some(0.5433333333333333));
Ok(())
}
tail takes a duration and returns an error for a bad one; there is no form with no argument.
tail() with no argument is the identity.
before(boundary) / after(boundary)
Events strictly before / after a temporal boundary (timestamp or event key):
- TypeScript
- Rust
series.before(Date.now() - 60_000); // events older than 1m
series.after(start); // events newer than `start`
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000, 30_000, 40_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48, 0.62, 0.53])],
)?;
assert_eq!(series.before(20_000).key().begin(), &[0, 10_000]);
assert_eq!(series.after(20_000).key().begin(), &[30_000, 40_000]);
Ok(())
}
overlapping(range) / containedBy(range)
| Method | Keeps events… |
|---|---|
overlapping(range) | whose key intersects the range (any overlap) |
containedBy(range) | whose key is fully inside the range |
within(range) | same as containedBy (alias for ergonomics) |
- TypeScript
- Rust
const overlapping = series.overlapping(yesterdayRange);
const fully = series.containedBy(yesterdayRange);
use pond_rs::{Column, EventKey, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000, 30_000, 40_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48, 0.62, 0.53])],
)?;
let range = EventKey::time_range(10_000, 30_000)?;
let overlapping = series.overlapping(range.clone());
let fully = series.contained_by(range);
let within = series.within(10_000, 30_000)?; // takes the two ends
assert_eq!(overlapping.len(), 3); // both ends are included
assert_eq!(fully.len(), 3); // the same rows here, since rows are moments
assert_eq!(within.len(), 3);
Ok(())
}
within takes the two ends rather than a range. Both ends count as inside, for all three.
trim(range)
Like overlapping(range), but partially-overlapping events get
their keys clipped to the range boundaries. Use this when you
want a subseries that "fits" inside a window without leaking past
either edge:
- TypeScript
- Rust
const clipped = series.trim(reportingWindow);
use pond_rs::{Column, EventKey, TimeRangeKeys, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
// Two jobs, each running from begin to end.
let jobs = TimeSeries::from_time_ranges(
"jobs",
TimeRangeKeys {
begin: vec![0, 20_000],
end: vec![15_000, 50_000],
},
vec![Column::required("id", vec!["a", "b"])],
)?;
let window = EventKey::time_range(10_000, 30_000)?;
let clipped = jobs.trim(window);
assert_eq!(clipped.key().begin(), &[10_000, 20_000]);
assert_eq!(clipped.key().end().unwrap(), &[15_000, 30_000]);
Ok(())
}
Column projection
Where the selectors above narrow the rows, these narrow the
columns — both return a new TimeSeries (with a narrowed schema),
zero-copy, source untouched.
select(...keys)
Keep the key column plus the named value columns; drop the rest. The result type narrows to just those fields:
- TypeScript
- Rust
series.select('cpu', 'healthy'); // TimeSeries with only cpu + healthy
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000],
vec![
Column::required("cpu", vec![0.31, 0.44]),
Column::required("host", vec!["a", "b"]),
Column::required("healthy", vec![true, false]),
],
)?;
let picked = series.select(&["cpu", "healthy"])?;
let names: Vec<&str> = picked.columns().iter().map(|c| c.name()).collect();
assert_eq!(names, ["cpu", "healthy"]);
Ok(())
}
rename(mapping)
Relabel columns:
- TypeScript
- Rust
series.rename({ cpu: 'load' }); // 'cpu' column becomes 'load'
use pond_rs::{Column, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000],
vec![Column::required("cpu", vec![0.31, 0.44])],
)?;
let renamed = series.rename(&[("cpu", "load")])?; // 'cpu' column becomes 'load'
assert_eq!(renamed.column("load")?.numbers()?.get(0), Some(0.31));
assert!(renamed.column("cpu").is_err());
Ok(())
}
Range predicates
These return a boolean or a TimeRange, not a series.
overlaps(other)
Does this series's timeRange() overlap another?
- TypeScript
- Rust
seriesA.overlaps(seriesB); // boolean
seriesA.overlaps(rangeB); // also accepts a TimeRange directly
use pond_rs::{Column, EventKey, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let a = TimeSeries::new(
"a",
vec![0, 20_000, 40_000],
vec![Column::required("v", vec![1.0, 2.0, 3.0])],
)?;
let b = TimeSeries::new(
"b",
vec![30_000, 60_000],
vec![Column::required("v", vec![4.0, 5.0])],
)?;
// These take a span, so pass the other series' span.
let b_range = b.time_range().unwrap();
assert!(a.overlaps(b_range.clone()));
assert!(!a.contains(b_range.clone()));
assert!(a.contains(EventKey::time_range(5_000, 15_000)?));
assert_eq!(
a.intersection(b_range),
Some(EventKey::time_range(30_000, 40_000)?)
);
Ok(())
}
In pond-rs overlaps, contains and intersection take a span, not another series; pass the other series' time_range(). This covers contains and intersection below as well.
contains(other)
Is the other range / series fully inside this one's timeRange()?
day.contains(meeting); // boolean
intersection(other)
The shared TimeRange, or undefined if no overlap:
day.intersection(meeting); // TimeRange | undefined
Iteration
TimeSeries is iterable. for...of walks events in order; spread
yields the underlying array:
- TypeScript
- Rust
for (const event of series) {
console.log(event.begin(), event.get('cpu'));
}
const all = [...series]; // ReadonlyArray<EventForSchema<S>>
use pond_rs::{Column, Reducer, TimeSeries};
fn main() -> Result<(), pond_rs::PondError> {
let series = TimeSeries::new(
"cpu",
vec![0, 10_000, 20_000, 30_000, 40_000],
vec![Column::required("cpu", vec![0.31, 0.44, 0.48, 0.62, 0.53])],
)?;
let mut total = 0.0;
for row in series.rows() {
total += row.number("cpu")?.unwrap_or(0.0);
}
// The same result, one pass over the column.
assert_eq!(series.reduce_number("cpu", Reducer::Sum)?, Some(total));
let plain: f64 = series.column("cpu")?.numbers()?.iter().flatten().sum();
assert_eq!(plain, total);
Ok(())
}
For array methods over the Event rows, reach for series.events. But
for a reduction over a single column, the column API is the one-pass,
validity-aware path — and skips materializing Events entirely:
series.events.reduce((s, e) => s + (e.get('cpu') ?? 0), 0); // row API
series.column('cpu').sum(); // same result, one column pass
Output forms
These are query-flavored too — they read the series and return data
in a different shape, without producing a new TimeSeries.
| Method | Returns |
|---|---|
toJSON({ rowFormat }) | JSON-friendly payload accepted by TimeSeries.fromJSON(...) |
toRows() | Array of normalized row tuples — same shape that new TimeSeries({ rows }) takes |
toObjects() | Array of objects keyed by schema column name |
toPoints() | Wide { ts, ...values }[] rows for charting (ts is event.begin()) |
toArray() | Shallow, mutable copy of the Event array (like events, but not readonly) |
Every form here materializes row / Event objects — they're the
row-API boundary. For a dense typed array of a single field (the canvas
/ WebGL path), stay columnar with
series.column('cpu').toFloat64Array() — see
Reading columns.
In pond-rs these are to_json(RowFormat::Array) (or RowFormat::Object),
to_columns_json(), to_points_json() and to_arrow(). pond-rs has no
toRows, toObjects or toArray; walk rows() instead.
toPoints is the chart-bridge — see
Charting. toJSON and friends are the
serialization story — see Creating series.
See also
- Transformations — once you've found the event(s) you want, the methods that change them.
- Reshaping —
groupBy,pivotByGroup,joinfor when "interrogate" turns into "combine sources". - Cleaning data —
fill,filterfor normalizing what queries return.