Skip to content

Data and streaming

Derived-series filters API ​

The calls

movingAverage(values: ArrayLike<number>, window: number): Float32Array

The mean of each point and the window - 1 points before it.

The usual way to see the shape of a noisy signal. The first window - 1 points are NaN: a trailing average genuinely has no value before it has a window to average, and reporting the partial mean there would draw a curve that settles for reasons that are about the filter rather than the data.

Computed as a running sum rather than a sum per point, so the cost is the length of the series and not the length times the window.

difference(values: ArrayLike<number>, lag?: number): Float32Array

The difference from lag points earlier, in the series' own units.

What percentChange is when the quantity has a meaningful zero or can cross it: a temperature that goes from -1 to 1 has changed by two degrees and by no sensible percentage.

percentChange(values: ArrayLike<number>, lag?: number): Float32Array

The change from lag points earlier, as a percentage of that earlier value.

The first lag points are NaN, and so is any point whose earlier value was zero: a change from nothing is not a percentage of anything, and reporting it as a very large number would put a spike on the chart that says more about the divisor than about the data.

derive(values: ArrayLike<number>, f: (value: number, index: number) => number): Float32Array

A filter of the caller's own, applied point by point.

Here so that writing one does not mean writing the typed-array plumbing around it. f is handed each value and its index and returns the derived value, or NaN where it has nothing to say.

calls/derived-series-filters-api
const raw = Float32Array.from({ length: 800 }, (_, i) => 12 + 3 * signal(i));
const derived = {
  // The mean of each point and the 39 before it.
  movingAverage: () => movingAverage(raw, 40),
  // The change over ten points, in the series' own units.
  difference: () => difference(raw, 10),
  // The same change as a percentage of the earlier value.
  percentChange: () => percentChange(raw, 10),
  // A filter of the page's own: degrees Celsius to Fahrenheit.
  derive: () => derive(raw, (celsius) => (celsius * 9) / 5 + 32),
}[filter]();

const x = plot.uploadBuffer(Float64Array.from(raw, (_, i) => i));
plot.setCharts({
  charts: [
    { series: [{ x, y: plot.uploadBuffer(raw), label: "raw" }] },
    { series: [{ x, y: plot.uploadBuffer(derived), label: filter }] },
  ],
  grid: { columns: 1 },
});

Also called

2 in the code above, in the order it makes them

plot.uploadBuffer(values: Float32Array | Float64Array): BufferHandle

Upload a block of scalars and return a handle naming it.

A buffer keeps the precision it arrived in. Use Float64Array for time and for anything where f32's twenty-four bits of mantissa would lose something real: a millisecond timestamp narrowed to f32 rounds every sample into a five-minute bucket, and it does that silently.

plot.setCharts(document: ChartDocument): readonly ChartHandle[]

Draw a document of charts, and get a handle for each one.

The handles come back in document order and are what names a chart afterwards: chartView, setChartView, chartPicked, and the chart on a hover, a hit or any other event. A page linking two charts holds two handles rather than allocating ids.

Replaces the charts and the series each one reads.

Throws rather than drawing a partial document, and a throw leaves the previous document in place. What it refuses: a misspelled key, a handle that names something other than a buffer, a range whose ends are the wrong way round, and a column on a chart kind that cannot read it. All four would otherwise be a chart that draws something subtly wrong, or nothing, with nothing said.

Setting a document again is the way to change one: there is no per-chart edit, and the compile is cheap because the data does not move. The buffers are named by handle, so a live chart writes to them with writeBuffer and never sets a document at all.