Charts
Concepts

State and draw

Handlers compute once per event and draw only reads. This split keeps a script fast.

A script splits its work in two:

  • Handlers (on.<stream>) run once per event. They do the heavy work and write the results into state.
  • draw runs every frame. It reads state and emits shapes. It never changes state.
state: () => ({ points: [] as { time: number; value: number }[] }),
on: {
  bars: (s, { bar }) => {
    s.points.push({ time: bar.time, value: expensive(bar) }); // once per bar
  },
},
draw: (s, frame, g) => {
  for (const p of s.points) {
    // cheap: position and emit
  }
},

Why

draw runs far more often than data arrives: on every pan, zoom, resize and crosshair move, up to 60 times a second. Work in draw repeats every frame, work in a handler happens once.

When draw runs

  • Only inside the chart's frame. Handlers and events never draw.
  • After handlers, events and widget callbacks the runtime asks for one redraw. Many events in one frame cost one draw.
  • The chart also redraws without new data. draw must give the same picture from the same state.
  • For animation, call ctx.requestRedraw() from draw and use frame.now.

State

  • state() creates the object fresh on every start. Everything in it is yours: arrays, maps, class instances.
  • Handlers mutate it in place. There is no need to return a new object.
  • draw, legend, widgets and events get the same object.
  • Nothing survives a restart. Saved settings are inputs, not state.

Caching expensive results for draw

Some results are only needed for what is in view, e.g. a profile per session. Build them lazily in draw and cache them in state with a version number, as the TPO does:

if (entry.built?.version !== entry.version) {
  entry.built = { version: entry.version, profile: tpo.build(/* … */) };
}

This is the one exception to "draw only reads": the cache is derived data, never input for handlers.

More: Performance.

On this page