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.