Jupyter Widgets: The Good Parts
This page is largely adapted from “Building a Custom Widget” in the Jupyter Widgets documentation. I’d hoped to avoid separately documenting the Jupyter Widgets API, but their tutorial mixes boilerplate/packaging details with core “concepts” to Jupyter Widgets. Below is an attempt to distill these concepts for anywidget authors.
The Widget Front End
This section frames the Juptyer Widgets documentation in the context of anywidget. Remember that anywidget is just abstraction over traditional Jupyter Widgets that removes boilerplate and packaging details.
Comparison with traditional Jupyter Widgets
anywidget simplifies creating your widget’s front-end code. Its only
requirement is that your widget front-end code is a valid
JavaScript module
and exports a function called render
. This render
function is similar to the
traditional
DOMWidgetView.render
.
Concretely, custom widgets are traditionally defined like:
import { DOMWidgetModel, DOMWidgetView } from "@jupyter-widgets/base";
// All boilerplate, anywidget takes care of this ...
class CustomModel extends DOMWidgetModel {
/* ... */
}
class CustomView extends DOMWidgetView {
render() {
let view = this;
let el = this.el;
let model = this.model;
/* ... */
}
}
export { CustomModel, CustomView };
… which must be transformed, bundled, and installed in multiple notebook environments.
In anywidget, the above code simplifies to:
/** @param {{ model: DOMWidgetModel, el: HTMLElement }} context */
export function render(context) {
let el = context.el;
let model = context.model;
/* ... */
}
… which explicity defines the widget view via the render
function, and
(implicitly) anywidget defines the associated widget model (i.e.,
CustomModel
). anywidget front-end code is often so minimal that it can
easily be inlined as a Python string:
class CustomWidget(anywidget.AnyWidget):
_esm = """
export function render(context) {
let el = context.el;
let model = context.model;
/* ... */
}
"""
The render
function
Just like DOMWidgetView.render
, your widget’s render
function is executed
exactly one per output cell that displays the widget instance. Therefore,
render
primarily serves two purposes:
- Initializing content to display (i.e., create and append element(s) to
context.el
) - Registering event handlers to update or display model state any time it
changes (i.e., passing callbacks to
context.model.on
)
Connecting JavaScript with Python
The Jupyter Widgets framework is build on top of the IPython Comm framework (short for communication). It’s worth reading the Low Level Widget Explanation to understand the core of Jupyter Widget’s Model, View, Controller (MVC) architecture, but in short the Comm framework exposes two mechanisms to send/receive data to/from the front end:
1. Traitlets
traitlets
are the easiest and most flexible way to synchronize data between the front end
and Python. The sync=True
keyword argument tells the widget framework to
handle synchronizing that value to the front end. Take the following
CustomWidget
:
class CustomWidget(anywidget.AnyWidget):
_esm = pathlib.Path("index.js")
my_value = traitlets.Int(0).tag(sync=True)
It defines an Integer my_value
trait, which is synchronized with the front
end. The render
function now has the ability to:
- get
my_value
// index.js
export function render({ model, el }) {
let my_value = model.get("my_value");
}
- set
my_value
// index.js
export function render({ model, el }) {
model.set("my_value", 42);
model.save_changes(); // required to send update to Python
}
- listen for changes to
my_value
(and register event handlers)
// index.js
export function render({ model, el }) {
function on_change() {
let new_my_value = model.get("my_value");
console.log(`The 'my_value' changed to: ${new_my_value}`);
}
model.on("change:my_value", on_change);
}
Note: In the snippet above,
on_change
is called an event handler because it executes any timemy_value
is updated from either Python or the front-end code (i.e., a change event).
An important aspect traitlets, and their first-class support in Juptyer Widgets, is that it is easy to compose Jupyter Widgets together in Python. For example,
import ipywidgets
# create a custom widget
widget = CustomWidget()
# link a slider widget with our custom widget
slider = ipywidgets.IntSlider()
ipywidgets.link((widget, "my_value"), (slider, "value"))
# log the value of `my_value` any time it changes
output = ipywidgets.Output()
@output.capture()
def handle_change(change):
"""Prints new value to `output` widget"""
print(change.new)
widget.observe(handle_change, names=["my_value"])
ipywidgets.VBox([slider, widget, output])
It doesn’t matter if our widget is updated from JavaScript or Python, the IPython framework ensures it stays in sync with all the different components.
2. Custom messages
A second mechanism to send data to the front end is with custom messages. Within
your render
function, you can listen to msg:custom
events on the model
.
For example,
class CustomMessageWidget(anywidget.AnyWidget):
_esm = """
export function render({ model, el }) {
model.on("msg:custom", msg => {
console.log(`new message: ${JSON.stringify(msg)}`);
});
}
"""
widget = CustomMessageWidget()
widget # display the widget
# send message
widget.send({ "type": "my-event", "foo": "bar" })
# Browser console:
# new message: '{ "type": "my-event", "foo": "bar" }'
Warning: Custom messages are only received if your front-end code has executed (i.e., the widget is displayed before sending messages). Calling the snippet above out of order:
widget = CustomMessageWidget() # send message, but no event listeners! widget.send({ "type": "my-event", "foo": "bar" }) # displays widget (starts listening for events) widget
Tips for beginners
anywidget is a minimal layer on top of Jupyter Widgets and explicitly avoids inventing new concepts or widget APIs. Its design allows widget authors to have nearly same low-level control over their Jupyter integrations, but this flexibility can be intimidating and confusing new widget authors.
Here are some general recommendations for being productive with anywidget:
-
Start small. Jupyter Widgets combine many concepts and tools from JavaScript and Python. Unlike traditional widgets, anywidget allows you to learn both ecosystems incrementally. Start with a minimal example, and slowly add new functionality. Change traitlets. Update model state from JavaScript, then update model state from Python. Add an event handler. It’s a great way to learn.
-
Learn to identify ECMAScript modules (ESM). ESM is the core technology used by anywidget, and a deeper understanding will help you discern what is and (perhaps more importantly) is not standard JavaScript. I recommend reading Lin Clark’s ”ES modules: A cartoon deep-dive” to learn more.
-
Prefer Traitlets over custom messages for state synchronization. Widget state can be fully recreated from traits without Python running, whereas custom messages require both an active Python kernel and special ordering of function calls. Write logic that treats your
model
as the source of truth (see Two-Way Data-Binding Example). -
Use the browser console. View errors or intermediate values in your front-end code with the browser’s developer tools. Getting comfortable with the console will help demystify the front end and enable you to quickly debug your widgets.
-
Have fun. A primary goal of anywidget is to make it simple and enjoyable to create custom widgets. While it can serve as the foundation for a useful domain-specific integration, anywidget can also be used as a learning tool to poke around with the front end - or yet another way to Rick Roll your friends.