Skip to main content

Temporal relations

How an event's temporal key relates to a query range determines which selection operator you reach for. Pond names three distinct relations — within, overlapping, trim — to force the choice explicit. Picking the wrong one is silent data corruption; picking the right one is the difference between a chart that shows clean edges and one that leaks data past the visible range.

The three relations

Given a query range = [start, end) and an event with key extent [event.begin(), event.end()):

RelationConditionBehaviour on straddling events
withinevent.begin() ≥ range.start AND event.end() ≤ range.endDrop events whose extent crosses a boundary
overlappingevent.begin() < range.end AND event.end() > range.startKeep straddling events with original extents
trimSame as overlapping, then clip the event's extent to rangeKeep straddling events with clipped extents

Five test events (a–e) against a query range from 02:00 to 05:00:
within keeps b and d; overlapping keeps b, c, d, e; trim keeps b
unchanged, clips c to 02:00–02:30, leaves d unchanged, and clips e
to 04:30–05:00

import { TimeRange } from 'pond-ts';

const range = new TimeRange({
start: Date.parse('2025-01-01T00:02:00Z'),
end: Date.parse('2025-01-01T00:05:00Z'),
});

cpu.within(range); // keep events fully contained
cpu.overlapping(range); // keep events that intersect
cpu.trim(range); // intersect and clip key extents

When to use which

Use within when…Use overlapping when…Use trim when…
Running analytics over a closed windowCounting "any activity in window" presenceRendering a chart limited to a visible range
Sums / averages must not double-countStraddling events are "in" the conceptual setStraddling event extents must not leak past edges
Bucket integrity matters more than coverageCoverage matters more than exactnessOutput is visualized, not aggregated

Trim and value semantics

trim clips the key extent but currently passes value columns through unchanged. A 5-minute event clipped to 30 seconds keeps its original numeric values — there's no proportional-contribution weighting (e.g. clippedDuration / originalDuration). This is a deliberate default; proportional contribution is queued as a future option on trim if a use case earns it. For now, post-process clipped events explicitly if you need duration-weighted values.

Open vs closed range semantics

All three relations use half-open ranges[start, end):

  • An event whose key timestamp equals range.start is included (left-closed boundary).
  • An event whose key timestamp equals range.end is excluded (right-open boundary).
  • A TimeRange event with event.end === range.start is not considered overlapping — its end touches but doesn't cross.

This is consistent with Sequence boundaries (also half-open) and matches the dominant convention in time-series literature. The half-open shape matters when chaining selections: within(rangeA) followed by within(rangeB) where rangeB.start === rangeA.end is guaranteed to retain no overlap and lose no events at the seam.

Pointwise selections

Operations that return a single event or a fixed-count slice — they preserve the event grid (no clipping, no aggregation), just narrow it positionally:

cpu.first(); // first event by key order
cpu.last(); // last event by key order
cpu.at(0); // event at index 0
cpu.at(-1); // last event (negative index from end)
cpu.head(10); // first 10 events
cpu.tail(10); // last 10 events
cpu.tail('1m'); // events whose begin > lastEvent.begin - 60s

tail is the temporal counterpart to Array.slice(-n): it accepts either a count or a duration. The duration form is what you reach for when the question is "what happened in the last N seconds."

Pointwise vs range-relations

OperationTypePreserves event keys?Returns
first / last / at / at(-1)PointwiseYesOne event or undefined
head(n) / tail(n)Pointwise (count)YesNew series, n events
tail(duration)Pointwise (time)YesNew series
within(range)Range relationYesNew series
overlapping(range)Range relationYesNew series
trim(range)Range relationNo (clips extents)New series

Pointwise selections never modify event keys. Range relations except trim also preserve keys. Only trim produces new events with new keys — that's the operator's whole job.

Where this shows up

  • Slicing for chart renderingseries.trim(visibleRange) is the right call when the rendered data shouldn't leak past the visible-axis edges.
  • Closed analytics windowsseries.within(window).reduce(...) guarantees no straddling-event contamination.
  • "Has any activity" checksseries.overlapping(range).length > 0 is the cheapest "did something happen in this range" predicate.
  • Live equivalentsLiveView.window('1m') is the streaming analogue of tail('1m'). See Series.
  • Temporal keys — all three relations work uniformly on Time, TimeRange, and Interval keys via begin() / end(). See Temporal keys.