• SSC Lunch Time Python
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Lunch Time Python #8: ipywidgets¶

Jupyter Notebooks are a perfect fit for scientific work with Python. They combine the following elements:

  • Code
  • Documentation
  • Visualization
  • UI Controls

This allows us to write scientifically meaningful, executable documents that contain results, their interpretation and their provenance. They are a key element for reproducible research.

What are widgets?¶

Jupyter has a so-called rich display system. If Python code returns an object, Jupyter accesses special methods on the object to decide how to display it. This can involve pretty printing, HTML, images, video, sounds etc:

In [1]:
from PIL import Image
from io import BytesIO
import requests
In [2]:
response = requests.get(
    "https://ssciwr.github.io/lunch-time-python/lunchtime5/thingstaette.png"
)
img = Image.open(BytesIO(response.content))
In [3]:
?img._repr_png_
In [4]:
img
Out[4]:
No description has been provided for this image

ipywidgets provides a number of widgets that are Python objects that display as HTML. The interactive behaviour of this HTML snippet is implemented in JavaScript and uses callback functions in Python. This way, you write interactive notebooks with pure Python.

In [5]:
import ipywidgets
In [6]:
button = ipywidgets.Button(description="Click Me!")
In [7]:
button
Out[7]:
In [8]:
def handler(change):
    button.description = "Thanks!"
In [9]:
button.on_click(handler)

Input widgets (I)¶

We can create simple input fields that allow users to put in data. We can then access that data from Python reading and writing:

In [10]:
widget = ipywidgets.Text()
In [11]:
widget
Out[11]:
In [12]:
widget.value
Out[12]:
''
In [13]:
widget.value = "Test"

Input widgets (II)¶

Many similar working subflavors exist (for a full list see the docs):

In [14]:
ipywidgets.FloatText(value=42.0, step=0.01)
Out[14]:
In [15]:
ipywidgets.IntSlider(min=-10, max=10)
Out[15]:
In [16]:
ipywidgets.Checkbox(value=True, description="Some Option")
Out[16]:

Selection widgets¶

In [17]:
widget = ipywidgets.Dropdown(options=["Model A", "Model B", "Model C"])
In [18]:
widget
Out[18]:
In [19]:
widget.value
Out[19]:
'Model A'
In [20]:
ipywidgets.RadioButtons(options=["Model A", "Model B", "Model C"])
Out[20]:
In [21]:
ipywidgets.Select(options=["Linux", "Windows", "macOS"], description="OS:")
Out[21]:

Container widgets¶

If multiple widgets should be placed together, possibly applying some styling, they can be grouped into container widgets. In contrast to other widgets, these do not have an accessible value, but some have selected_index:

In [22]:
widgets = [ipywidgets.Text(value=f"#{i}") for i in range(4)]
In [23]:
ipywidgets.HBox(children=widgets)
Out[23]:
In [24]:
ipywidgets.VBox(children=widgets)
Out[24]:
In [25]:
ipywidgets.Accordion(children=widgets, titles=tuple(f"Tab #{i}" for i in range(4)))
Out[25]:
In [26]:
tab = ipywidgets.Tab(children=widgets, titles=tuple(f"Tab #{i}" for i in range(4)))
In [27]:
tab
Out[27]:
In [28]:
tab.selected_index
Out[28]:
0

Putting things together¶

In [29]:
import io


def img_to_widget(i):
    membuf = io.BytesIO()
    i.save(membuf, format="png")
    return ipywidgets.Image(value=membuf.getvalue(), format="png")
In [30]:
img_widget = img_to_widget(img)
cropped_widget = img_to_widget(img)
In [31]:
x0 = ipywidgets.IntText(value=0, layout=ipywidgets.Layout(width="100px"))
y0 = ipywidgets.IntText(value=0, layout=ipywidgets.Layout(width="100px"))
x1 = ipywidgets.IntText(value=img.size[0], layout=ipywidgets.Layout(width="100px"))
y1 = ipywidgets.IntText(value=img.size[1], layout=ipywidgets.Layout(width="100px"))
In [32]:
controls = ipywidgets.VBox(
    children=[
        ipywidgets.VBox(children=[ipywidgets.Label("Upper left:"), x0, y0]),
        ipywidgets.VBox(children=[ipywidgets.Label("Lower right:"), x1, y1]),
    ]
)
In [33]:
def crop_handler(_):
    cropped_widget.value = img_to_widget(
        img.crop([x0.value, y0.value, x1.value, y1.value])
    ).value


x0.observe(crop_handler, names="value")
y0.observe(crop_handler, names="value")
x1.observe(crop_handler, names="value")
y1.observe(crop_handler, names="value")
In [34]:
app = ipywidgets.AppLayout(
    left_sidebar=controls,
    center=img_widget,
    right_sidebar=cropped_widget,
    pane_widths=(1, 2, 2),
)
In [35]:
app
Out[35]:

A simple alternative - interact¶

ipywidgets contains a much simpler interface that automatically creates widgets for you. You simply need to annotate ("decorate") a function that does something and you will get a continuously updated interactive version:

In [36]:
@ipywidgets.interact(x=(0, 100), y=(0, 100))
def add(x, y):
    return x + y

Notably, this does not change the function nature of add. It is merely displaying a UI as a side effect of the function definition:

In [37]:
?add

ipywidgets.interact has many more options and flavors. Here are some:

In [38]:
@ipywidgets.interact(
    operation=[("add", 1.0), ("subtract", -1.0)],
    rounding=False,
    x=(0, 100, 0.1),
    y=(0, 100, 0.1),
)
def op(operation, rounding, x, y):
    val = x * operation + y
    if rounding:
        return round(val)
    else:
        return val
In [39]:
import time
In [40]:
@ipywidgets.interact_manual(x=(0, 100), y=(0, 100))
def slow_add(x, y):
    time.sleep(1)
    return x + y

More information¶

For more information, see the ipywidgets documentation:

https://ipywidgets.readthedocs.io