Most front-end streaming charts buffer raw records and redraw them — so cost grows with the volume of the stream, and a high-rate source eventually swamps the main thread. Semiotic's streaming-aggregation layer inverts that: it aggregates on ingest into bounded, event-time windows, so per-frame work scales with the resolutionof the view, not the arrival rate. It is, in effect, the Kafka Streams windowed-aggregation model running client-side.
Three opt-in capabilities make up the layer — windowed aggregation, event-time ingestion, and banded staleness — each additive on top of the existing realtime charts. Everything below is live.
Windowed aggregation
Set aggregate on a realtime chart and pushed events are reduced into tumbling, hopping, orsession windows keyed on the datum's own time field. Each window holds a full online statistics object (count, mean, variance, min, max), so one configuration can draw the mean / sum / min / max / count line plus a ±σ or min–max band — without re-scanning a buffer. The demo below is a firehose: it pushes 15 events every 80 ms, yet the chart only ever draws a handful of window marks.
statbandwindow
0 events ingested → 0 windows drawn· per-frame work is O(windows), not O(events)
Watch the readout. Tens of thousands of events collapse to a couple of dozen window marks — and because retain caps the live window count, memory stays flat for an unbounded stream. That single mechanism is the answer to four things people usually treat separately: bounded memory, level-of-detail downsampling, backpressure, and windowing semantics.
JSX
<RealtimeLineChart ref={chartRef} timeAccessor="t" valueAccessor="v" aggregate={{ window: "hopping", // "tumbling" | "hopping" | "session" size: "1m", // ms or a duration string ("10s", "1m30s") hop: "10s", // hopping only stat: "mean", // mean | sum | min | max | count band: "stddev", // "stddev" | "minmax" | "none" → ±σ envelope retain: 60, // keep the 60 most-recent windows (bounded memory) }} />
Naming note. This is the aggregation window — the interval events are reduced over. It is unrelated towindowMode ("sliding" | "growing"), which is the ring-buffer's eviction policy. Different concept, deliberately different word.
Event-time ingestion
A jittery or merged multi-source stream arrives out of order, and a chart that appends in arrival order draws a zigzag. Turn oneventTime and Semiotic buffers pushed events for a boundedlateness / grace window, releasing them in event-time order. Records older than watermark − lateness arelate: dropped or kept per policy, and always counted and surfaced through onObservation so lateness is observable rather than silent. Toggle it off to see the zigzag return.
event-timelate policy
reordering within a 2s grace window · 0 late records dropped & counted
JSX
<RealtimeLineChart ref={chartRef} timeAccessor="t" valueAccessor="v" eventTime={{ lateness: "2s", latePolicy: "drop" }} // "drop" | "keep" onObservation={(o) => { if (o.type === "late-data") { console.warn("late record", o.eventTime, "watermark", o.watermark, "total", o.lateCount) } }} />
Banded staleness
Liveness usually flips binary — live, then suddenly stale. Banded staleness instead dims the chart progressively through fresh → aging → stale → expired as wall-clock idle time crosses multiples of a threshold, sharing one schedule with per-datum decay and annotation freshness. Pause the stream and watch it age.
current band:fresh
Pause the stream: the chart dims through fresh → aging → stale → expired as idle time crosses 1× / 1.5× / 3× the 2s threshold.
JSX
<RealtimeLineChart ref={chartRef} staleness={{ graded: true, threshold: 5000 }} // graded: true → fresh < 1×, aging < 1.5×, stale < 3×, expired ≥ 3× threshold // override band thresholds/opacities: // staleness={{ graded: { thresholds: { stale: 4 }, opacities: { aging: 0.8 } } }} />
The primitives underneath
The chart props are sugar over pure, separately-importable modules fromsemiotic/realtime. Use them directly to roll your own streaming statistics:
RunningStats — Welford online mean/variance/min/max with an O(1) parallel merge (the operation that rolls fine windows up into coarse ones).WindowAccumulator — tumbling/hopping/session windows over event-time, a RunningStats per window.ReorderBuffer — the bounded out-of-order / lateness buffer.parseWindowDuration — "1m30s" → ms.
JS
import { RunningStats, WindowAccumulator } from "semiotic/realtime" const acc = new WindowAccumulator({ window: "tumbling", size: 60_000 }) acc.push(eventTimeMs, value) // O(1) amortized, any arrival order const windows = acc.emit() // [{ start, end, count, mean, stddev, min, max, partial }]