How to use ipywidgets to make your Jupyter notebook interactive

By Matt Wright

Have you ever created a Python-based Jupyter notebook and analyzed data that you want to explore in a number of different ways? For example, you may want to look at a plot of data, but filter it ten different ways. What are your options to view these ten different results?

  1. Copy and paste a cell, changing the filter for each cell, then executing the cell. You will end up with ten different cells with ten different values.
  2. Modify the same cell, execute it and view the results, then modify it again, ten times.
  3. Parameterize the notebook (perhaps using a tool like Papermill) and execute the entire notebook with ten different sets of parameters.
  4. Some combination of the above.

These all are non-ideal if we want quick interaction and the ability to explore the data. Those options are also prone to typing errors or lots of extra editing work. They may work great for the original developer of a notebook, but allowing a user who doesn’t undestand Python syntax to modify variables and re-execute cells may not be the best option. What if you could just give the user a simple form, with a button, and they could modify the form and see the results they want?

It turns out you can do this pretty easily right in Jupyter, without creating a full webapp. This is possible with ipywidgets, also known just as widgets. I’ll show you the basics in this article of building a few simple forms to view and analyze some data.

Jupyter widgets are special bits of code that will embed JavaScript and html in your notebook and present a visual representation in your brower when executed in a notebook. These components allow a user to interact with the widgets. The widgets can execute code on certain actions, allowing you to update cells without a user having to re-execute them or even modify any code.

Getting started

First, you need to make sure that ipywidgets is installed in your environment. This will depend a bit on which Jupyter environment you are using. For older Jupyter and JupyterLab installs, make sure to check the details in the docs. But for a basic install, just use pip

pip install ipywidgets

or for conda

conda install -c conda-forge ipywidgets

This should be all that you need to do in most situations to get things running. 

Example

Instead of going through all the widgets and getting into details right away, let’s grab some interesting data and explore it manually. Then we’ll use widgets to make a more interactive version of some of this data exploration. Let’s grab some data from the Chicago Data Portal – specifically their dataset of current active business licenses. Note that if you just run the code as below, you’ll only get 1000 rows of data. Check the documentation on how to to grab all the data.

Note: all of this code was written in a Jupyter notebook using Python 3.8.6. While this article shows the output, the best way to experience widgets is to interact with them in your own environment. You can download a notebook of this article here.

import pandas as pd
df = pd.read_csv('https://data.cityofchicago.org/resource/uupf-x98q.csv')
df[['LEGAL NAME', 'ZIP CODE', 'BUSINESS ACTIVITY']].head()

As we can see from the data, the business activity is pretty verbose, but the zip code is an easy way to do some simple searches and filters of data. For our smaller data set, let’s just grab the zip codes that have 20 or more businesses.

zips = df.groupby('ZIP CODE').count()['ID'].sort_values(ascending=False)
zips = list(zips[zips > 20].index)
zips
[60618, 60622, 60639, 60609, 60614, 60608, 60619, 60607]

Now, a reasonable scenario for filtering data might be create a report filtering by zip code, showing the legal name and address of a business, ordered by expiration date of the license. This would be a pretty simple (even if somewhat messy) expression in pandas. For example, in this data set we can take the top zip code and look at a few columns like this.

df.loc[df['ZIP CODE'] == zips[0]].sort_values(by='LICENSE TERM EXPIRATION DATE', ascending=False)[['LEGAL NAME', 'ADDRESS', 'LICENSE TERM EXPIRATION DATE']]

Now what if someone wanted to be able to run this report for different zip codes, looking at different columns, and sorting by other columns? The user would have to be comfortable editing the cell above, rerunning it, and maybe executing other cells to look for the column names and other values.

Instead, we can use widgets to make a form that allows this interaction to be executed visually. In this article you will learn enough about widgets to build a form and dynamically show the results.

Since most of us are familiar with forms in our web browsers, it makes sense to think about widgets as parts of typical forms. Widgets can represent numerical, boolean, or text values. They can be selectors of pre-existing lists, or can accept free text (or password text). You can also use them to display formatted output or images. The full list of widgets describes them in more detail. You can also create your own custom widgets, but for our purposes, we will be able to do all the work with standard widgets.

A widget is just an object that can be displayed in a Jupyter notebook once created. It will render itself (and its underlying content) and (possibly) allow user interaction.

Making a form

For our form, we will need to gather four pieces of information:

  1. The zip code to filter
  2. The column to sort on
  3. Whether the sort is ascending or descending
  4. The columns to display.

These four pieces of information will be captured by the following form elements:

  1. A selection dropdown
  2. A selection dropdown
  3. A checkbox
  4. A multi-selection list

These three widgets will provide a quick intro to widgets, and once you know how to instantiate and use one widget, the others are quite similar. Before we can create a widget, we need to import the library. Let’s look at dropdowns first.

import ipywidgets as widgets widgets.Dropdown( options=zips, value=zips[0], description='Zip Code:', disabled=False,
)
Of course, just creating an object doesn’t allow us to use it, so we need to assign it to a variable, and the display function can be used to render it, the same as we see above.
zips_dropdown = widgets.Dropdown( options=zips, value=zips[0], description='Zip Code:', disabled=False,
) display(zips_dropdown)

We can easily do the same for the columns.

columns_dropdown = widgets.Dropdown( options=df.columns, value=df.columns[4], description='Sort Column:', disabled=False,
) display(columns_dropdown)
And for boolean values, you have a few options. You can do a CheckBox or ToggleButton. I’ll go with the first.
sort_checkbox = widgets.Checkbox( value=False, description='Ascending?', disabled=False)
display(sort_checkbox)
Finally for this example, we want to be able to select all the columns we want to see in the output. We’ll use a SelectMultiple for that. Note that if you use the shift and ctrl (or Command on a Mac) keys to select multiple options.
columns_selectmultiple = widgets.SelectMultiple( options=df.columns, value=['LEGAL NAME'], rows=10, description='Visible:', disabled=False
)
display(columns_selectmultiple)
Last, we will show a button that we can click to force updates. (Note that we won’t end up needing this in the end, there’s a simpler way to interact with our elements, but buttons can be useful for many situations).
button = widgets.Button( description='Run', disabled=False, button_style='', # 'success', 'info', 'warning', 'danger' or '' tooltip='Run report', icon='check' # (FontAwesome names without the `fa-` prefix)
)
display(button)

Handling output

Before we hook our button up to a function, we need to make sure we can capture the output of our function. If we want to view a DataFrame, or print text, or log some information to stdout, we need to be able to capture that information and clear it, if necessary. This is what the Output widget is for. Note that you don’t have to use an output widget, but if you want your output to appear in a certain cell, you will need to use this. The cell where the Output widget is displayed will render the results.

out = widgets.Output(layout={'border': '1px solid black'})

Hooking it all up

Now that we’ve generated all our user interface components, how do we display them all in one spot and hook them up to generate actions? 

First, let’s create a simple layout with all the items together.

box = widgets.VBox([zips_dropdown, columns_dropdown, sort_checkbox, columns_selectmultiple, button])
display(box)

Handling events

For widgets that can produce events, you can provide a function that will receive the event. For a Button, the event is on_click, and it requires a function that will take a single argument, the Button itself. If we use the Output we created above (as a context manager using a with statement), clicking the button will cause the text “Button clicked” to be appended to the cell output. Note that the cell that receives the output will be the one where the Output was rendered.

def on_button_clicked(b): with out: print("Button clicked.") button.on_click(on_button_clicked, False)

A better way to hook things up

The above example is simple, but doesn’t show us how we’d get the values from the other inputs. Another way to do that is to use interact. It works as both a function or a function decorator to automatically create widgets that allow you to interactively change the inputs to a function. Based on the named argument type, it will generate a widget that allows you to change that value. Using interact is a quick way to provide user interaction around a function. The function will be called each time a widget is updated. As you move the slider, the square of the number will be printed if the checkbox is checked, and the number will just be printed unchanged otherwise.

def my_function2(x, y): if y: print(x*x) else: print(x) interact(my_function2,x=10,y=False);

Note that you can provide more information to interact to provide more appropriate user interface elements (see the docs for examples). But since we already made widgets, we could just use those instead. The best way to do that is to use another function, interactive.  interactive is like interact, but allows you to interact with the widgets that were created (or supply them directly), and to display values when you want. Since we already made some widgets, we can just let interactive know about them by providing each of them as keyword arguments. The first argument is a function, and that function’s arguments need to match the subsequent keyword arguments to interactive. Each time we change one of the values in the form, the function will be invoked with the values from the form widgets. With just a few lines of code, we now have an interactive tool for looking at and filtering this data.

But first, I’ll make a cell with an output to receive the display.

report_output = widgets.Output()
display(report_output)
from ipywidgets import interactive def filter_function(zipcode, sort_column, sort_ascending, view_columns): filtered = df.loc[df['ZIP CODE'] == zipcode].sort_values(by=sort_column, ascending=sort_ascending)[list(view_columns)] with report_output: report_output.clear_output() display(filtered) interactive(filter_function, zipcode=zips_dropdown, sort_column=columns_dropdown, sort_ascending=sort_checkbox, view_columns=columns_selectmultiple) 

Now, the same form created earlier above is rendered in the cell. The output will appear in whichever cell the display(report_output) line was executed. As you modify any of the form elements, the resulting filtered DataFrame will be displayed in that cell.

This has been just a quick overview of using ipywidgets to make Jupyter notebooks more interactive. Even if you are comfortable editing Python code and re-executing cells to update and explore data, widgets may be a great way to make that exploration more dynamic and convenient, along with being less error prone. If you need to share notebooks with people who are not comfortable editing Python code, widgets can be a lifesaver and really help the data come alive.

Just reading about these widgets is not nearly as interesting as running examples and working with them yourself. Give these examples a try and then try using widgets in your own notebooks.