Skip to content

Zoom and pan

Linked / synchronised charts ​

The calls

plot.setChartView(id: ChartHandle, x: readonly [number, number] | null, y: readonly [number, number] | null): void

Show a range on chart id. null hands that axis back to the document.

Zoom and pan need none of this -- the plot drives them from the pointer. This is for what only a page can decide:

Throws on a range whose low end is not below its high end, rather than quietly swapping them: a reversed range usually means two variables the wrong way round, and silently fixing it hides that.

plot.chartView(id: ChartHandle): { x: [number, number] | null; y: [number, number] | null; }

What chart id is showing, as [lo, hi] per axis.

null on an axis the document still decides, which is what a chart nobody has zoomed reports for both. That is not "showing nothing": an auto axis goes on fitting its data every frame.

id is the handle setCharts returned for the chart.

plot.resetChartView(id: ChartHandle): void

Hand chart id's axes back to the document.

The programmatic form of the gesture a viewer already has: a double click anywhere on a chart does this, and does it even when the data has been left off screen, which is the case it exists for. A page needs this only for a control of its own, or to reset a chart the pointer is not over.

Back to the document rather than to a remembered range. An axis the document leaves alone goes on fitting its data, so a live chart resumes following it instead of freezing at whatever it showed when the view was taken.

calls/linked-synchronised-charts
const count = 1500;
const x = plot.uploadBuffer(Float64Array.from({ length: count }, (_, i) => i / 10));
const column = (f) => plot.uploadBuffer(Float32Array.from({ length: count }, (_, i) => f(i / 10)));
const pressure = column((t) => 4 + Math.sin(t / 6) + 0.3 * Math.sin(t * 1.3));
const flow = column((t) => 2 + Math.cos(t / 6) * Math.exp(-t / 120));
// A handle per chart, in the order the document lists them.
const [upper, lower] = plot.setCharts({
  charts: [
    { series: [{ x, y: pressure, label: "pressure" }], title: "Drag me" },
    { series: [{ x, y: flow, label: "flow" }], title: "I follow" },
  ],
  grid: { columns: 1 },
});
return {
  // What `plot.events()` returned for a frame.
  events(list) {
    for (const event of list) {
      if (event.type !== "viewChanged" || event.chart !== upper) continue;
      // Null means the upper chart went back to fitting its data, and
      // then so does the lower one.
      if (event.x === null) plot.resetChartView(lower);
      else plot.setChartView(lower, event.x, null);
    }
  },
  pointer() {
    const view = plot.chartView(lower);
    return view.x === null ? "lower chart: fitting its data" : `lower chart: x from ${fixed(view.x[0])} to ${fixed(view.x[1])}`;
  },
};

Also called

3 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.

plot.events(): PlotEvent[]

What the charts reported on the last frame.

Draining: a frame's events are handed over once. Calling this every frame reads what just happened; not calling it accumulates nothing, because each frame replaces the set.