Interactive widgets#
What are widgets?#
Widgets are eventful python objects that have a representation in the browser, often as a control like a slider, textbox, etc.
This notebook is an introduction to ipywidgets
(see docs here),
one of the most popular widgets libraries for Jupyter.
What can they be used for?#
You can use widgets to build interactive GUIs for your notebooks. You can also use widgets to synchronize stateful and stateless information between Python and JavaScript.
Using widgets#
To use the widget framework, you need to import ipywidgets.
import ipywidgets as ipw
repr#
Widgets have their own display repr
which allows them to be displayed using IPython
’s display framework.
Constructing and returning an IntSlider
automatically displays the widget (as seen below).
Widgets are displayed inside the output area below the code cell.
Clearing cell output will also remove the widget.
slider = ipw.IntSlider()
slider
Closing widgets#
You can close a widget by calling its close()
method.
# slider.close()
Widget properties#
All of the IPython
widgets share a similar naming scheme.
To read the value of a widget, you can query its value
property.
w = ipw.IntSlider()
w
w.value
0
Similarly, to set a widget’s value, you can set its value
property.
w.value = 55
Widget list#
This is a non-exhaustive list of the mostly used widgets. You can see the full list here.
Numeric widgets#
There are many widgets distributed with ipywidgets
that are designed to display numeric values.
Widgets exist for displaying integers and floats, both bounded and unbounded.
The integer widgets share a similar naming scheme to their floating point counterparts.
By replacing Float
with Int
in the widget name, you can find the Integer equivalent.
IntSlider#
ipw.IntSlider(
value=7,
min=0,
max=10,
step=1,
description="Test:",
disabled=False,
continuous_update=False,
orientation="horizontal",
readout=True,
readout_format="d",
)
FloatSlider#
ipw.FloatSlider(
value=7.5,
min=0,
max=10.0,
step=0.1,
description="Test:",
disabled=False,
continuous_update=False,
orientation="horizontal",
readout=True,
readout_format=".1f",
)
IntRangeSlider#
ipw.IntRangeSlider(
value=[2, 7],
min=0,
max=10,
step=1,
description="Test:",
disabled=False,
continuous_update=False,
orientation="horizontal",
readout=True,
readout_format="d",
)
FloatProgress#
ipw.FloatProgress(
value=7.5,
min=0,
max=10.0,
step=0.1,
description="Loading:",
bar_style="info",
orientation="horizontal",
)
IntText#
ipw.IntText(value=7, description="An integer:", disabled=False)
Boolean widgets#
There are three widgets that are designed to display a boolean value.
Checkbox#
ipw.Checkbox(value=False, description="Check me", indent=False)
Selection widgets#
There are several widgets that can be used to display single selection lists, and two that can be used to select multiple values.
All inherit from the same base class.
You can specify the enumeration of selectable options by passing a list (options are either (label, value) pairs, or simply values for which the labels are derived by calling str
).
Dropdown#
ipw.Dropdown(
options=["1", "2", "3"],
value="2",
description="Number:",
disabled=False,
)
The following is also valid, displaying the words 'One', 'Two', 'Three'
as the dropdown choices but returning the values 1, 2, 3
.
ipw.Dropdown(
options=[("One", 1), ("Two", 2), ("Three", 3)],
value=2,
description="Number:",
)
String widgets#
There are several widgets that can be used to display a string value.
The Text
and Textarea
widgets accept input.
The HTML
and HTMLMath
widgets display a string as HTML (HTMLMath
also renders math).
The Label
widget can be used to construct a custom control label.
Text#
ipw.Text(
value="Hello World",
placeholder="Type something",
description="String:",
disabled=False,
)
Textarea#
ipw.Textarea(
value="Hello World",
placeholder="Type something",
description="String:",
disabled=False,
)
Label#
The Label
widget is useful if you need add custom text and place it next to other widgets.
ipw.HBox(
[
ipw.Label(value=r"This is before the slider"),
ipw.FloatSlider(),
ipw.Label(value=r"This is after the slider"),
]
)
HTML#
ipw.HTML(
value="Hello <b>World</b>",
)
Image#
file = open("../../images/ESS_02_small.jpg", "rb")
image = file.read()
ipw.Image(
value=image,
format="jpg",
width=300,
height=400,
)
Container/Layout widgets#
These widgets are used to hold other widgets, called children. Each has a children
property that may be set either when the widget is created or later.
HBox#
items = [ipw.Button(description=str(i)) for i in range(4)]
ipw.HBox(items)
VBox#
left_box = ipw.VBox([items[0], items[1]])
right_box = ipw.VBox([items[2], items[3]])
ipw.HBox([left_box, right_box])
Widget events#
Widget properties are IPython
traitlets and traitlets are eventful. To handle changes, the observe
method of the widget can be used to register a callback. The doc string for observe
can be seen below.
help(ipw.Widget.observe)
Help on function observe in module traitlets.traitlets:
observe(self, handler: 't.Callable[..., t.Any]', names: 'Sentinel | str | t.Iterable[Sentinel | str]' = traitlets.All, type: 'Sentinel | str' = 'change') -> 'None'
Setup a handler to be called when a trait changes.
This is used to setup dynamic notifications of trait changes.
Parameters
----------
handler : callable
A callable that is called when a trait changes. Its
signature should be ``handler(change)``, where ``change`` is a
dictionary. The change dictionary at least holds a 'type' key.
* ``type``: the type of notification.
Other keys may be passed depending on the value of 'type'. In the
case where type is 'change', we also have the following keys:
* ``owner`` : the HasTraits instance
* ``old`` : the old value of the modified trait attribute
* ``new`` : the new value of the modified trait attribute
* ``name`` : the name of the modified trait attribute.
names : list, str, All
If names is All, the handler will apply to all traits. If a list
of str, handler will apply to all names in the list. If a
str, the handler will apply just to that name.
type : str, All (default: 'change')
The type of notification to filter by. If equal to All, then all
notifications are passed to the observe handler.
Mentioned in the doc string, the callback registered must have the signature handler(change)
where change
is a dictionary holding the information about the change.
Using this method, an example of how to output an IntSlider
’s value as it is changed can be seen below.
sl = ipw.IntSlider()
but = ipw.Button()
def on_value_change(change):
but.description = str(change["new"])
sl.observe(on_value_change, names="value")
ipw.HBox([sl, but])
Linking Widgets#
Often, you may want to simply link widget attributes together. Synchronization of attributes can be done in a simpler way than by using bare traitlets events.
The method is to use the link
and dlink
functions from the traitlets module (these two functions are re-exported by the ipywidgets
module for convenience). This only works if we are interacting with a live kernel.
caption = ipw.Label(value="The values of slider1 and slider2 are synchronized")
sliders1, slider2 = ipw.IntSlider(description="Slider 1"), ipw.IntSlider(
description="Slider 2"
)
l = ipw.link((sliders1, "value"), (slider2, "value"))
display(caption, sliders1, slider2)
caption = ipw.Label(value="Changes in source values are reflected in target1")
source, target1 = ipw.IntSlider(description="Source"), ipw.IntSlider(
description="Target 1"
)
dl = ipw.dlink((source, "value"), (target1, "value"))
display(caption, source, target1)
The link()
and dlink()
functions return a Link
or DLink
object.
The link can be broken by calling the unlink
method.
l.unlink()
dl.unlink()
Using “Interact”#
ipywidgets
also comes with a high-level function called interact
which tries to do a lot of the work for you, by guessing what kind of widget you would like to have to interact with a function, depending on the function input types.
You can read more on how to use interact
here.
Exercises#
1. Dynamically disabling and hiding widgets#
In this exercise, we will learn how to use the disabled
and visibility
properties of the widgets, to control their behaviour and appearance.
The goal is to create an interface that allows to enter into text boxes a number of vector components which depend on how many dimensions are available.
Create a
RadioButtons
widget with 3 options:"1D"
,"2D"
, and"3D"
, and the"1D"
value should be the selected value to begin with.Create 3
FloatText
widgets which all have0
as a value and"x:"
,"y:"
, and"z:"
as descriptions for the vector position.Create a further 3
FloatText
widgets which all have0
as a value and"v_x:"
,"v_y:"
, and"v_z:"
as descriptions for the velocity components.Arrange your widgets into 3 columns using the
VBox
andHBox
widgets.Disable the
y
andz
position components by using the.disabled
property of the widgets.Hide the
v_y
andv_z
velocity components by using the.layout.visibility="hidden"
property of the widgets.In the end, it should look something like this:
Create a function that will run through the position and velocity components and update the
.disabled
and.visibility="hidden/visible"
properties according to the selected value in theRadioButtons
widget.Install a call-back using the
.observe
property from theRadioButtons
to the update function so that the function is triggered every time the selected value in theRadioButtons
is changed and check that everything is working as it should.Voila!
Bonus: Instead of a
RadioButtons
, use aIntText
widget to allow any number of dimensions to be entered. This means you have to dynamically create/destroyFloatText
widgets in the positions and velocity columns. The contents of aVBox
can be modified by changing its list of.children
. It also does not make much sense to have disabled or hidden widgets since we do not know a priori the maximum number of dimensions. The goal is to only keep the widgets that we need, and they will all always be visible and enabled.
Solution:
Show code cell content
rb = ipw.RadioButtons(
options=["1D", "2D", "3D"],
description="Ndims:",
value="1D",
layout={"width": "50px"},
)
ndim = int(rb.value[0])
pos = []
vel = []
for i, x in enumerate("xyz"):
pos.append(
ipw.FloatText(
value=0,
description=f"{x}:",
disabled=ndim < 1 + i,
layout={"width": "200px"},
)
)
vel.append(ipw.FloatText(value=0, description=f"v_{x}:", layout={"width": "200px"}))
if i > ndim - 1:
vel[-1].layout.visibility = "hidden"
def update_components(change):
ndim = int(change["new"][0])
for i in range(len(pos)):
pos[i].disabled = ndim < 1 + i
if i > ndim - 1:
vel[i].layout.visibility = "hidden"
else:
vel[i].layout.visibility = "visible"
rb.observe(update_components, names="value")
box = ipw.HBox([rb, ipw.VBox(pos), ipw.VBox(vel)])
box
Bonus:
Show code cell content
layout = {"width": "200px"}
ndim = ipw.IntText(value=1, description="Ndims:", layout=layout)
pos = []
vel = []
for i in range(ndim.value):
pos.append(ipw.FloatText(value=0, description=f"pos_{i}:", layout=layout))
vel.append(ipw.FloatText(value=0, description=f"vel_{i}:", layout=layout))
pos_box = ipw.VBox(pos)
vel_box = ipw.VBox(vel)
def update_components(change):
ndim_new = change["new"]
ndim_old = len(pos_box.children)
if ndim_old < ndim_new:
new_pos = []
new_vel = []
for i in range(ndim_new - ndim_old):
new_pos.append(
ipw.FloatText(
value=0, description=f"pos_{i + ndim_old}:", layout=layout
)
)
new_vel.append(
ipw.FloatText(
value=0, description=f"vel_{i + ndim_old}:", layout=layout
)
)
pos_box.children = list(pos_box.children) + new_pos
vel_box.children = list(vel_box.children) + new_vel
else:
pos_box.children = [pos_box.children[i] for i in range(ndim_new)]
vel_box.children = [vel_box.children[i] for i in range(ndim_new)]
ndim.observe(update_components, names="value")
box = ipw.HBox([ndim, pos_box, vel_box])
box
2. Use a slider to change the number of scatter points in a Matplotlib figure#
Create a scatter plot with Matplotlib
import numpy as np
import matplotlib.pyplot as plt
%matplotlib widget
N = 100
x = np.random.normal(0.0, scale=20.0, size=N)
y = np.random.normal(0.0, scale=20.0, size=N)
fig, ax = plt.subplots()
scat = ax.scatter(x, y, color="blue", alpha=0.5)
Add a
FloatLogSlider
that ranges from 1 to 1.0e5Define a function that would take in the value of the slider (via the
change
argument) and update the number of points in the scatter plot (for simplicity, you can delete the current scatter plot and draw a new one on the axes)Add a callback from the slider to the update function using the
observe
method.Display the slider above or below the figure.
Once you got this working, try adding a
ColorPicker
widget that would be used to update the color of the scatter points.
Solution:
Show code cell content
N = 100
x = np.random.normal(0.0, scale=20.0, size=N)
y = np.random.normal(0.0, scale=20.0, size=N)
fig, ax = plt.subplots()
scat = ax.scatter(x, y, color="blue", alpha=0.5)
cp = ipw.ColorPicker(
concise=False, description="Pick a color", value="blue", disabled=False
)
def update_scatter(change):
ax.collections[0].remove()
M = int(change["new"])
x = np.random.normal(0.0, scale=20.0, size=M)
y = np.random.normal(0.0, scale=20.0, size=M)
ax.scatter(x, y, color=cp.value, alpha=0.5)
sl = ipw.FloatLogSlider(
value=N,
base=10,
min=0, # max exponent of base
max=5, # min exponent of base
step=0.1, # exponent step
description="Log Slider",
)
sl.observe(update_scatter, names="value")
def update_color(change):
ax.collections[-1].set_color(change["new"])
cp.observe(update_color, names="value")
ipw.HBox([sl, cp])
3. Create an interface to dynamically resize an image#
In this exercise, we will create an interface with a displayed image and some control widgets below, that will enable dynamic resizing of the image.
Using the
PIL
(Python Image Library), get an image from a web url:
import PIL.Image as Image
img = Image.open("../../images/ESS_02_small.jpg")
img
Place the image inside an
Image
widget:
im_widget = ipw.Image(value=img._repr_png_(), width=img.width, height=img.height)
Create two
IntText
widgets (one for the image width and one for the image height) and aButton
widget that will run the image resizing function when clicked.Place the widgets inside a
HBox
container widget to keep them in the same line, and place theim_widget
and theHBox
inside aVBox
container to place the image above the control widgets.At this point, it is probably a good idea to try and display the
VBox
to check that everything is set up properly.Next, create a function that, when called, would resize the image. In that function, you will need to:
Make a copy of the original image
Resize the copy with the
PIL
resize()
function:copied_image.resize((new_width, new_height))
Update the
value
of theim_widget
with thenew_image._repr_png_()
Finally, you need to install a
on_click
call-back (see here) from theButton
to the resizing function so that the function is triggered when theButton
is clicked.Voila!
Bonus: Try to achieve the same but using two
IntSlider
s instead ofIntText
so that you can resize the image by simply dragging sliders!
Solution:
Show code cell content
im_size = [img.width, img.height]
width = ipw.IntText(value=img.width, description="Width:")
height = ipw.IntText(value=img.height, description="Height:")
button = ipw.Button(description="Resize!")
hbox = ipw.HBox([width, height, button])
def resize_image(event):
resized = img.copy().resize((width.value, height.value))
im_widget.value = resized._repr_png_()
im_widget.width = width.value
im_widget.height = height.value
return
button.on_click(resize_image)
sl_width = ipw.IntSlider(value=img.width, min=2, max=img.width)
sl_height = ipw.IntSlider(value=img.height, min=2, max=img.height)
def update_width(change):
width.value = change["new"]
resize_image(None)
return
def update_height(change):
height.value = change["new"]
resize_image(None)
return
sl_width.observe(update_width, names="value")
sl_height.observe(update_height, names="value")
ipw.VBox([im_widget, hbox])
Bonus:
Show code cell content
sl_width = ipw.IntSlider(description="width", value=img.width, min=2, max=img.width)
sl_height = ipw.IntSlider(
descriptiuon="height", value=img.height, min=2, max=img.height
)
def update_width(change):
width.value = change["new"]
resize_image(None)
return
def update_height(change):
height.value = change["new"]
resize_image(None)
return
sl_width.observe(update_width, names="value")
sl_height.observe(update_height, names="value")
ipw.VBox([ipw.HBox([sl_width, sl_height]), im_widget])