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.
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
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):
const r = series.timeRange(); // TimeRange | undefined
r?.begin(); // number (epoch ms)
r?.end();
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).
series.at(0); // first event, or undefined
series.at(-1); // last event, same as series.last()
first() / last()
series.first(); // EventForSchema<S> | undefined
series.last(); // EventForSchema<S> | undefined
find(predicate)
First event matching a predicate, like Array.prototype.find.
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:
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
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":
const previous = series.atOrBefore(Date.now());
const next = series.atOrAfter(Date.now());
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:
series.some((e) => (e.get('cpu') ?? 0) > 0.9); // any spike?
series.every((e) => e.get('host') !== undefined); // all hosts known?
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.
const head = series.slice(0, 100); // first 100 events
const tail = series.slice(-10); // last 10 events
tail(duration)
Trailing time window measured from the last event's begin().
"The last 30 seconds":
series.tail('30s'); // TimeSeries<S>
series.tail('5m').reduce({ cpu: 'avg' }); // current state
tail() with no argument is the identity.
before(boundary) / after(boundary)
Events strictly before / after a temporal boundary (timestamp or event key):
series.before(Date.now() - 60_000); // events older than 1m
series.after(start); // events newer than `start`
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) |
const overlapping = series.overlapping(yesterdayRange);
const fully = series.containedBy(yesterdayRange);
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:
const clipped = series.trim(reportingWindow);
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:
series.select('cpu', 'healthy'); // TimeSeries with only cpu + healthy
rename(mapping)
Relabel columns:
series.rename({ cpu: 'load' }); // 'cpu' column becomes 'load'
Range predicates
These return a boolean or a TimeRange, not a series.
overlaps(other)
Does this series's timeRange() overlap another?
seriesA.overlaps(seriesB); // boolean
seriesA.overlaps(rangeB); // also accepts a TimeRange directly
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:
for (const event of series) {
console.log(event.begin(), event.get('cpu'));
}
const all = [...series]; // ReadonlyArray<EventForSchema<S>>
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.
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.