Build custom widgets with ipywidgets and plotly

Jacky Kaub
8 min readJan 8, 2023

--

Photo by Jon Tyson on Unsplash

Starting a new data project can be an exciting but also challenging experience. Making effective data exploration is often a key factor in the success of a project, but finding the best tools for the job can be a challenge.

In this article, we will explore the use of ipywidgets and plotly to setup in a few lines of code widgets usable directly in your notebooks. It is an introduction on how to build interactive notebooks, and is the foundation to explore more relevant use cases in greater detail in future articles.

The ipywidgets library

Ipywidgets is a powerful tool for building interactive data applications such as a small dashboard or a labeling tool. It consists of a set of interactive elements like sliders, buttons, and text boxes that can be combined to create interactive widgets usable directly in a Jupyter Notebook. It is also compatible with data visualisation tools (like plotly or matplotlib) and dataframes, bringing interactivity to data exploration.

Basic UI elements

In practice, each interactive element of the widget is made of two components: a basic user interface element (like a button or dropdown menu) and a callback function that describes what happens when the element is interacted with.

The example below shows the code for a simple button and its callback.

from ipywidgets.widgets import Button

#Define a button
say_hello_button = Button(description='Say "hello!"')

#Define the interaction that happened when the button is clicked
def callback_button(button):
print("hello world!")

#bind the function to "on_click" callback of the Button
say_hello_button.on_click(callback_button)

#display the widget
display(say_hello_button)
Hello world !

Another example with the Dropdown widget:

from ipywidgets.widgets import Dropdown

# A dropdown display a list of options from which we can select one
options = ["foo","bar","foobar"]
selection_dropdown = Dropdown(options=options)

# The callback for the dropdown listen to a change within the dropdown.
# Specifying "names=value" will listen to changes of the value of the list.
# The change variable is a dictionnary containing old and new values.
def dropdown_callback(change):
#print previous and new value
old, new = change["old"], change["new"]
print(f"{old} -> {new}")

#Bind the callback to the dropdown
selection_dropdown.observe(dropdown_callback, names="value")
display(selection_dropdown)
Dropdown in action

Figure widgets

Plotly’s FigureWidget is a high-level interface to plotly that is compatible with the ipywidgets framework. The FigureWidget can be used directly within a widget in conjunction with other UI element to build advanced user interfaces.

The FigureWidget provides many callbacks to add custom interactions to the figure, such as:

  • figure.on_click: trigger a function when a user click on the figure
  • figure.on_double_click: trigger a function when a user double click on the figure
  • figure.on_hover: trigger a function when the user over an element of the fig
  • and so on…

Let’s illustrate this with a callback changing the color of a point when clicking on it.

We start by create a simple FigureWidget and display it:

import plotly.graph_objects as go
import numpy as np

n = 50 #number of points
colors = ["black", "red"] #colors to use
x,y = np.random.random(n),np.random.random(n) #defining random points
c = np.zeros(n).astype(int) #initializing colors to black

#Figure widget
fig = go.FigureWidget()

fig.add_trace(
go.Scatter(
x = x,
y = y,
marker = {"color":[colors[k] for k in c]},
mode = "markers"
)
)

fig.update_layout(template = "presentation", title="Our first interactif chart")

display(fig)
A chart, not yet interactive

Now we can add interactivity.

To do this, we’ll select the trace of interest and add a relevant callback function.

In this case our callback function will simply take the index of the point clicked, and revert its color.

import plotly.graph_objects as go
import numpy as np

n = 50 #number of points
colors = ["black", "red"] #colors to use
x,y = np.random.random(n),np.random.random(n) #defining random points
c = np.zeros(n).astype(int) #initializing colors to black

#Figure widget
fig = go.FigureWidget()

fig.add_trace(
go.Scatter(
x = x,
y = y,
marker = {"color":[colors[k] for k in c]},
mode = "markers"
)
)

# ******** ADDED TO PREVIOUS CODE ********
def change_color_on_click(trace, points, state):
"""Our callback. 'points' contains information such as the index,
x and y values
The function will be trigger every time the user click on a point.
"""

#Let's retrieve the index of the point of interest from "points"
idx = points.point_inds[0]

#Modify the color of that index
c[idx]= 1-c[idx]

#Finaly, update the trace, using the batch_update method allowing us to modify multiple parameters at once
with fig.batch_update():
trace.marker["color"]=[colors[k] for k in c]

#Next we get the trace to modify, and modify its "on_click" callback
trace = fig.data[0]
trace.on_click(change_color_on_click)
# ******** ADDED TO PREVIOUS CODE ********

fig.update_layout(template = "presentation", title="Our first interactif chart")

display(fig)
The chart is now interactive

Widget Layout

We have interactive UI elements, we have interactive figures. The final piece we need to build our widget is a custom layout. Ipywidgets provides several types of containers, including HBox (horizontal layout), VBox (vertical layout), and GridBox (grid layout), among others.

The example below shows how to combine, for example, VBox and HBox to organize our layout.

#Set some UI component without interactions for the demo
button1 = Button(description = "Button 1")
button2 = Button(description = "Button 2")
button3 = Button(description = "Button 3")
dropdown = Dropdown(options = ['foo','bar','goo'])

#Create a horizontal box to display the 3 buttons next to each other
buttons_widget = HBox([button1,button2,button3])

#Create a vertical box containing the dropdown component, the previous hbox,
#and our plotly figure
widget = VBox([dropdown, buttons_widget, fig])

display(widget)
An example of widget layout

Combining all together

Let’s combine everything that we learnt so far by creating a more complicated widget that allow users to set the color of points in a chart.

To keep things organized, we’ll package the code in a class that manages the state of variables internally, and we will build the widget step by step.

class Widget:
'''Out widget will take a list of color "colors" as well as x and y values
to build a scatter plot'''

def __init__(self, x, y , colors):

self._x = x
self._y = y
self._colors = colors
self._current_color = colors[0]

Initiate the figure widget

Let’s first build the initial FigureWidget containing our x and y, with all points having the default color of the first element in the list. The figure is not yet interactive, but we’ll add a callback function later.

def _initialise_figure(self):

'''This method initiate the FigureWidget'''

self._fig = go.FigureWidget()
self._fig.add_trace(
go.Scatter(
x = self._x,
y = self._y,
mode = "markers",
marker = {"color":[self._current_color for e in self._x]}
)
)
self._fig.update_layout(template="presentation")
#todo: add a on_click callback

Initiate the buttons

We want one button per color in the list. When a button is clicked, it becomes the active button and selects the active color. The active button should be highlighted with a different color, which can be controlled using the button_style parameter.

We will come back on the interactions later, let’s focus on building the buttons for now.

def _build_single_button(self, color):
'''This function build a button with a given color'''
button = Button(description = color)
#The button with the active color has a special style to highlight the active color
if color == self._current_color:
button.button_style = "success"

#Todo: add the on_click callback
return button

def _build_buttons_widget(self):
'''This function build a widget with all the buttons, stacked horizontally'''
#Create a list of button with each color
self._buttons = [self._build_single_button(color) for color in self._colors]
#Build a horizontal widget out of the list of buttons
self._buttons_widget = HBox(self._buttons)

Initiate the global layout of the widget

We will create a last function to create the final widget layout containing the figure and the buttons in a VBox. This function will be called when the class is initialized, creating a layout with all the UI elements.

def _build_layout(self):
'''This function build the widget layout with the different components'''
#Build the buttons widget
self._build_buttons_widget()
#Build the figure widget
self._initialise_figure()
#build the final widget
self._widget = VBox([self._buttons_widget, self._fig])

def display(self):
'''A little function to display the widget'''
display(self._widget)

We can now display the widget, which is not yet interactive. This is what the entire code should look like:

#Initialise some random x and y
n = 30
x,y = np.random.random(n),np.random.random(n)
#Initialise some colors
colors = ["black", "red", "blue","orange"]
#Initialise the widget
widget = Widget(x,y,colors)
#Visualise the widget
widget.display()
The initial widget, not interactive

Create the button on_click callback

Clicking on a button will have two effects:

  • It will set the current color to the color of the button
  • It will modify the color style of the buttons to highlight the current active color

First let’s create the on_click callback function for the button:

def _click_button_function(self, color, button):
'''This function is triggered everytime a user press the button
It will change the state of the _current_color variable and
Modify the color of the buttons
'''
#Set the current color to the color of the button
self._current_color = color

#Set the style of all button to default style
for button_ in self._buttons:
button_.button_style = ""

#Set active button style to "success"
button.button_style = "success"

You should notice here that unlike the example in the part I, our on_click callback function takes additional parameters (and not only the button object). In order to pass properly our function to the on_click callback with the color input, we need to use a lambda function.

    def _build_single_button(self, color):
'''This function build a button with a given color'''
button = Button(description = color)
#The button with the active color has a special style to highlight the active color
if color == self._current_color:
button.button_style = "success"

# ***** NEW CODE ******
button.on_click(lambda button: self._click_button_function(color, button))
# *** END NEW CODE ****
return button

Create the FigureWidget on_click callback

The callback for changing the color of a point is very similar to the one exposed in the previous part, but it now modifies the color of the clicked point based on the internal state of the widget.

def _change_color_on_click(self, trace, points, state):
"""Our callback. 'points' contains information such as the index,
x and y values
The function will be trigger every time the user click on a point.
"""
#Let's retrieve the index of the point of interest from "points"
idx = points.point_inds[0]

#Finaly, update the trace, using the batch_update method allowing us to modify multiple parameters at once
with self._fig.batch_update():
trace.marker["color"]=[c if i!=idx else self._current_color for i,c in enumerate(trace.marker["color"])]

The callback function is attached to the relevant figure trace as shown previously.

The widget in action

Our widget is now ready to be tried out. This is how the full code should look like:

Our widget in action

You can find the full code of today’s article here.

Conclusion

To conclude, ipywidgets and plotly are powerful tools for building interactive data applications in Jupyter notebooks.

By combining different UI elements and interactive figures, you can build advanced user interfaces and explore data in more efficient ways. The techniques described in this article provide a foundation for building a wide range of interactive data applications, and we’ve just covered some basic use cases.

In the next article, we will look at how to prototype a fully-integrated image segmentation tool with the same elements we used today.

--

--