CopernNet : Point Cloud Segmentation using ActiveSampling Transformers
In the dynamic field of railway maintenance, accurate data is critical. From ensuring the health of ...
Published on: March 31, 2020
This tutorial gives a brief introduction into using ipywidgets in Jupyter Notebooks. Ipywidgets provide a set of building blocks for graphical user interfaces that are powerful, yet easy to use.
A simple use case could be adding some basic controls to a plot for interactive data exploration. On the other side of the spectrum, we can combine widgets together to build full-fledged graphical user interfaces.
Here, we first introduce the interact
function, which is a convenient way to quickly create suitable widgets to control functions. Second, we look into specific widgets and stack them together to build a basic gui application.
The notebook used for this tutorial is available on github, together with a link to a live version on binder.
To run the notebook locally, the very first requirement is a working Jupyter environment. Setting up an installation lies outside the scope of the tutorial, but can be found in the official docs. Note that for this tutorial, all libraries were installed using pip, or the pacman package manager. Anaconda currently has a matplotlib issue that gives some problems (at least on Windows 10). Therefore, if you have problems displaying plots correctly, try using pip only, or Linux. The examples were tested on Windows 10 and Arch Linux.
The versions of packages explicitly used to create the examples are:
To get started, we set the ipympl backend, which makes matplotlib plots interactive. We do this using a magic command, starting with %. We also import some libraries: matplotlib for plotting, NumPy to generate data, and ipywidgets for obvious reasons.
In [1]:
%matplotlib widget
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np
Widgets can be created either directly or through the interact
function. We explore interact
first, as it is convenient for quick use. interact
takes a function as its first argument, followed by the function arguments with their possible values. This creates a widget that allows to select those values, calling the callback with the current value for every selection. This may sound rather abstract at first, but an example will hopefully make it clearer.
The say_my_name
function below prints the text ‘my name is {name}’. We pass say_my_name
into interact
, together with a list of names. This creates a dropdown filled with the names in the list. Every time we pick a name, say_my_name
is called with the currently selected name and the printed message gets updated.
In [2]:
def say_my_name(name):
"""
Print the current widget value in short sentence
"""
print(f'My name is {name}')
widgets.interact(say_my_name, name=["Jim", "Emma", "Bond"]);
You might wonder how interact
decides to create a dropdown list. It turns out that this choice is based on the input options. In the example above, these were given as a list of values, resulting in a dropdown list.
We could also create a slider by passing a tuple of the form (start, stop, step) in which the values are numerical. If all values are integers, this creates an IntSlider; if floats are present (no surprise), a FloatSlider. Alternatively, we could construct a checkbox by simply passing a boolean (i.e. True or False).
The next cell shows this behavior by reusing a single function with different input options to create different kinds of widgets.
In [3]:
def say_something(x):
"""
Print the current widget value in short sentence
"""
print(f'Widget says: {x}')
widgets.interact(say_something, x=[0, 1, 2, 3])
widgets.interact(say_something, x=(0, 10, 1))
widgets.interact(say_something, x=(0, 10, .5))
_ = widgets.interact(say_something, x=True)
interact
is not limited to single argument functions. We can pass multiple arguments to create multiple widgets, following the same rules as above. Below, we use a three
function with three arguments to create three different widgets. Whenever one of the values is changed, three
is called with the current values of the three widgets as its arguments.
In [4]:
def three(x, y, z):
return (x, y, z)
_ = widgets.interact(
three,
x=(0, 10, 1),
y=True,
z=['a', 'b', 'c']
)
Sometimes, not all arguments need to be linked to the widget. We can fix those using the fixed
function. For example, fixing the z
argument in three
results in widgets for x
and y
only.
In [5]:
_ = widgets.interact(
three,
x=(0, 10, 1),
y=True,
z=widgets.fixed('I am fixed')
)
We can also use decorator syntax to create widgets with interact
.
In [6]:
@widgets.interact(x=(0, 10, 1))
def foo(x):
"""
Print the current widget value in short sentence
"""
print(f'Slider says: {x}')
Making widgets and printing stuff is all well and good, but let’s do something slightly more useful and create a perfectly fake oscilloscope. For this, we use matplotlib to create a plot with a fixed vertical scale and a grid. Next, we generate some x
values between 0 and 2pi and define a function to return the sine of x
for some frequency w
, amplitude amp
and phase angle phi
.
Finally, we define an update function that takes three arguments: w
, amp
and phi
, corresponding with the parameters controlling our sine. This function clears all existing lines from the ax object (if any) and then plots our sine wave. Adding the interact decorator completes our beautiful interactive plot.
In [7]:
# set up plot
fig, ax = plt.subplots(figsize=(6, 4))
ax.set_ylim([-4, 4])
ax.grid(True)
# generate x values
x = np.linspace(0, 2 * np.pi, 100)
def my_sine(x, w, amp, phi):
"""
Return a sine for x with angular frequeny w and amplitude amp.
"""
return amp*np.sin(w * (x-phi))
@widgets.interact(w=(0, 10, 1), amp=(0, 4, .1), phi=(0, 2*np.pi+0.01, 0.01))
def update(w = 1.0, amp=1, phi=0):
"""Remove old lines from plot and plot new one"""
[l.remove() for l in ax.lines]
ax.plot(x, my_sine(x, w, amp, phi), color='C0')
For more information on how to use interact
, check the official documentation. In the next bit, we’ll use the widgets directly and stack them together to build larger apps.
Often, we will want to create widgets manually, for example to build larger interfaces with interconnected components. There are many widgets to choose from. The next cell shows a quick and dirty listing of all classes defined in the ipywidgets module. Not all of these are meant for everyday use (e.g. DOMWidget and CoreWidgets), but most of them are immediately useful. The official list can be found here.
In [8]:
widgets.Textarea(
'\n'.join([w for w in dir(widgets) if not w.islower()]),
layout=widgets.Layout(height='200px')
)
The Label
widget shows the value text as uneditable text. If more formatting is required, you can use an HTML
widget. As the name implies, this widget renders an html string. For editable text, there are the Text
and Textarea
widgets.
In [9]:
label = widgets.Label(
value='A label')
html = widgets.HTML(
value='<b>Formatted</b> <font color="red">html</font>',
description=''
)
text = widgets.Text(
value='A text field',
description='text field'
)
textarea = widgets.Textarea(
value='A text area for longer texts',
description='text area'
)
# show the three together in a VBox (vertical box container)
widgets.VBox([label, html, text, textarea])
The cell below shows a few common selection widgets, some of which we met before. The IntRangeSlider is like an IntSlider, but as the name implies, it allows the selection of an lower and upper bound of a range. RadioButtons allow the selection of single value from a list of options, similar to the dropdown list. Checkboxes are displayed a little differently with their description on the right, but still indented. The indent can be removed by passing the argument indent=False
.
A personal favorite is the combobox at the end, which starts showing a list of matching possibilities as one starts typing.
In [10]:
int_slider = widgets.IntSlider(
value=5,
min=0, max=10, step=1,
description='slider'
)
int_range_slider = widgets.IntRangeSlider(
value=(20, 40),
min=0, max=100, step=2,
description='range slider'
)
dropdown = widgets.Dropdown(
value='feb',
options=['jan', 'feb', 'mar', 'apr'],
description='dropdown'
)
radiobuttons = widgets.RadioButtons(
value='feb',
options=['jan', 'feb', 'mar', 'apr'],
description='radio buttons'
)
combobox = widgets.Combobox(
placeholder='start typing... (e.g. L or o)',
options=['Amsterdam', 'Athens', 'Lisbon', 'London', 'Ljubljana'],
description='combo box'
)
checkbox = widgets.Checkbox(
description='checkbox',
value=True
)
# a VBox container to pack widgets vertically
widgets.VBox(
[
int_slider,
int_range_slider,
dropdown,
radiobuttons,
checkbox,
combobox,
]
)
As before, making and displaying widgets is great, but putting them to work is awesome. Below, we show an example of an application similar to the one above: a sine and a slider (just the one this time). This example shows how we can use the observe
method to connect a function to a widget trait. Traits
are special properties that come from a parent class called HasTraits
. To look into this in further detail, check out the traitlets
library.
The value
property of a widget is such a trait
, meaning we can use observe
to connect a callback function, which will get called every time value
changes. The next cell shows an example, where the frequency of a sine is connected to a slider. Note the continuous_update
option when creating the IntSlider. This option, also available in other widgets, makes sure the callback is only called when making changes is done (e.g. mousebutton release, enter), and not on every value traversed along the way.
In [11]:
x = np.linspace(0, 2 * np.pi, 100)
fig, ax = plt.subplots()
line, = ax.plot(x, np.sin(x))
ax.grid(True)
def update(change):
line.set_ydata(np.sin(change.new * x))
fig.canvas.draw()
int_slider = widgets.IntSlider(
value=1,
min=0, max=10, step=1,
description='$\omega$',
continuous_update=False
)
int_slider.observe(update, 'value')
int_slider
When the value of the slider changes, the callback function is called with a single argument, change
. The next cell shows an example for a slider with a callback that only prints its input argument. As you can see, change
is a dictionary-like object with several items:
'name': the name of the trait 'old': the previous value of the trait 'new': the current value of the trait 'owner': the widget owning the trait (here, int_slider)
In [12]:
def show_change(change):
display(change)
int_slider = widgets.IntSlider(value=7, min=0, max=10)
int_slider.observe(show_change, 'value')
int_slider.value = 6
Widgets can also be linked together using the link
function. This function takes two tuples of the form (widget, trait) and links the given traits of the given widgets. In the example below, value
on the first slider is connected to min
on the second. In this way, when we change the value of the first, the minimum value of the second is updated correspondingly.
There are also a dlink
and jslink
function doing a similar thing. dlink
works in one direction only, i.e. updating slider 1 would update slider 2, but not the other way around. jslink
only works in the front-end, in JavaScript, and does not need a live ipykernel to work (see more in these docs).
Below, the two sliders are initialised with the same min
and max
values. However, after linking them together, updating the value
of the first to 5 automatically updates min
for the second as well.
In [13]:
sl1 = widgets.IntSlider(description='slider 1', min=0, max=10)
sl2 = widgets.IntSlider(description='slider 2', min=0, max=10)
link = widgets.link(
(sl1, 'value'),
(sl2, 'min')
)
sl1.value = 5
widgets.VBox([sl1, sl2])
Links can be removed using the unlink method on the link object link.unlink()
.
There are various ways to organise widgets in an interface. We can use boxes, tabs, accordion, or a templated layout. Here, we will only look at boxes. To see the other options, please check here and here.
First, we create some buttons to play with.
In [14]:
b1 = widgets.Button(description='button 1')
b2 = widgets.Button(description='button 2')
b3 = widgets.Button(description='button 3')
Using HBox
and VBox
widgets, we can easily present our buttons in a row or column layout.
In [15]:
widgets.HBox([b1, b2, b3])
In [16]:
widgets.VBox([b1, b2, b3])
These boxes can also be nested to create more complicated layouts. Below, we create two VBoxes. One containing buttons and another containing a dropdown and some radiobuttons. Then we put the VBoxes themselves into an HBox to lay them out next to one another.
In [17]:
def make_boxes():
vbox1 = widgets.VBox([widgets.Label('Left'), b1, b2])
vbox2 = widgets.VBox([widgets.Label('Right'), dropdown, radiobuttons])
return vbox1, vbox2
vbox1, vbox2 = make_boxes()
widgets.HBox([vbox1, vbox2])
To make the left and right boxes more visible, we add some layout through the Layout
widget. Note that the syntax for setting layout parameters resembles css. For our box layout, we add a solid, 1px thick red border. In addition, we add some space in the form of a margin (spacing to other widgets) and padding (spacing between border and widgets inside). Instead of having a margin-top, margin-left and so on, the margin and padding are given as a single string with the values in the order of top, right, bottom & left.
In [18]:
box_layout = widgets.Layout(
border='solid 1px red',
margin='0px 10px 10px 0px',
padding='5px 5px 5px 5px')
vbox1, vbox2 = make_boxes()
vbox1.layout = box_layout
vbox2.layout = box_layout
widgets.HBox([vbox1, vbox2])
This code can have some suprising behavior. The Layout
object is mutable and two boxes share a single instance. Hence, making changes to the layout of box1 will also be reflected in box2. For example, see what happens when we change the width and colour of vbox1.
In [19]:
box_layout = widgets.Layout(
border='solid 1px red',
margin='0px 10px 10px 0px',
padding='5px 5px 5px 5px')
vbox1, vbox2 = make_boxes()
vbox1.layout = box_layout
vbox2.layout = box_layout
# change vbox1 only?
vbox1.layout.width = '400px'
vbox1.layout.border = 'solid 2px green'
widgets.HBox([vbox1, vbox2])
A simple workaround is to put the layout in a function that returns a freshly created instance, so that every widget gets its very own layout object. Then, when making changes to vbox1, vbox2 will not change.
In [20]:
def make_box_layout():
return widgets.Layout(
border='solid 1px red',
margin='0px 10px 10px 0px',
padding='5px 5px 5px 5px'
)
vbox1, vbox2 = make_boxes()
vbox1.layout = make_box_layout()
vbox2.layout = make_box_layout()
# really change vbox1 only
vbox1.layout.width = '400px'
vbox1.layout.border = 'solid 2px green'
widgets.HBox([vbox1, vbox2])
As mentioned at the start of this section, there are other options to design more advanced applications. An interesting alternative is the AppLayout
widget, which facilitates building a classic application layout using a column layout sandwiched between a header and footer. Head over to the offical docs for some examples.
In the next bit, we put it all together and build a simple application. We will create a matplotlib figure again, but this time inside an Output
widget. Output
can take all kinds of input and display the notebook. By creating the figure inside the output context, it will not be drawn until the output widget is used. We can also display the same figure in multiple places, which is sometimes useful in larger applications.
In [21]:
output = widgets.Output()
# create some x data
x = np.linspace(0, 2 * np.pi, 100)
# default line color
initial_color = '#FF00DD'
with output:
fig, ax = plt.subplots(constrained_layout=True, figsize=(6, 4))
# move the toolbar to the bottom
fig.canvas.toolbar_position = 'bottom'
ax.grid(True)
line, = ax.plot(x, np.sin(x), initial_color)
No figure is shown yet, until we use the output widget:
In [22]:
output
Note that we used the constrained_layout
when creating the figure. This is similar to the figure’s tight_layout
method, and makes space for the axis labels. However, constrained_layout
is more convenient in combination with the widget matplotlib backend, as it can be applied before the figure is rendered. With tight_layout
, we would first have to show the figure and then call the method to make everything fit. It is an experimental feature though, so use with care: ‘constrained_layout‘.
Next, we create control widgets with their callback functions and connect them. After getting the callbacks in place, we set the default values for the labels through their corresponding widgets.
In [23]:
# create some control elements
int_slider = widgets.IntSlider(value=1, min=0, max=10, step=1, description='freq')
color_picker = widgets.ColorPicker(value=initial_color, description='pick a color')
text_xlabel = widgets.Text(value='', description='xlabel', continuous_update=False)
text_ylabel = widgets.Text(value='', description='ylabel', continuous_update=False)
# callback functions
def update(change):
"""redraw line (update plot)"""
line.set_ydata(np.sin(change.new * x))
fig.canvas.draw()
def line_color(change):
"""set line color"""
line.set_color(change.new)
def update_xlabel(change):
ax.set_xlabel(change.new)
def update_ylabel(change):
ax.set_ylabel(change.new)
# connect callbacks and traits
int_slider.observe(update, 'value')
color_picker.observe(line_color, 'value')
text_xlabel.observe(update_xlabel, 'value')
text_ylabel.observe(update_ylabel, 'value')
text_xlabel.value = 'x'
text_ylabel.value = 'y'
Finally, we box everything up and display everything together.
In [24]:
controls = widgets.VBox([int_slider, color_picker, text_xlabel, text_ylabel])
widgets.HBox([controls, output])
To create more high level components, we can also subclass a container and build up our gui from within. Containers have a children
property to which we can assign a list of widgets that should be displayed. Although we can assign a list, this is turned into a tuple and cannot be modified afterwards. To remove or add a widget at runtime, the children tuple can be turned back into a list, followed by an insert or deletion and finalised by reassigning to the children property. Since it can be easy to make mistakes when going by index, we tend to add a placeholder box in which we only place the ‘dynamic’ widget.
The example below packs the entire oscilliscope ‘dashboard’ in a single component by subclassing VBox
. All the required widgets are defined in the Sines class and added as its children
. The callbacks are defined as instance methods. It may not be a masterpiece in object oriented programming, but hopefully it shows the idea of constructing larger reusable components. Note that we need to call super().__init__()
from __init__
to properly initialise the parent class.
In [25]:
def make_box_layout():
return widgets.Layout(
border='solid 1px black',
margin='0px 10px 10px 0px',
padding='5px 5px 5px 5px'
)
class Sines(widgets.HBox):
def __init__(self):
super().__init__()
output = widgets.Output()
self.x = np.linspace(0, 2 * np.pi, 100)
initial_color = '#FF00DD'
with output:
self.fig, self.ax = plt.subplots(constrained_layout=True, figsize=(5, 3.5))
self.line, = self.ax.plot(self.x, np.sin(self.x), initial_color)
self.fig.canvas.toolbar_position = 'bottom'
self.ax.grid(True)
# define widgets
int_slider = widgets.IntSlider(
value=1,
min=0,
max=10,
step=1,
description='freq'
)
color_picker = widgets.ColorPicker(
value=initial_color,
description='pick a color'
)
text_xlabel = widgets.Text(
value='',
description='xlabel',
continuous_update=False
)
text_ylabel = widgets.Text(
value='',
description='ylabel',
continuous_update=False
)
controls = widgets.VBox([
int_slider,
color_picker,
text_xlabel,
text_ylabel
])
controls.layout = make_box_layout()
out_box = widgets.Box([output])
output.layout = make_box_layout()
# observe stuff
int_slider.observe(self.update, 'value')
color_picker.observe(self.line_color, 'value')
text_xlabel.observe(self.update_xlabel, 'value')
text_ylabel.observe(self.update_ylabel, 'value')
text_xlabel.value = 'x'
text_ylabel.value = 'y'
# add to children
self.children = [controls, output]
def update(self, change):
"""Draw line in plot"""
self.line.set_ydata(np.sin(change.new * self.x))
self.fig.canvas.draw()
def line_color(self, change):
self.line.set_color(change.new)
def update_xlabel(self, change):
self.ax.set_xlabel(change.new)
def update_ylabel(self, change):
self.ax.set_ylabel(change.new)
Sines()
There is a lot more to ipywidgets than was presented here. A good first start are the official ipywidgets and traitlets docs. There is a lot of active development, so it is always interesting to check for updates. There is also a lot of ongoing work on ipympl
, so staying up to date is a good idea when using it.
Two other projects that we would like to mention are Voila and ipyvuetify. The Voila project makes it possible to present a notebook as an interactive web application with a live kernel. Ipyvuetify provides a great set of widgets based on vuetify (example in binder).
Subscribe to our newsletter and stay up to date.