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:
- Plots, like altair, bokeh, and plotly.
- Maps, like pydeck and ipyleaflet.
- Tables, ipydatagrid and ipysheet.
- 3D visualizations, like ipyvolume and pythreejs.
- Media streaming, like ipywebrtc.
- Other awesome widgets
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.
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 ipyleaflet pydeck==0.8.0
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
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
#| 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==0.8.0
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.
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
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
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:
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
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
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:
- The ipywidgets API.
- The widget implementation’s API (e.g.,
altair
’sChart
,plotly
’sFigure
, etc). - 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
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:
- 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.
- 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.
- 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).
- 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.