bootstack.Chart#

class bootstack.Chart(figure=None, *, render=None, signal=None, data_source=None, debounce=0, toolbar=False, themed=True, seaborn_desat=0.75, parent=None, **kwargs)#

Bases: PublicWidgetBase

Embed a matplotlib figure in a bootstack app, themed to match.

Chart is the bridge between bootstack and the scientific Python plotting stack. You build a plot with matplotlib (or seaborn, which draws onto the same axes) and hand the figure to a Chart; it embeds the figure as a first-class widget that fills its space and recolors its chrome — figure and axes backgrounds, spines, ticks, and text — to match the active theme, flipping with light/dark like everything else in the app.

Chart is deliberately not a plotting API: it does not wrap matplotlib’s drawing calls. You keep the full expressive power of matplotlib/seaborn and bootstack owns only the embedding, theming, and redraw.

matplotlib is an optional dependency. Install it with the visualization extra: pip install bootstack[viz] (or bootstack[viz-seaborn] to add seaborn). Constructing a Chart without matplotlib installed raises a bootstack.errors.BootstackError explaining how to install it.

Build the figure with matplotlib’s object API (matplotlib.figure.Figure) rather than pyplot — an embedded figure must be a standalone object, and pyplot would also open a separate window.

There are two ways to use it:

  • Figure host — pass a figure you built. The chart embeds it and recolors its chrome to the theme. You own the figure; theming the data series is up to you.

  • Managed render — pass a render callback and a reactive source: one or more signal objects, or a data_source. The chart owns the figure and redraws for you: each redraw clears the axes, applies the theme as matplotlib settings — including a semantic accent color cycle so multiple series are on-brand — then calls your render. It re-renders when the theme changes or the bound signal / data source updates:

    count = bs.Signal(20)
    def render(ax, n):
        ax.plot(range(n), [i * i for i in range(n)])
    bs.Chart(render=render, signal=count)   # redraws when `count` changes
    

    With data_source=, render receives the source’s records (a list of dicts) and the chart re-renders whenever the source changes — so a chart and a DataTable can share one source and stay in sync.

Parameters:
  • figure (Any) – The matplotlib Figure to display (figure-host mode). Omit to start empty, or when using render.

  • render (Callable | None) – A drawing callback for managed mode, called as render(ax) or render(ax, data) where data is the bound signal value(s). The chart clears and re-themes the axes before each call, so just draw.

  • signal (Any) – A bootstack.Signal (or a list of them) whose value is passed to render as data; the chart re-renders when it changes. Requires render. Mutually exclusive with data_source.

  • data_source (Any) – A data source (any bootstack.data source) to plot. Its records are passed to render as data (a list of dicts), and the chart re-renders whenever the source changes — so a chart and a DataTable can share one source and stay in sync. Reads all rows matching the source’s current where/order, so filter or sort the source to scope or shape what is plotted. Requires render; mutually exclusive with signal.

  • debounce (int) – Milliseconds to coalesce rapid signal changes before re-rendering. 0 (default) re-renders on every change.

  • toolbar (bool | list[str]) – Show a themed navigation toolbar above the chart. True includes all standard tools — home, back, forward, pan, zoom, save; pass a list to choose a subset in order (for example ["pan", "zoom", "save"]), or [] for an empty bar you fill yourself. Defaults to False (no toolbar). The toolbar drives matplotlib’s own navigation, so pan and zoom work on any embedded figure; save routes through bootstack’s file dialog. Add your own buttons through the toolbar property.

  • themed (bool) – Whether bootstack colors your data series. True (default) applies the semantic accent color cycle (and seaborn palette) so multiple series are on-brand. Pass False to keep your own series colors — the chart’s chrome (background, axes, text, ticks, grid) still tracks the theme so it fits the app, but the accent cycle and seaborn seeding are not imposed, leaving data colors to you (or to your own matplotlib style). Per-series colors you set explicitly always win regardless of this flag.

  • seaborn_desat (float) – How much to soften the accent colors when seeding seaborn’s palette, in [0, 1] (seaborn’s own desat). Defaults to 0.75 — seaborn plots are usually area-filled, where the full accent saturation reads heavy, so the seeded palette is muted to suit its aesthetic while staying on-brand. 1.0 keeps the accents fully saturated. Only affects seaborn (the matplotlib line cycle stays vivid) and only when themed=True.

  • parent (Any) – Explicit parent widget. If omitted, the current context-stack container is used.

  • **kwargs (Any) – Layout placement options applied by the parent container (e.g. grow, horizontal, row, column), plus container styling such as surface and padding. See Arranging Widgets.

property ax: Any#

The figure’s primary Axes, creating one if the figure is empty.

A convenience for the common single-plot case:

chart = bs.Chart()
chart.ax.plot([1, 2, 3])
chart.draw()
property figure: Any#

The embedded matplotlib Figure.

Assign a new Figure to swap what is displayed; the old canvas is torn down and the new figure is themed and drawn.

property is_attached: bool#

Whether the widget is currently placed in its layout.

True while the widget occupies space in its parent; False after detach (or before it has ever been placed). A detached widget keeps its state and can be returned to the layout with attach.

property schedule: Schedule#

Scheduler tied to this widget’s lifetime.

All jobs are automatically cancelled when the widget is destroyed. First access creates the Schedule instance; subsequent accesses return the same instance.

Usage:

self.schedule.delay(500, callback)
self.schedule.every(1000, tick)
job = self.schedule.idle(refresh)
job.cancel()
property toolbar: Any#

The navigation toolbar, for adding your own buttons.

Available only when the chart was created with toolbar=True. Returns a bootstack.Toolbar whose built-in navigation buttons (home, back, forward, pan, zoom, save) sit on the left and the coordinate readout on the right; anything you add lands beside the built-in tools:

chart = bs.Chart(render=render, signal=sig, toolbar=True)
chart.toolbar.add_divider()
chart.toolbar.add_button(icon="arrow-clockwise", on_click=refresh)
chart.toolbar.add_widget(bs.ThemeToggle)

It is the full Toolbar surface — add_button, add_divider, add_widget, and the rest all work.

Raises:

BootstackError – if the chart was created without toolbar=True.

animate(setup, update, *, interval=30, frames=None)#

Drive a smooth, high-performance animation via blitting.

Unlike the managed render= path (which rebuilds the whole figure on each update — right for occasional data changes, but limited to ~30 fps), animation updates artists in place and redraws only them over a cached background, sustaining high frame rates.

Parameters:
  • setup (Callable) – Called once with the themed Axes. Create your artists with initial or empty data and set FIXED axis limits — blitting needs stable axes — then return the artist, or a list of artists, to animate.

  • update (Callable) – Called every frame as update(value, artist) (or update(value)), where artist is whatever setup returned. For a continuous animation (the default) value is the elapsed time in seconds since the animation started — drive motion by this and the apparent speed stays constant even when frame timing jitters. If you pass frames, value is the current frame value instead. Mutate the artist’s data in place; do not create new artists or rescale the axes.

  • interval (int) – Target milliseconds between frames. Default 30 (~33 fps).

  • frames (Any) – An iterable of frame values to step through fixed data, or None (default) for a continuous, time-driven animation.

Returns:

A ChartAnimation handle — call stop() / start() to control it. The animation stops automatically when the widget is destroyed.

Return type:

ChartAnimation

attach(**kwargs)#

Return a detached widget to its layout, optionally moving it.

With no arguments, restores the widget to exactly where detach took it from. Any layout kwargs accepted by the original placement (e.g. fill, expand, anchor, sticky, margin) override the stored options. For stacked widgets, index= sets the position among the currently attached siblings (or pass an explicit before=/after= sibling); without one, the snapshotted position is used.

Calling attach on a widget that is already attached moves it (the kwargs are re-applied). Fires on_attach.

Parameters:

**kwargs (Any) – Layout placement options to override for this placement.

Raises:

ParentResolutionError – If the widget was never placed in a layout.

destroy()#

Destroy the widget and release the resources it holds.

Removes the widget from its parent, destroys its children, and cancels any pending or repeating jobs on its schedule. After this the widget must not be used again. Destroying a container destroys everything inside it.

detach()#

Remove the widget from its layout without destroying it.

The widget stops occupying space but keeps its state, children, and event bindings, ready to be returned with attach. The current position is snapshotted so a plain attach() restores it exactly — for stacked siblings this is the index among the currently attached siblings, so detaching other siblings first shifts that index.

Calling detach on a widget that is already detached, or one that was never placed in a layout, does nothing. Fires on_detach.

draw()#

Redraw the figure after mutating it in place.

Call this after drawing onto ax (or the figure) so the change appears. Theme changes redraw automatically — this is for your own updates.

emit(event, *, data=None)#

Fire a named event on this widget, as if it produced the event itself.

This is how a composite widget surfaces high-level activity to its listeners, and the generic counterpart to the on_*() shorthands for firing events that have no dedicated method.

Parameters:
  • event (str) – The event name, unprefixed — the same name you pass to on() or an on_<event>() shorthand (e.g. 'change', 'select').

  • data (Any) – The payload delivered to handlers. For a data-carrying event, pass the matching payload dataclass from bootstack.events — the same object an on_<event>() handler receives. Leave as None for native events (click, hover, focus, …), which carry no payload.

Example

widget.emit("change", data=bs.events.ChangeEvent(value=new_value))
on(event, handler=None)#

Bind handler to event, or return a composable Stream.

With a handler — binds immediately and returns a Subscription:

sub = widget.on("change", handler)
sub.cancel()

Without a handler — returns a Stream for operator chaining. The Tk binding is created lazily when .listen() is called:

sub = widget.on("change").debounce(300).listen(handler)
sub.cancel()
Parameters:
  • event (str) – Event name (e.g. "change", "click").

  • handler (Callable[[Any], Any] | None) – Optional callback. If omitted, a Stream is returned.

Returns:

Subscription when a handler is provided; Stream otherwise.

Return type:

Stream | Subscription

on_attach(handler=None)#

Register a callback fired when the widget enters the layout.

Fires each time the widget becomes visible in its parent — on initial placement and on every attach. Pair it with on_detach to keep per-visibility resources (timers, observers) tied to the widget’s presence on screen. The handler receives a curated Event.

Parameters:

handler (Callable[[Event], Any] | None) – Called when the widget is attached. Omit to get a composable Stream.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_destroy(handler=None)#

Register a callback fired when the widget is destroyed.

Fires once, as the widget is torn down — the place to release resources the widget owns that aren’t cleaned up automatically (file handles, observers, external subscriptions). The handler receives a curated Event.

Parameters:

handler (Callable[[Event], Any] | None) – Called as the widget is destroyed. Omit to get a composable Stream.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_detach(handler=None)#

Register a callback fired when the widget leaves the layout.

Fires each time the widget stops occupying space in its parent — on detach and when an ancestor hides it. Pair it with on_attach to release per-visibility resources. The handler receives a curated Event.

Parameters:

handler (Callable[[Event], Any] | None) – Called when the widget is detached. Omit to get a composable Stream.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription