render_matplotlib

render_matplotlib class

Reactively render a matplotlib figure as an interactive ipympl widget.

Usage Source

render_matplotlib(
    _fn: Optional[ValueFn[ValueT]] = None,
    *,
    width: Optional[str] = None,
    height: Optional[str] = None,
    fill: Optional[bool] = None,
    fillable: Optional[bool] = None,
    rate_policy: Optional[RatePolicy] = None,
    rate_delay: Optional[float] = None,
)

@render_matplotlib is a decorator that creates a render function – a function that returns the figure to display and re-runs when the reactive values it reads change:

import matplotlib.pyplot as plt

from shinywidgets import render_matplotlib


@render_matplotlib
def plot():
    fig, ax = plt.subplots()
    ax.plot([1, 2, 3], [1, 4, 9])
    return fig

The render function may return a matplotlib.figure.Figure, a matplotlib.axes.Axes (rendered via its figure), or an ipympl.backend_nbagg.Canvas widget. Figures and axes are coerced to a Canvas (requires pip install 'shinywidgets[matplotlib]'), so this also works with libraries built on matplotlib (e.g., seaborn or pandas.DataFrame.plot()). A coerced canvas’s header (the “Figure N” title) is hidden by default; a Canvas you construct yourself keeps its own settings (e.g., header_visible=True). In a filling layout, the figure is resized to fit its container. With an explicit height, the figure keeps its figsize unless the output is also fillable=True.

Unlike @render.plot, which draws a static image, the figure stays alive on the server for as long as it is displayed, so it can respond to the user. Like @render.plot, a figure created through pyplot is closed (as by plt.close()) once it’s rendered. Still, prefer creating figures explicitly (e.g., Figure(layout="constrained") or plt.subplots()) over pyplot’s implicit current figure or axes (plt.plot(), plt.gca(), or seaborn functions without ax=), which are shared by every output and session in the process.

Create figures with a non-interactive backend: matplotlib.figure.Figure() directly, or pyplot under the agg backend (the default when no display is available, as on a server; select it locally with matplotlib.use("agg") before creating figures). A figure created under a GUI backend (Qt, Tk, macOS, …) can still be rendered – its window is destroyed and ipympl’s canvas replaces its own – but that path depends on platform-specific behavior, so it is supported on a best-effort basis and warns at render time.

Note

Accessing the rendered widget: the render function gains two read-only attributes: .value, the value it most recently returned, and .widget, the rendered Canvas instance. Widget state can also be read reactively with reactive_read().

Responding to user interaction: the output has methods that read what the user does in the plot, much like input.my_slider() reads an input. Call them in a reactive function (a @render.* function, @reactive.calc, or @reactive.effect), which then re-runs whenever the interaction changes:

  • clicked(): the most recent mouse press on an axes.
  • picked(): the most recent pick of an artist created with picker=.
  • limits(ax=0): an axes’ x/y limits after a zoom or pan.

How they behave:

  • clicked() and picked() return None until the interaction happens. That’s a normal state, not an error, so check for it, or use req(plot.clicked()) to wait for it; they keep their value across re-renders (the payload then refers to the previous render’s figure). limits() waits for the first render, then always holds the axes’ current limits.
  • Each press fires clicked() – a double-click arrives as three presses.
  • limits() updates at most once the user pauses for 250 ms (see rate_policy and rate_delay).
  • They can’t be read inside this output’s own render function, which builds a new figure each time it runs. To change the figure in response, modify .widget from a @reactive.effect instead.

For anything else, matplotlib’s own callbacks (e.g., fig.canvas.mpl_connect("motion_notify_event", f)) still work too, and may set reactive.values.

Mutating the widget: to update the rendered figure without losing the user’s zoom, modify the artists of .widget.figure and call .widget.draw_idle().

Parameters

width : Optional[str] = None

A CSS length unit (e.g., "400px" or "100%") for the output’s width.

height : Optional[str] = None

A CSS length unit for the output’s height. When None (the default), the output is designed to fill the height of its parent container (see fill and fillable).

fill : Optional[bool] = None

Whether the output should grow or shrink to fit its parent container. When None (the default), this is True if height is not specified.

fillable : Optional[bool] = None

Whether the output container should fill available space (scrolling its contents on overflow). When None (the default), this is True if height is not specified.

rate_policy : Optional[RatePolicy] = None

How to rate-limit limits() (debounced by default) and reactive_read() on the widget: "debounce" or "throttle". None (the default) keeps the debounce, and debounces reactive_read() only if rate_delay is given.

rate_delay : Optional[float] = None

The rate-limit window in seconds (default 0.25). 0 disables rate limiting.

Examples

from matplotlib.figure import Figure
from shiny import App, reactive, render, ui
from shinywidgets import output_widget, render_matplotlib

app_ui = ui.page_fluid(
    ui.input_slider("n", "Points", 10, 100, 50),
    output_widget("plot"),
    ui.output_text("visible"),
)


def server(input, output, session):
    xlim = reactive.value((0.0, 0.0))

    @render_matplotlib
    def plot():
        fig = Figure(layout="constrained")
        ax = fig.subplots()
        ax.plot(range(input.n()))
        ax.callbacks.connect("xlim_changed", lambda ax: xlim.set(ax.get_xlim()))
        return fig

    @render.text
    def visible():
        lo, hi = xlim()
        return f"Showing x from {lo:.1f} to {hi:.1f}"


app = App(app_ui, server)

Each accessor can also be read by name – out.read("clicked") – which also accepts the widget’s bare synced-trait names.

See also

output_widget(): in Shiny Core, the UI needs this to hold the output (Shiny Express displays it where the render function is defined).

Methods

clicked() The most recent mouse press on an axes, as a matplotlib.backend_bases.MouseEvent (.xdata/.ydata in the data coordinates of the axes under the press, .inaxes that axes).
limits() The x and y limits of axes ax (an index into .widget.figure.axes), as ((x0, x1), (y0, y1)).
picked() The most recent pick of an artist, as a matplotlib.backend_bases.PickEvent (.artist the picked artist, .ind the picked point indices). Only artists created with picker= (e.g., ax.plot(x, y, picker=5)) are pickable.

clicked()

render_matplotlib.clicked()

The most recent mouse press on an axes, as a matplotlib.backend_bases.MouseEvent (.xdata/.ydata in the data coordinates of the axes under the press, .inaxes that axes).

Returns None until the user clicks, and keeps its value across re-renders (the payload then refers to the previous render’s figure), so use req(plot.clicked()) to wait for a click. Presses that start a toolbar pan/zoom drag, or land outside any axes, don’t count. Every press fires – a double-click arrives as three presses. Not rate-limited.

limits()

render_matplotlib.limits(ax: int = 0)

The x and y limits of axes ax (an index into .widget.figure.axes), as ((x0, x1), (y0, y1)).

Seeded from the figure’s current limits at render and updated after every zoom or pan (including ones made in Python). Reading it before the first render cancels silently (like req(False)); it never returns None after that. Debounced (250 ms) by default; see the rate_policy and rate_delay parameters. Raises IndexError when the figure has no axes ax.

picked()

render_matplotlib.picked()

The most recent pick of an artist, as a matplotlib.backend_bases.PickEvent (.artist the picked artist, .ind the picked point indices). Only artists created with picker= (e.g., ax.plot(x, y, picker=5)) are pickable.

Returns None until the user picks, and keeps its value across re-renders (the payload then refers to the previous render’s figure). Every pick fires. Not rate-limited.