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()):
| Relation | Condition | Behaviour on straddling events |
|---|---|---|
| within | event.begin() ≥ range.start AND event.end() ≤ range.end | Drop events whose extent crosses a boundary |
| overlapping | event.begin() < range.end AND event.end() > range.start | Keep straddling events with original extents |
| trim | Same as overlapping, then clip the event's extent to range | Keep straddling events with clipped extents |

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 window | Counting "any activity in window" presence | Rendering a chart limited to a visible range |
| Sums / averages must not double-count | Straddling events are "in" the conceptual set | Straddling event extents must not leak past edges |
| Bucket integrity matters more than coverage | Coverage matters more than exactness | Output 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.startis included (left-closed boundary). - An event whose key timestamp equals
range.endis excluded (right-open boundary). - A
TimeRangeevent withevent.end === range.startis 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
| Operation | Type | Preserves event keys? | Returns |
|---|---|---|---|
first / last / at / at(-1) | Pointwise | Yes | One event or undefined |
head(n) / tail(n) | Pointwise (count) | Yes | New series, n events |
tail(duration) | Pointwise (time) | Yes | New series |
within(range) | Range relation | Yes | New series |
overlapping(range) | Range relation | Yes | New series |
trim(range) | Range relation | No (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 rendering —
series.trim(visibleRange)is the right call when the rendered data shouldn't leak past the visible-axis edges. - Closed analytics windows —
series.within(window).reduce(...)guarantees no straddling-event contamination. - "Has any activity" checks —
series.overlapping(range).length > 0is the cheapest "did something happen in this range" predicate. - Live equivalents —
LiveView.window('1m')is the streaming analogue oftail('1m'). See Series. - Temporal keys — all three relations work uniformly on
Time,TimeRange, andIntervalkeys viabegin()/end(). See Temporal keys.