Real-time and animated charts#
Drive smooth, continuous motion — a live waveform, a streaming metric — without rebuilding the figure each frame.
How it works#
The managed render path rebuilds the whole figure on each update — right for
occasional data changes, but limited to roughly 30 fps. For continuous motion,
chart.animate(setup, update) is the fast path: it updates artists in place
and redraws only them over a cached background (blitting), sustaining high frame
rates.
import math
xs = [i / 20 for i in range(140)]
def setup(ax):
(line,) = ax.plot([], [])
ax.set_xlim(0, xs[-1]) # FIXED limits — blitting needs stable axes
ax.set_ylim(-1.2, 1.2)
return line # the artist(s) to animate
def update(t, line): # t = elapsed seconds
line.set_data(xs, [math.sin(x - t * 2) for x in xs])
chart = bs.Chart(grow=True)
anim = chart.animate(setup, update, interval=30)
setup runs once to create the artists and set fixed axis limits (blitting
needs stable axes). update runs every frame with the elapsed time in
seconds — drive motion by t and the apparent speed stays constant even
when frame timing jitters. Mutate the artist’s data in place; don’t create new
artists or rescale the axes.
animate returns a handle: anim.stop() / anim.start() pause and resume,
and anim.running reports the state. The animation also pauses itself when
the chart is hidden (a switched-away tab, a minimized window) and resumes when
shown, so off-screen charts on a dashboard cost nothing; it stops when the widget
is destroyed.
Note
For a live data feed, keep a rolling window of recent samples and set the
artist’s data from it in update — the same blitting path, fed by your
incoming data instead of a clock.
Example#
1
2XS = [i / 12 for i in range(180)] # dense sampling → smooth curves
3MID = XS[len(XS) // 2]
4
5
6def signal(x, t):
7 return math.sin(x - t * 2.2)
8
9
10def filtered(x, t):
11 return 0.7 * math.cos(x * 1.3 - t * 1.6)
12
13
14def setup(ax):
15 """Create every artist once, pin the axes, and return them to animate."""
16 (sig_line,) = ax.plot([], [], linewidth=2.5, label="signal")
17 (flt_line,) = ax.plot([], [], linewidth=2.5, label="filtered")
18 (dot,) = ax.plot([], [], "o", markersize=10, markerfacecolor="white",
19 markeredgecolor=sig_line.get_color(), markeredgewidth=2.5)
20 ax.set_xlim(0, XS[-1])
21 ax.set_ylim(-1.3, 1.3)
22 ax.set_xlabel("time")
23 ax.set_ylabel("amplitude")
24 ax.grid(True, alpha=0.25)
25 ax.legend(loc="upper right")
26 return [sig_line, flt_line, dot]
27
28
29def update(t, artists):
30 """Each frame: travel the waves and let the dot ride the signal."""
31 sig_line, flt_line, dot = artists
32 sig_line.set_data(XS, [signal(x, t) for x in XS])
33 flt_line.set_data(XS, [filtered(x, t) for x in XS])
34 dot.set_data([MID], [signal(MID, t)])
35
36
37with bs.App(title="Animated chart", size=(680, 440), padding=16, gap=12) as app:
38 bs.Label("A blitting animation — smooth at high frame rates", font="heading-md")
39 chart = bs.Chart(grow=True, horizontal="stretch")
40 anim = chart.animate(setup, update, interval=20)
41
42 with bs.Row(gap=8):
43 bs.Button("Pause", on_click=anim.stop)
44 bs.Button("Resume", accent="primary", on_click=anim.start)
45
46app.run()
When to use#
Use animate for continuous, high-rate motion (waveforms, live sensors,
progress). For occasional updates — a value changes, a record is added — the
reactive render path in Live and data-driven charts is simpler and enough. The full
animation reference is on the Chart guide.