render_plotly
render_plotly class
Reactively render a plotly figure.
Usage Source
render_plotly(
_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_plotly 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 plotly.graph_objects as go
from shinywidgets import render_plotly
@render_plotly
def plot():
return go.Figure(go.Scatter(x=[1, 2, 3], y=[1, 4, 9]))The render function may return either a plotly.graph_objects.Figure (which is coerced to a FigureWidget) or a plotly.graph_objects.FigureWidget widget (requires pip install 'shinywidgets[plotly]'). Alternatively, it may return a Dash-style (figure, config) tuple, where config is a dict of Plotly.js config options (e.g., {"displayModeBar": False}), mirroring Dash’s dcc.Graph(figure=..., config=...) API. The config is merged over plotly’s defaults, so it also takes precedence over shinywidgets’ own defaults (e.g., responsive).
Note
Accessing the rendered widget: the render function gains two read-only attributes: .value, which is exactly what it returned (a figure or the (figure, config) tuple), and .widget, which is always the coerced FigureWidget (with config merged into its _config trait). When the render function returns a go.Figure, .value is that figure, not the displayed one, so modifying it has no effect.
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 clicked points.hover(): the points under the mouse.selected(): the points of a box or lasso selection, once the drag ends (selecting(): the same, while the user drags).brushed(): the selected region itself (brushing(): while dragging).relayout(): the axis ranges after a zoom, pan, or autoscale.double_clicked(): a pulse on every double-click (e.g. a view reset).legend_clicked(): the traces of the most recent legend click.visible_traces(): the current visibility of every trace, seeded from the displayed figure.selected_rows(df): the rows of your data the user selected. This needs each point to carry its row ascustomdata, e.g.px.scatter(df, ..., custom_data=[df.index]); see the method for details.
How they behave:
- Each returns
Noneuntil the interaction happens. That’s a normal state, not an error, so check for it (req(pts is not None); notreq(pts), since a data frame has no truth value). - A re-render (e.g. when the render function reads an input) gives the user a new plot, but every accessor keeps its value – the last interaction still happened.
selected(),brushed(), andvisible_traces()also follow the displayed plot: the new plot re-seeds them at attach, so they track what the user sees (initially nothing selected, so the selection accessors becomeNoneonce it reports).clicked(),hover(),selecting(),brushing(),relayout(),double_clicked(), andlegend_clicked()hold the old plot’s last interaction until a new one happens on the new plot. - Repeating an interaction (clicking the same point twice) isn’t a change, so readers don’t re-run.
- A double-click on the plot background clears
clicked(), and a deselect (double-clicking after a selection) clears the selection accessors. A double-click on a marker re-fires the click after the clear, so it leavesclicked()unchanged. Withdragmode="select"or"lasso", a plain click also resetsselected()andbrushed()(plotly.js reports it as a zero-area selection). - The streaming ones,
hover(),selecting(), andbrushing(), update at most once the user pauses for 250 ms (seerate_policyandrate_delay). - They can’t be read inside this output’s own render function, which builds a new plot each time it runs. To change the plot in response to a click, modify
.widgetfrom a@reactive.effectinstead (see the example).
Mutating the widget: trait changes on .widget are synced to the browser. Re-rendering rebuilds the figure, resetting zoom and selection. To change a displayed figure instead, read inputs under reactive.isolate() in the render function and modify .widget in a reactive.effect (use with .widget.batch_update(): for many changes, or .widget.batch_animate() for animated transitions).
Sizing: a figure with an explicit layout.height keeps that height instead of filling its container. For a plot that fills a fixed height, use output_widget("id", height="450px", fillable=True): without fillable=True, the plot renders at plotly’s default height (360px) regardless of the output’s height.
Typing: no return-type annotation is needed – the renderer’s value type is statically bounded, so type checkers flag unsupported return values at decoration time, and the output’s .value attribute is inferred from the render function’s actual return type. If you do annotate, annotate narrowly (e.g., -> go.Figure or -> tuple[go.Figure, dict[str, Any]]): .value is typed from the annotation, so annotating with a wide union of all supported shapes would force you to narrow .value before each use.
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 (seefillandfillable). fill : Optional[bool] = None-
Whether the output should grow or shrink to fit its parent container. When
None(the default), this isTrueifheightis not specified. fillable : Optional[bool] = None-
Whether the output container should fill available space (scrolling its contents on overflow). When
None(the default), this isTrueifheightis not specified. rate_policy : Optional[RatePolicy] = None-
How to rate-limit
hover(),selecting(), andbrushing()(debounced by default) andreactive_read()on the widget:"debounce"or"throttle".None(the default) keeps each accessor’s own default, and debouncesreactive_read()only ifrate_delayis given. rate_delay : Optional[float] = None-
The rate-limit window in seconds (default 0.25).
0disables rate limiting.
Examples
from shiny import App, ui
from shinywidgets import output_widget, render_plotly
import plotly.graph_objects as go
app_ui = ui.page_fluid(output_widget("plot"))
def server(input, output, session):
@render_plotly
def plot():
fig = go.Figure(go.Scatter(x=[1, 2, 3], y=[1, 4, 9]))
return fig, {"displayModeBar": False}
app = App(app_ui, server)Reacting to clicks and selections:
import plotly.express as px
from shiny import reactive, req
from shiny.express import render
from shinywidgets import render_plotly
df = px.data.iris()
@render_plotly
def plot():
fig = px.scatter(df, x="sepal_width", y="sepal_length", custom_data=[df.index])
return fig.update_layout(dragmode="select")
@render.data_frame
def picked():
return plot.selected_rows(df) # empty until the user selects
@reactive.effect
def _():
pts = plot.clicked()
req(pts is not None)
plot.widget.layout.title.text = f"Clicked x={pts['x'].iloc[0]}"Each accessor can also be read by name – out.read("selected") – 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
brushed() |
The region of the most recent selection, once the gesture ends: {"range": {"x": [x0, x1], "y": [y0, y1]}} for a box, or {"lassoPoints": {"x": [...], "y": [...]}} for a lasso, in data coordinates (as plotly.js reports them). |
brushing() |
Like brushed(), but updates while the user drags the selection. Debounced (250 ms) by default; see the rate_policy and rate_delay parameters. |
clicked() |
The points of the most recent click, one row per point. |
double_clicked() |
A pulse that fires on every double-click: None until the first one, then True, re-firing readers on each subsequent one even though the value stays True. |
hover() |
The points under the mouse, with the same columns as clicked(). |
legend_clicked() |
The traces affected by the most recent legend click, one row per trace: trace_index, name, and legendgroup. The clicked trace is the first row; when it belongs to a legendgroup, the group’s other traces follow (plotly.js toggles the whole group by default, but only the clicked trace when the legend’s groupclick="toggleitem"), unlike R plotly’s plotly_legendclick, which ships the traces’ full data. |
relayout() |
The axis changes of the most recent zoom, pan, or autoscale, as plotly.js reports them: keys like "xaxis.range[0]" and "xaxis.autorange" (other relayout keys are dropped). |
selected() |
The points of the most recent box or lasso selection, once the gesture ends, with the same columns as clicked(). |
selected_rows() |
The rows of df that the user selected (see selected()). |
selecting() |
Like selected(), but updates while the user drags the selection. Debounced (250 ms) by default; see the rate_policy and rate_delay parameters. |
visible_traces() |
The current visibility of every trace: trace_index, name, and visible (False once a legend click hides the trace, or the figure set visible=False/"legendonly"). |
brushed()
render_plotly.brushed()The region of the most recent selection, once the gesture ends: {"range": {"x": [x0, x1], "y": [y0, y1]}} for a box, or {"lassoPoints": {"x": [...], "y": [...]}} for a lasso, in data coordinates (as plotly.js reports them).
Returns None until the user selects, and again after a deselect, or after a plain click with dragmode="select"/"lasso" (a zero-area selection). Not rate-limited.
brushing()
render_plotly.brushing()Like brushed(), but updates while the user drags the selection. Debounced (250 ms) by default; see the rate_policy and rate_delay parameters.
clicked()
render_plotly.clicked()The points of the most recent click, one row per point.
Columns are trace_index, point_index (the point’s position within its trace, not a row of your data), x, y, and z and customdata when the figure has them. Returns None until the user clicks, and again after a double-click on the plot background or a deselect, so check for None (e.g. req(pts is not None)) to wait for a click. A double-click on a marker re-fires the click after the clear, so it leaves the value unchanged. Clicking the same point twice doesn’t fire twice. Not rate-limited. Requires pandas.
Kept across re-renders: after a re-render with new data, a retained point_index can point at a different point, so key lookups off customdata (see selected_rows()) rather than the position.
double_clicked()
render_plotly.double_clicked()A pulse that fires on every double-click: None until the first one, then True, re-firing readers on each subsequent one even though the value stays True.
This is how “the user reset the view” becomes observable on non-cartesian subplots: a cartesian double-click reset also reports axis ranges to relayout(), but mapbox, ternary, and smith subplots don’t. The exception is 3D: plotly.js’s gl3d subplot never emits plotly_doubleclick, so a double-click on a scatter_3d scene doesn’t fire this pulse.
Note that a double-click on the plot background also clears clicked() (and a deselect clears the selection accessors), so clicked() becoming None conflates double-click and deselect by design – read this accessor to tell them apart. In the "select"/"lasso" dragmodes a background double-click deselects without firing this pulse (plotly.js emits plotly_doubleclick only where it means resetting the view, matching R plotly). Not rate-limited.
hover()
render_plotly.hover()The points under the mouse, with the same columns as clicked().
Returns None until the mouse first crosses a point, and again when it leaves the points (plotly.js’s plotly_unhover), so check for None (e.g. req(pts is not None)). Debounced (250 ms) by default, so readers update once the mouse rests; see the rate_policy and rate_delay parameters. Requires pandas.
legend_clicked()
render_plotly.legend_clicked()The traces affected by the most recent legend click, one row per trace: trace_index, name, and legendgroup. The clicked trace is the first row; when it belongs to a legendgroup, the group’s other traces follow (plotly.js toggles the whole group by default, but only the clicked trace when the legend’s groupclick="toggleitem"), unlike R plotly’s plotly_legendclick, which ships the traces’ full data.
Returns None until the user clicks a legend item, and re-fires on every click, even of the same item (a second click toggles visibility back). A legend double-click (isolate traces) also fires it, once per click. Read visible_traces() for the visibility state the click produces. Not rate-limited. Requires pandas.
relayout()
render_plotly.relayout()The axis changes of the most recent zoom, pan, or autoscale, as plotly.js reports them: keys like "xaxis.range[0]" and "xaxis.autorange" (other relayout keys are dropped).
The payload is a delta – only the keys that gesture changed, not the current axes – and it keeps its value across re-renders, where it then refers to the previous render’s plot (R plotly’s event_data("plotly_relayout") behaves the same way). For the displayed plot’s current ranges, read plot.widget.layout (e.g. plot.widget.layout.xaxis.range) – plotly.py syncs every zoom and pan back to it; this accessor works well as the reactive trigger for such a read. Returns None until the user changes an axis. Not rate-limited (plotly.js reports once per gesture).
selected()
render_plotly.selected()The points of the most recent box or lasso selection, once the gesture ends, with the same columns as clicked().
Selection must be enabled on the plot: set dragmode="select" or "lasso" on the figure’s layout, or leave the modebar visible so the user can pick the Box/Lasso Select tool.
Returns None until the user selects, and again after a deselect (double-click); an empty frame for a selection that caught no points. With dragmode="select" or "lasso", a plain click also resets it (plotly.js reports the click as a zero-area selection). Not rate-limited. Requires pandas. See selected_rows() for rows of your data – and prefer keying lookups off customdata as it does: point_index is a position, which a re-render with new data can silently re-point at a different point.
selected_rows()
render_plotly.selected_rows(df: IntoDataFrameT, *, key: Optional[str] = None)The rows of df that the user selected (see selected()).
Plotly reports points of the plot, not rows of your data, so each point must carry its row’s identity as customdata, which is matched to df:
- pandas:
px.scatter(df, ..., custom_data=[df.index]). Matched todf’s index labels, so this also works on a filtered or sorteddf. - Any frame (e.g., polars), by a key column:
custom_data=["id"]withkey="id". Matched to that column’s values. Without an id column, add one withdf.with_row_index("id").
Returns an empty frame while nothing is selected, so it can be passed straight to @render.data_frame.
selecting()
render_plotly.selecting()Like selected(), but updates while the user drags the selection. Debounced (250 ms) by default; see the rate_policy and rate_delay parameters.
visible_traces()
render_plotly.visible_traces()The current visibility of every trace: trace_index, name, and visible (False once a legend click hides the trace, or the figure set visible=False/"legendonly").
Seeded from the displayed figure at render, so it reflects the initial figure before any interaction, and updated after every restyle that changes visibility (legend clicks and double-clicks, or .widget.update_traces(visible=...), batched or not) and after trace-structure changes (.widget.add_trace(...), delete_traces, reordering). Visibility here is each trace’s visible attribute; traces expanded by a groupby transform report their own, not each group’s. Not rate-limited. Requires pandas.