Jupyter notebook has become very famous nowadays and has been used by data scientists, researchers, students, developers worldwide for doing data analysis. Interactive dashboards and applications are getting quite common day by day. It has become a need of an hour to create interactive apps and dashboards so that others can analyze further using interactive widgets. Old ways of creating static applications and dashboards won't help much in this fierce competition of being everything interactive. Applications and dashboards designed as web app quite commonly use javascript for interactivity purposes which requires quite a good amount of javascript learning.
But what if you are a python developer and do not want to invest time in learning another programming language but still want to build interactive apps and dashboards?
Python has a library called ipywidgets
which can help you with that. It provides a list of widgets quite common in web apps and dashboards like dropdown, checkbox, radio buttons, and many more. It'll let you code in pure python and will generate an interactive widget for you using javascript underneath. This way you won't need to learn javascript and continue to use python by just learning ipywidgets
.
Every widget generated by ipywidgets consists of two components behind the scene:
ipywidgets
can be easily integrated with a lot of other Python libraries like matplotlib
, holoviews
, bokeh
, bqplot
, ipyvolume
, ipyleaflet
, ipywebrtc
, ipythreejs
and many more.
ipywidgets provides a list of functions which can let us create widgets UI for any of our existing function. It'll create widgets by itself by looking at parameters of functions and create widgets UI with all parameters represented as one widget. It's a very good way to start using ipywidgets
. We'll then learn in the future about customizing these widgets further.
We'll start by importing necessary functions which will help us creates widgets UI.
from ipywidgets import interact, interactive, fixed, interact_manual
interact()
¶We can pass any of our functions as input to interact()
function and it'll create widgets UI by looking at parameters of function with proper widgets for each parameter. We'll start converting a few basic functions.
def func1(x):
return 5*x
interact(func1, x=10);
We can see that interact() function detects from our value x=10
that it should create a slider for this parameter. We can modify slider values and it'll recall function with new value and return new output.
interact(func1, x=2.2);
interact(func1, x=True);
interact(func1, x="Hello !!");
We can see from the above examples that based on the type of argument value passed to a parameter, it'll create a widget that is best suitable to represent that type of argument values. It created slider for float & integer, a checkbox for boolean, text box for a string.
We can even pass interact
as a decorator and it'll just work the same.
@interact(x=10)
def func1(x):
return 10 * x
fixed()
to Prevent Widgets from Getting Created¶Sometimes we have functions with more than one parameters and we might want to explore just a few parameters fixing values of other parameters. We can do that using fixed()
which will prevent interact()
from creating a widget.
def func2(a,b,c):
return (a*b) + c
interact(func2, a=5, b=5, c=fixed(10));
We can see that the slider is only produced for parameters a
and b
only.
When we pass an integer value to a widget, it creates an integer slider in range(-value, +value *3) with a step value of 1. We can pass more than one value to a parameter in an interactive method to generate a widget according to our min, max values.
interact(func1, x=(1,5));
interact(func1, x=(1,5, 0.5));
Below table gives an overview of different widget abbreviations:
Keyword argument | Widget |
`True` or `False` | Checkbox |
`'Hi there'` | Text |
`value` or `(min,max)` or `(min,max,step)` if integers are passed | IntSlider |
`value` or `(min,max)` or `(min,max,step)` if floats are passed | FloatSlider |
`['good','bad']` or `[('one', 1), ('two', 2)]` | Dropdown |
Please make a note that a dropdown is used if a list or a list of tuples is given with discrete choices, and a slider is used if a tuple is given with a range of values.
interact(func1, x=['good ','bad ']);
interact(func1, x=[('first', 100), ('second', 200)]);
We can also create widget objects and pass them as a parameter to interact function according to our needs. It'll prevent interact from taking decision by itself and create an object which we might not need exactly. Let's start by creating a few widget objects.
ipywidgets
has list of objects for creating widgets like IntSlider
, FloatSlider
, Dropdown
, Text
, Checkbox
, etc. We can pass description
parameter with each widget as it'll create a label next to the widget. We can even pass LateX
as a string to description parameter and it'll create a formula as well.
import ipywidgets as widgets
int_slider = widgets.IntSlider(min=10, max=50, value=25, step=2, description="Integer Slider")
int_slider
float_slider = widgets.FloatSlider(min=10.0, max=50.0, value=25.0, step=2.5, description="Float Slider")
float_slider
Each widget has a list of a parameter which can be accessed by following dot notation on it.
float_slider.value
float_slider.value = 28
We can access all available attributes of a widget by calling keys()
method on it.
print(float_slider.keys)
widgets.Checkbox(value=True, description="Check")
widgets.Button(description="Check")
widgets.Dropdown(options=["A","B","C","D"], description="Select Right Option")
widgets.Textarea(value="Please enter text here..")
We can pass the above-created widgets as a parameter value to interact()
and it'll use those widgets instead of creating widgets by itself. We can prevent the default behavior of interact()
function this way and force it to use our designed widgets according to our needs.
interact(func1, x= widgets.IntSlider(min=10, max=50, value=25, step=2, description="Integer Slider"));
interact(func1, x= widgets.FloatSlider(min=10.0, max=50.0, value=25.0, step=2.5, description="Float Slider"));
interactive()
¶ipywidgets
provides another function called interactive()
to create UI of widgets by passing a function to it. Unlike interact()
function, interactive()
returns objects which does not displays widgets automatically. We need to use IPython
function display()
to display widgets UI as well as the output of a function.
from IPython.display import display
def func3(a,b,c):
display((a+b)^c)
w = interactive(func3, a=widgets.IntSlider(min=10, max=50, value=25, step=2),
b=widgets.IntSlider(min=10, max=50, value=25, step=2),
c=widgets.IntSlider(min=10, max=50, value=25, step=2),
)
display(w)
print(type(w))
The interactive
object is of type VBox
which is a container object of ipywidgets
. VBox
can layout various widgets according to vertical layout. We can access its children as well as arguments.
w.children
w.kwargs
We'll below create a simple example that modifies matplotlib
plot according to the values of widgets. We'll be plotting a simple line with equation y=m*x + c
. Our method will have parametersand
cwhile
x` will be random numbers array.
import matplotlib.pyplot as plt
import numpy as np
def plot(m,c):
x = np.random.rand(10)
y = m *x + c
plt.plot(x,y)
plt.show()
interactive(plot, m=(-10,10, 0.5), c=(-5,5,0.5))
interactive_output
¶interactive_output
lets us layout widgets according to our need. interactive_output
does not generate output UI but it lets us create UI, organize them in a box and pass them to it. This gives us more control over the layout of widgets.
m = widgets.FloatSlider(min=-5,max=5,step=0.5, description="Slope")
c = widgets.FloatSlider(min=-5,max=5,step=0.5, description="Intercept")
# An HBox lays out its children horizontally
ui = widgets.HBox([m, c])
def plot(m, c):
x = np.random.rand(10)
y = m *x + c
plt.plot(x,y)
plt.show()
out = widgets.interactive_output(plot, {'m': m, 'c': c})
display(out, ui)
Please make a not above that UI generated by interact(), interactive(), interactive_output() immediately calls function and updates UI with new output after widget value is changed. This might not be ideal choice as sometimes function can be taking time to calculate its output which might hang UI in case of frequent widget value change.
interact_manual()
and continous_update
¶We can use a function like interact_manual()
for preventing UI updates after widget values are changed. interact_manual()
provides us with button pressing which will run a function after widget value changes with this new value. This will prevent UI from immediately updating and creating fluctuations.
def cpu_intensive_func(i):
from time import sleep
print('Sleeping...')
sleep(1)
print(i)
interact_manual(cpu_intensive_func,i=widgets.FloatSlider(min=1e4, max=1e6, step=1e4));
Another way to delay the update of UI after a change in widget value is by setting the continuous_update
parameter to False
. This will prevent a call to function as long as widget value is changing. Once a person leaves the mouse button, it'll then call a function to update UI with a new widget value.
interact(cpu_intensive_func,i=widgets.IntSlider(min=1e4, max=1e6, step=1e4, continuous_update=False));
Output
widget can capture stdout, stderr, and output generated by widgets UI as well. We can use the Output
widget with with
statement as well to direct output to it.
out = widgets.Output(layout={"border":"1px solid green"})
out
with out:
display(widgets.IntSlider())
out
out2 = widgets.Output(layout={"border":"1px solid black"})
with out2:
print("Testing String Output")
Output of function can also be directed to Output
widget using Output widgets as decorator.
out3 = widgets.Output(layout={"border":"1px solid red"})
@out3.capture()
def func1():
print("Prints Inside Function")
func1()
We can also clear output widgets using clear_output()
function on widget.
out3.clear_output()
interactive_output()
function also generates output widgets as a result. We can also organize layout by connecting output with widgets.
a = widgets.IntSlider(description='a')
def f(a):
print("Square of a : %f is %f"%(a, a*a))
out = widgets.interactive_output(f, {'a': a})
out.layout = {"border":"1px solid red"}
widgets.VBox([a, out])
We can link more widgets as well using linking functionality of ipywidgets so that if the value of one of the widget changes then another one also changes and synchronize with it.
ipywidgets lets us link objects in 2 ways:
Python Linking
- link()
and dlink()
links widgets using jupyter python kernelJavascript Linking
= jslink()
and jsdlink()
links widgets only using javascript and not jupyter kernel involvement.x = widgets.IntText()
y = widgets.IntSlider()
display(x,y)
two_way_link_python = widgets.link((x, 'value'), (y, 'value'))
The above link created between x
and y
is two ways which means that changes in the value of x will change the value of y and vice versa. Please note that we are linking the value
property of both objects.
Below we have created a link using only javascript. It'll not require jupyter kernel running to work whereas the above example based on python linking will require jupyter kernel running to see changes.
x = widgets.IntText()
y = widgets.IntSlider()
display(x,y)
two_way_link_javascript = widgets.jslink((x, 'value'), (y, 'value'))
Above both links are two ways. If you want to create only a one-way link than you can do it using dlink()
and jsdlink()
.
x = widgets.IntText()
y = widgets.IntSlider()
display(x,y)
one_way_link_python = widgets.dlink((x, 'value'), (y, 'value'))
x = widgets.IntText()
y = widgets.IntSlider()
display(x,y)
one_way_link_javascript = widgets.jsdlink((x, 'value'), (y, 'value'))
Above created both links are one direction from x
to y
only. It means that if we change the value of x
then the value of y
will change but value change in y
will not reflect a change in x
because it's one-way links.
We can unlink widgets as well if linking is not needed anymore. we can do it by calling unlink()
method on the link object.
two_way_link_javascript.unlink()
two_way_link_python.unlink()
one_way_link_python.unlink()
one_way_link_javascript.unlink()
ipywidgets
also lets us execute callback functions based on events. Events can be considered as clicking Button, changing slider values, changing text area value, etc. we might want to execute particular process when any kind of event is performed. ipywidgets
provide such functionality by calling observe()
method and passing it function which you want to execute as callback event. we'll explain events with few examples below. A button allows us to execute the same functionality by passing the method to it's on_click()
method.
button = widgets.Button(description="Click Me!")
output = widgets.Output()
display(button, output)
@output.capture()
def on_button_clicked(b):
print(type(b))
print("Button clicked.")
b.icon="warning"
button.on_click(on_button_clicked)
label = widgets.Label(value='Text Captured : ')
text = widgets.Text(description="Text")
def text_change(change):
print(change)
label.value = "Text Captured : "+change["new"]
text.observe(text_change, names='value')
display(label, text)
We are passing method named text_change
to observe()
method of text widget so that any change in text value calls text_change()
method. We are also passing names=value
which will inform observe()
that we need to capture changes in value
property of Text widget. We are also printing what observe is passing to method when values in text widget changes. Please take a look that it passes the old and new value of widget as state capture for that change.
caption = widgets.Label(value='The values of slider is : ')
slider = widgets.IntSlider(min=-5, max=5, value=0, description='Slider')
def handle_slider_change(change):
print(change)
caption.value = 'The values of slider is : ' + str(change.new)
slider.observe(handle_slider_change, names='value')
display(caption, slider)
Please make a note that observe() gives us higher level of configuration compared to link(). If we want to only change value of widget based on another widget change then link() is ideal but if you want to do some kind of calculations with every change in widget value then observe() is ideal choice for you.
We'll now explain various layout and styling available with ipywidgets
. Various layout methods will let us organize a list of widgets according to different ways whereas the styling attribute of a widget will let us take care of the styling of widgets like height, width, color, button icon, etc.
layout
attribute¶Each widget exposes layout
attribute which can be given information about its layout. CSS properties quite commonly used in styling and layout can be passed to the layout attribute.
b = widgets.Button(description='Sample Button',
layout=widgets.Layout(width='30%', height='50px', border='5px dashed blue'))
b
The above button takes 30%
of space of the page and 50px
as height.
VBox
and HBox
for arranging widgets¶We can utilize VBox
and HBox
layout objects to layout objects as vertical and horizontal respectively. Below we are creating 4 buttons and all have a width of 30%
.
b1 = widgets.Button(description="Button1", layout=widgets.Layout(width="30%"))
b2 = widgets.Button(description="Button2", layout=widgets.Layout(width="30%"))
b3 = widgets.Button(description="Button3", layout=widgets.Layout(width="30%"))
b4 = widgets.Button(description="Button4", layout=widgets.Layout(width="30%"))
h1 = widgets.HBox(children=[b1,b2])
h1
h2 = widgets.HBox(children=[b3,b4])
h2
widgets.VBox(children=[h1,h2])
Box
Layout¶We can layout elements by simply calling Box
layout object. It'll lay out all elements next to each other in a row. We can force layout based on passing size information as layout to Box objects to enforce our layout. We are creating Box which takes 30%
of available page space and organized four buttons into it as a column. We can see that buttons are taking 30%
width of Box layout object which itself takes 30%
of the whole page.
layout = widgets.Layout(display='flex',
flex_flow='column',
border='solid',
width='30%')
widgets.Box(children=[b1,b2,b3,b4], layout=layout)
layout = widgets.Layout(display='flex',
flex_flow='row',
border='solid',
width='30%')
widgets.Box(children=[b1,b2,b3,b4], layout=layout)
item_layout = widgets.Layout(height='100px', min_width='40px')
items = [widgets.Button(layout=item_layout, description=str(i), button_style='success') for i in range(40)]
box_layout = widgets.Layout(overflow_x='scroll',
border='3px solid black',
width='500px',
height='',
flex_flow='row',
display='flex')
carousel = widgets.Box(children=items, layout=box_layout)
widgets.VBox([widgets.Label('Scroll horizontally:'), carousel])
GridBox
is another object provided by ipywidgets
which lets us organize things as grids. Grids are like a table with a specified number of rows and columns. We can pass a list of objects to GridBox
and then force layout further using Layout
object. We need to pass values of parameters grid_template_columns
, grid_template_rows
and grid_gap
for creation of grids. The parameter grid_template_columns
helps us specify what should be sizes of various columns whereas parameter grid_template_rows
helps us specify sizes of rows in the grid. The parameter grid_gap
has 2 values representing the gap between row boxes and column boxes respectively.
widgets.GridBox(children=[widgets.Button(description=str(i), layout=widgets.Layout(width='auto', height='auto'),
button_style='danger') for i in range(19)],
layout=widgets.Layout(
width='60%',
grid_template_columns='100px 50px 100px 50px',
grid_template_rows='80px auto 80px',
grid_gap='12px 2px')
)
widgets.GridBox(children=[widgets.Button(description=str(i), layout=widgets.Layout(width='auto', height='auto'),
button_style='danger') for i in range(16)],
layout=widgets.Layout(
width='60%',
grid_template_columns='20% 20% 20% 20%',
grid_template_rows='80px 40px 80px 40px',
grid_gap='12px 2px')
)
Below is a list of common CSS properties that are available as parameter names in widget objects.
height
width
max_height
max_width
min_height
min_width
visibility
display
overflow
border
margin
padding
top
left
bottom
right
order
flex_flow
align_items
flex
align_self
align_content
justify_content
justify_items
grid_auto_columns
grid_auto_flow
grid_auto_rows
grid_gap
grid_template_rows
grid_template_columns
grid_template_areas
grid_row
grid_column
grid_area
Please feel free to explore various properties of ipywidgets
widget objects to get to know about layout and styling more.
Another option provided by ipywidgets to organize widgets is TwoByTwoLayout
. It gives us four places to put objects and if we do not provide a widget for any place then it keeps that place empty. We are organizing 4 buttons using this layout below. Please make a note that we have created a button with height
and width
as auto
which stretch element in available space.
b1 = widgets.Button(description="Button1",
layout=widgets.Layout(width="auto", height="auto"), button_style="success")
b2 = widgets.Button(description="Button2",
layout=widgets.Layout(width="auto", height="auto"), button_style="primary")
b3 = widgets.Button(description="Button3",
layout=widgets.Layout(width="auto", height="auto"), button_style="info")
b4 = widgets.Button(description="Button4",
layout=widgets.Layout(width="auto", height="auto"), button_style="warning")
widgets.TwoByTwoLayout(top_left=b1,
top_right=b2,
bottom_left=b3,
bottom_right=b4)
If we don't provide any element then it'll stretch other elements to take its space.
widgets.TwoByTwoLayout(top_left=b1,
top_right=b2,
bottom_right=b4)
We can prevent automatic stretching of the widget by passing False
to merge
parameter.
widgets.TwoByTwoLayout(top_left=b1,
top_right=b2,
bottom_right=b4,
merge=False
)
AppLayout is another layout strategy that lets us organize widgets like a web app or desktop application layout.
header = widgets.Button(description="Header",
layout=widgets.Layout(width="auto", height="auto"), button_style="success")
footer = widgets.Button(description="Footer",
layout=widgets.Layout(width="auto", height="auto"), button_style="primary")
left = widgets.Button(description="Left",
layout=widgets.Layout(width="auto", height="auto"), button_style="info")
right = widgets.Button(description="Right",
layout=widgets.Layout(width="auto", height="auto"), button_style="warning")
center = widgets.Button(description="Center",
layout=widgets.Layout(width="auto", height="auto"), button_style="danger")
widgets.AppLayout(header=header,
left_sidebar=left,
center=center,
right_sidebar=right,
footer=footer)
It'll merge widgets if widget for any place is not provided
widgets.AppLayout(header=header,
left_sidebar=left,
center=center,
footer=footer)
widgets.AppLayout(header=header,
left_sidebar=left,
center=center,
right_sidebar=right,)
widgets.AppLayout(header=header,
left_sidebar=left,
right_sidebar=right,
footer=footer)
GridspecLayout
¶It's another way of laying out widgets which is the same as matplotlib gridspec
. It lets us define a grid with a number of rows and columns. We can then put widgets by selecting a single row & column or span them to more rows and columns as well. We'll explain it with a few examples below.
grid = widgets.GridspecLayout(5, 4)
for i in range(5):
for j in range(4):
grid[i, j] = widgets.Button(description="[%d, %d]"%(i,j), button_style="primary")
grid
Please make a note that if we don't provide layout to Button objects with layout of auto for width and height then it won't stretch for whole size of parent widget.
We can span widgets to several rows and columns as well.
grid = widgets.GridspecLayout(6, 4)
grid[0,1:] = widgets.Button(description="Button1", layout=widgets.Layout(height="auto", width="auto"), button_style="success")
grid[-1,1:] = widgets.Button(description="Button2", layout=widgets.Layout(height="auto", width="auto"), button_style="danger")
grid[:,0] = widgets.Button(description="Button3", layout=widgets.Layout(height="auto", width="auto"), button_style="primary")
grid[1:-1,-1] = widgets.Button(description="Button4", layout=widgets.Layout(height="auto", width="auto"), button_style="primary")
grid[3:-1,1:-1] = widgets.Button(description="Button5", layout=widgets.Layout(height="auto", width="auto"), button_style="warning")
grid[1:3,1:-1] = widgets.Button(description="Button6", layout=widgets.Layout(height="auto", width="auto"), button_style="info")
grid
style
attribute¶We can pass CSS styles to the style attribute of a widget and it'll apply that CSS style to a widget. We already applied button_style
attribute in the above examples. We can check a list of available style
attributes in a widget by calling keys
on its style
attribute.
b = widgets.Button(description='Stylish Button')
b.style.button_color = 'tomato'
b
b.style.keys
slider= widgets.FloatSlider(description="Stylish Slider")
slider.style.handle_color="lawngreen"
slider
slider.style.keys
progress_bar = widgets.FloatProgress(value=3, min=0, max=10)
progress_bar.style.bar_color = "purple"
progress_bar
progress_bar.style.keys
This ends our tutorial on giving an introduction on how to use interactive widgets in a jupyter notebook using ipywidgets
. Please feel free to let us know your views. We have tried to cover as many topics as possible but the library is quite big and needs further exploration to better understand things further. We'll be creating more tutorials on using ipywidgets
with other python libraries.