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 intostate. drawruns every frame. It readsstateand 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.
drawmust give the same picture from the same state. - For animation, call
ctx.requestRedraw()fromdrawand useframe.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,widgetsand 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.