Jupyter Widgets

Shiny fully supports ipywidgets (aka Jupyter Widgets) via the shinywidgets package. Many notable Python packages build on ipywidgets to provide highly interactive widgets in Jupyter notebooks, including:

In this article, we’ll learn how to leverage ipywidgets in Shiny, including how to render them, efficiently update them, and respond to user input.

Not all Jupyter Widgets are ipywidgets

Although the term “Jupyter Widgets” is often used to refer to ipywidgets, it’s important to note that not all Jupyter Widgets are ipywidgets. For example, packages like folium and ipyvizzu aren’t compatible with ipywidgets, but do provide a _repr_html_ method for getting the HTML representation. It may be possible to display these widgets using Shiny’s @render.ui decorator.

Installation

To use ipywidgets in Shiny, start by installing shinywidgets:

pip install shinywidgets

Then, install the ipywidgets that you’d like to use. For this article, we’ll need the following:

pip install altair bokeh plotly pydeck ipyleaflet

Get started

To render an ipywidget you first define a reactive function that returns the widget and then decorate it with @render_widget. Some popular widgets like altair have specially-designed decorators for better ergonomics and we recommend using them if they exist.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 485

from shiny.express import input, ui
from shinywidgets import render_altair
import soft_dependencies

ui.input_selectize(
    "var", "Select variable",
    choices=["bill_length_mm", "body_mass_g"]
)

@render_altair
def hist():
    import altair as alt
    from palmerpenguins import load_penguins
    df = load_penguins()
    return alt.Chart(df).mark_bar().encode(
        x=alt.X(f"{input.var()}:Q", bin=True),
        y="count()"
    )
## file: requirements.txt
altair
anywidget
palmerpenguins
jsonschema
## file: soft_dependencies.py
# Temporary workaround to inform shinylive of soft dependencies
import anywidget
import jsonschema
import mypy_extensions
import toolz
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 485

from shiny.express import input, ui
from shinywidgets import render_bokeh

ui.input_selectize(
    "var", "Select variable",
    choices=["bill_length_mm", "body_mass_g"]
)

@render_bokeh
def hist():
    from bokeh.plotting import figure
    from palmerpenguins import load_penguins

    p = figure(x_axis_label=input.var(), y_axis_label="count")
    bins = load_penguins()[input.var()].value_counts().sort_index()
    p.quad(
        top=bins.values,
        bottom=0,
        left=bins.index - 0.5,
        right=bins.index + 0.5,
    )
    return p
## file: requirements.txt
bokeh
jupyter_bokeh
xyzservices
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 485

from shiny.express import input, ui
from shinywidgets import render_plotly

ui.input_selectize(
    "var", "Select variable",
    choices=["bill_length_mm", "body_mass_g"]
)

@render_plotly
def hist():
    import plotly.express as px
    from palmerpenguins import load_penguins
    df = load_penguins()
    return px.histogram(df, x=input.var())

## file: requirements.txt
palmerpenguins
plotly
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 485

import pydeck as pdk
import shiny.express
from shinywidgets import render_pydeck

@render_pydeck
def map():
    UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv"

    layer = pdk.Layer(
        "HexagonLayer",  # `type` positional argument is here
        UK_ACCIDENTS_DATA,
        get_position=["lng", "lat"],
        auto_highlight=True,
        elevation_scale=50,
        pickable=True,
        elevation_range=[0, 3000],
        extruded=True,
        coverage=1,
    )

    # Set the viewport location
    view_state = pdk.ViewState(
        longitude=-1.415,
        latitude=52.2323,
        zoom=6,
        min_zoom=5,
        max_zoom=15,
        pitch=40.5,
        bearing=-27.36,
    )

    # Combined all of it and render a viewport
    return pdk.Deck(layers=[layer], initial_view_state=view_state)
## file: requirements.txt
pydeck

Many other awesome Python packages provide widgets that are compatible with Shiny. In general, you can render them by applying the @render_widget decorator.

import shiny.express
from shinywidgets import render_widget

@render_widget
def widget():
    # Widget code goes here
    ...

Widget object

In order to create rich user experiences like linked brushing, editable tables, and smooth transitions, it’s useful to know how to efficiently update and respond to user input. In either case, we’ll need access to the Python object underlying the rendered widget. This object is available as a property, named widget, on the render function. From this widget object, you can then access its attributes and methods. As we’ll see later, special widget attributes known as traits, can be used to efficiently update and respond to user input.

Discovering traits

If you’re not sure what traits are available, you can use the widget.traits() method to list them.

This widget object is always a subclass of ipywidgets.Widget and may be different from the object returned by the render function. For example, the hist function below returns Figure, but the widget property is a FigureWidget (a subclass of ipywidgets.Widget). In many cases, this is useful since ipywidgets.Widget provides a standard way to efficiently update and respond to user input that shinywidgets knows how to handle. If you need the actual return value of the render function, you can access it via the value property.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 480
from shiny.express import render
from shinywidgets import render_plotly

@render_plotly
def hist():
    import plotly.express as px
    return px.histogram(px.data.tips(), x="tip")

@render.code
def info():
    return str([type(hist.widget), type(hist.value)])
## file: requirements.txt
pandas
plotly
Typing & class coercion

The “main” API for notable packages like altair, bokeh, plotly, and pydeck don’t subclass ipywidgets.Widget (so that they can be used outside of a notebook). Shinywidgets is aware of this and automatically coerces to the relevant subclass (e.g, plotly’s Figure -> FigureWidget).

As long as you’re using the dedicated decorators for these packages (e.g., @render_altair), the widget property’s type will know about the coercion (i.e., you’ll get proper autocomplete and type checking on the widget property).

Efficient updates

If you’ve used ipywidgets before, you may know that widgets have traits that can be updated after the widget is created. It’s often much more performant to update a widget’s traits instead of re-creating it from from scratch, and so you should update a widget’s traits when performance is critical.

For example, in a notebook, you may have written a code cell like this to first display a map:

import ipyleaflet as ipyl
map = ipyl.Map()

Then, in a later cell, you may have updated the map’s center trait to change the map’s location:

map.center = (51, 0)

With shinywidgets, we can do the same thing reactively in Shiny by updating the widget property of the render function. For example, the following code creates a map, then updates the map’s center whenever the dropdown changes.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400
from shiny import reactive
from shiny.express import input, ui
from shinywidgets import render_widget
import ipyleaflet as ipyl

city_centers = {
    "London": (51.5074, 0.1278),
    "Paris": (48.8566, 2.3522),
    "New York": (40.7128, -74.0060)
}

ui.input_select("center", "Center", choices=list(city_centers.keys()))

@render_widget
def map():
    return ipyl.Map(zoom=4)

@reactive.effect
def _():
    map.widget.center = city_centers[input.center()]
## file: requirements.txt
ipyleaflet
Re-render vs efficient update

If the app above had used @render_widget instead of @reactive.effect to perform the update, then the map would be re-rendered from stratch every time input.center changes, which causes the map to flicker (instead of a smooth transition to the new location).

Respond to user input

There are two different ways to respond to user input:

  1. Reactive traits
  2. Widget event callbacks

It’s usually easiest to use reactive traits but you may need to use event callbacks if the event information isn’t available as a trait.

Reactive traits

If you’ve used ipywidgets before, you may know that widgets have traits that can be accessed and observed. For example, in a notebook, you may have written a code cell like this to display a map:

import ipyleaflet as ipyl
map = ipyl.Map()

Then, in a later cell, you may have read the map’s center trait to get the current map’s location:

map.center

With shinywidgets, we can do the same thing reactively in Shiny by using the reactive_read() function to read the trait in a reactive context. For example, the following example creates a map, then displays/updates the map’s current center whenever the map is panned.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 460
import ipyleaflet as ipyl
from shiny.express import render
from shinywidgets import reactive_read, render_widget

"Click and drag to pan the map"

@render_widget
def map():
    return ipyl.Map(zoom=2)

@render.text
def center():
    cntr = reactive_read(map.widget, 'center')
    return f"Current center: {cntr}"
## file: requirements.txt
ipyleaflet
Observable traits

Under the hood, reactive_read() uses ipywidgets’ observe() method to observe changes to the relevant trait. So, any observable trait can be used with reactive_read().

Some widgets have attributes that contain observable traits. One practical example of this is the selections attribute of altair’s JupyterChart class, which has an observable point trait.

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 460
import altair as alt
from shiny.express import render
from shinywidgets import reactive_read, render_altair
from vega_datasets import data

"Click the legend to update the selection"

@render.code
def selection():
    pt = reactive_read(jchart.widget.selections, "point")
    return str(pt)

@render_altair
def jchart():
    brush = alt.selection_point(name="point", encodings=["color"], bind="legend")
    return alt.Chart(data.cars()).mark_point().encode(
        x='Horsepower:Q',
        y='Miles_per_Gallon:Q',
        color=alt.condition(brush, 'Origin:N', alt.value('grey')),
    ).add_params(brush)
## file: requirements.txt
altair
anywidget
vega_datasets

Widget event callbacks

Sometimes, you may want to capture user interaction that isn’t available through a widget trait. For example, ipyleaflet.CircleMarker has an .on_click() method that allows you to execute a callback when a marker is clicked. In this case, you’ll want to define a callback that updates some reactive.value everytime its triggered to capture the relevant information. That way, the callback information can be used to cause invalidation of other outputs (or trigger reactive side-effects):

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 450
import ipyleaflet as ipyl
from shiny.express import render
from shiny import reactive
from shinywidgets import render_widget

# Stores the number of clicks
n_clicks = reactive.value(0)

# A click callback that updates the reactive value
def on_click(**kwargs):
    n_clicks.set(n_clicks() + 1)

# Create the map, add the CircleMarker, and register the map with Shiny
@render_widget
def map():
    cm = ipyl.CircleMarker(location=(55, 360))
    cm.on_click(on_click)
    m = ipyl.Map(center=(53, 354), zoom=5)
    m.add_layer(cm)
    return m

@render.text
def nClicks():
    return f"Number of clicks: {n_clicks.get()}"
## file: requirements.txt
ipyleaflet
Widgets can contain other widgets

In the example above, we created a CircleMarker object, then added it to a Map object. Both of these objects subclass ipywidgets.Widget, so they both have traits that can be updated and read reactively.

Layout & styling

Layout and styling of ipywidgets can get a bit convoluted, partially due to potentially 3 levels of customization:

  1. The ipywidgets API.
  2. The widget implementation’s API (e.g., altair’s Chart, plotly’s Figure, etc).
  3. Shiny’s UI layer.

Generally speaking, it’s preferable to use the widget’s layout API if it is available since the API is designed specifically for the widget. For example, if you want to set the size and theme of a plotly figure, can use its update_layout method:

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 285

import plotly.express as px
from shiny.express import input, ui
from shinywidgets import render_plotly

ui.input_selectize(
    "theme", "Choose a theme",
    choices=["plotly", "plotly_white", "plotly_dark"]
)

@render_plotly
def plot():
    p = px.histogram(px.data.tips(), x="tip")
    p.update_layout(template=input.theme(), height=200)
    return p
## file: requirements.txt
pandas
plotly

Arranging widgets

The best way to include widgets in your application is to wrap them in one of Shiny’s UI components. In addition to being quite expressive and flexible, these components make it easy to implement filling and responsive layouts. For example, the following code arranges two widgets side-by-side, and fills the available space:

#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 250

import plotly.express as px
from shiny.express import input, ui
from shinywidgets import render_plotly

ui.page_opts(title = "Filling layout", fillable = True)

with ui.layout_columns():
  @render_plotly
  def plot1():
      return px.histogram(px.data.tips(), y="tip")

  @render_plotly
  def plot2():
      return px.histogram(px.data.tips(), y="total_bill")
## file: requirements.txt
pandas
plotly
Layout gallery

For more layout inspiration, check out the Layout Gallery.

Shinylive

Examples on this page are powered by shinylive, a tool for running Shiny apps in the browser (via pyodide). Generally speaking, apps that use shinywidgets should work in shinylive as long as the widget and app code is supported by pyodide. The shinywidgets package itself comes pre-installed in shinylive, but you’ll need to include any other dependencies in the requirements.txt file.

Examples

For more shinywidgets examples, see the examples/ directory in the shinywidgets repo. The outputs example is a particularly useful example to see an overview of available widgets.

Troubleshooting

If after installing shinywidgets, you have trouble rendering widgets, first try running this “hello world” ipywidgets example. If that doesn’t work, it could be that you have an unsupported version of a dependency like ipywidgets or shiny.

If you can run the “hello world” example, but other widgets don’t work, first check that the extension is properly configured with jupyter nbextension list. If the extension is properly configured, and still isn’t working, here are some possible reasons why:

  1. The widget requires initialization code to work in a notebook environment.
  • In this case, shinywidgets probably won’t work without providing the equivalent setup information to Shiny. A known case of this is bokeh, shinywidgets’ @render_bokeh decorator handles through inclusion of additional HTML dependencies.
  1. Not all widgets are compatible with ipywidgets!
  • Some web-based widgets in Python aren’t compatible with the ipywidgets framework, but do provide a repr_html method for getting the HTML representation (e.g., folium). It may be possible to display these widgets using Shiny’s @render.ui decorator, but be aware that, you may not be able to do things mentioned in this article with these widgets.
  1. The widget itself is broken.
  • If you think this is the case, try running the code in a notebook to see if it works there. If it doesn’t work in a notebook, then it’s likely a problem with the widget itself (and the issue should be reported to the widget’s maintainers).
  1. The widget is otherwise misconfigured (or your offline).
  • shinywidgets tries its best to load widget dependencies from local files, but if it fails to do so, it will try to load them from a CDN. If you’re offline, then the CDN won’t work, and the widget will fail to load. If you’re online, and the widget still fails to load, then please let us know by opening an issue.

For developers

If you’d like to create your own ipywidget that works with shinywidgets, we highly recommend using the anywidget framework to develop that ipywidget. However, if only care about Shiny integration, and not Jupyter, then you may want to consider using a custom Shiny binding instead of shinywidgets. If you happen to already have an ipywidget implementation, and want to use/add a dedicated decorator for it, see how it’s done here.