A deeper dive into working with the callbacks and events when widget values change.
import ipywidgets as widgets
from IPython.display import display
from traitlets import HasTraits
Every widget inherits from traitlets.HasTraits, which provides the functionality of observing values, linking wiht other widgets, and for validation of values. You can learn more about traitlets in it's user guide
Every HasTraits
class has an observe
method which allows observing properties changes. You can assign a Python callback function that will be called when a property changes.
The callback handler passed to observe will be called with one change argument. The change object holds at least a type
key and a name
key, corresponding respectively to the type of notification and the name of the attribute that triggered the notification.
Other keys may be passed depending on the value of type
. In the case where type is change
, we also have the following keys:
owner
: the HasTraits instanceold
: the old value of the modified trait attributenew
: the new value of the modified trait attributename
: the name of the modified trait attribute.Since Widget
classes inherit from HasTraits
, you can register handlers to the change events whenever the model gets updates from the front-end or from other code.
To investigate this lets take the final example from the previous section with the small change of removing the names=True
# Create and display our widgets
slider = widgets.FloatSlider(description='Input:')
square_display = widgets.HTML(description="Square: ", value='{}'.format(slider.value**2))
display(widgets.VBox([slider, square_display]))
# Create function to update square_display's value when slider changes
def update_square_display(change):
square_display.value = '{}'.format(change.new**2)
slider.observe(update_square_display) # removed names="value"
# Create and display our widgets
slider = widgets.FloatSlider(description='Input:')
square_display = widgets.HTML(description="Square: ", value='{}'.format(slider.value**2))
display(widgets.VBox([slider, square_display]))
# We use an output widget here for capturing the print calls and showing them at the right place in the Notebook
output = widgets.Output()
display(output)
# Create function to update square_display's value when slider changes
@output.capture()
def update_square_display(change):
print(change)
square_display.value = '{}'.format(change.new**2)
slider.observe(update_square_display) # removed names="value"
After investigating, we can see that we are getting notifications for the _property_lock
trait. This is why it's important to use the
names=
argument.
# Create and display our widgets
slider = widgets.FloatSlider(description='Input:')
square_display = widgets.HTML(description="Square: ", value='{}'.format(slider.value**2))
display(widgets.VBox([slider, square_display]))
output = widgets.Output()
display(output)
@output.capture()
def update_square_display(change):
print(change)
square_display.value = '{}'.format(change.new**2)
slider.observe(update_square_display, names="value") # added back the names="value"
This captures all changes to the value, not just those made using the mouse.
slider.value = .3
names
?¶Any named trait that the widget has can be observed. You can see what traits a widget has with the .traits()
method
slider.traits()
.value
and validation¶Most of the ipywidgets
widgets have a .value
trait that corresponds to the data type they represent. In addition to powering observe
traitlets also performs validation and coercion for these values. So if you try to set one to the wrong type you will get an error or it may be coerced to a new data type.
int_slider = widgets.IntSlider()
float_slider = widgets.FloatSlider()
# fine
float_slider.value = 5.5
# Will get rounded to an int
int_slider.value = 5.5
print(int_slider.value)
# raises an error
int_slider.value = "5.5"
Using observe
and the Output
widget print out the reverse of the text string in the Textarea
widget below.
text = widgets.Textarea()
output = widgets.Output()
# ...
# ...
display(text, output)
# %load solutions/observe-reverse.py
observe
or link
?¶When to use observe
observe
is most useful when you want to have a sideeffect on something that is not a widget (e.g. modifying a matplotlib plot, or saving a file), or when you need to information on the previous value.
link
is the easiest way to connect traits of two widgets together. You can also transform the values by passing a function to the transform
argument.
slider1 = widgets.IntSlider()
slider2 = widgets.IntSlider()
widgets.link((slider1, "value"), (slider2, "value"))
display(slider1, slider2)
You aren't limited in linking value
with value
. Any traits that have compatible types can be linked. In this example the min
value of the main slider is controlled by the second slider.
main_slider = widgets.IntSlider(value=3, min=0, max=10, description="main slider")
min_slider = widgets.IntSlider(value=0, min = -10, max=10, description="min slider")
widgets.link((main_slider, "min"), (min_slider, "value"))
display(widgets.VBox([main_slider, min_slider]))
Using widgets.link
with the transform
argument and the two functions to make both text boxes update when the other is modified.
def C_to_F(temp):
return 1.8 * temp + 32
def F_to_C(temp):
return (temp -32) / 1.8
degree_C = widgets.FloatText(description='Temp $^\circ$C', value=0)
degree_F = widgets.FloatText(description='Temp $^\circ$F', value=C_to_F(degree_C.value))
display(degree_C, degree_F)
# %load solutions/temperature-link.py
Earlier you used link
to link the value of one widget to another.
There are a couple of other linking methods that offer more flexibility:
dlink
is a directional link; updates happen in one direction but not the other.jslink
and jsdlink
do the linking in the front endThe first method is to use the link
and dlink
. This only works if we are interacting with a live kernel.
caption = widgets.HTML(value='Changes in source values are reflected in target1, but changes in target1 do not affect source')
source, target1 = widgets.IntSlider(description='Source'),\
widgets.IntSlider(description='Target 1')
display(caption, source, target1)
dl = widgets.dlink((source, 'value'), (target1, 'value'))
Links can be broken by calling unlink
.
dl.unlink()
You can also directly link widget attributes in the browser using the link widgets, in either a unidirectional or a bidirectional fashion.
Javascript links persist when embedding widgets in html web pages without a kernel.
caption = widgets.Label(value='The values of range1 and range2 are synchronized')
range1, range2 = widgets.IntSlider(description='Range 1'),\
widgets.IntSlider(description='Range 2')
display(caption, range1, range2)
l = widgets.jslink((range1, 'value'), (range2, 'value'))
caption = widgets.Label(value='Changes in source_range values are reflected in target_range1')
source_range, target_range1 = widgets.IntSlider(description='Source range'),\
widgets.IntSlider(description='Target range 1')
display(caption, source_range, target_range1)
dl = widgets.jsdlink((source_range, 'value'), (target_range1, 'value'))
The links can be broken by calling the unlink
method.
l.unlink()
dl.unlink()
link
and jslink
¶Python Linking
Pros:
Cons:
Client Linking
Pros:
Cons:
For more details on the difference see the documentation.
leader = widgets.IntSlider(description="leader")
py_follower = widgets.IntSlider(description='python link')
js_follower = widgets.IntSlider(description='client link')
display(leader, js_follower, py_follower)
l_js = widgets.jslink((leader, 'value'), (js_follower, 'value'))
l_py = widgets.link((leader, 'value'), (py_follower, 'value'))
Some widgets offer a choice with their continuous_update
attribute between continually updating values or only updating values when a user submits the value (for example, by pressing Enter or navigating away from the control). In the next example, we see the "Delayed" controls only transmit their value after the user finishes dragging the slider or submitting the textbox. The "Continuous" controls continually transmit their values as they are changed. Try typing a two-digit number into each of the text boxes, or dragging each of the sliders, to see the difference.
a = widgets.IntSlider(description="Delayed", continuous_update=False)
b = widgets.IntText(description="Delayed", continuous_update=False)
c = widgets.IntSlider(description="Continuous", continuous_update=True)
d = widgets.IntText(description="Continuous", continuous_update=True)
widgets.jslink((a, 'value'), (b, 'value'))
widgets.jslink((a, 'value'), (c, 'value'))
widgets.jslink((a, 'value'), (d, 'value'))
widgets.VBox([a,b,c,d])
Widgets defaulting to continuous_update=True
:
Text
Textarea
Widgets defaulting to continuous_updateFalse
:
IntText
)Some widgets like the Button
have special events on which you can hook Python callbacks.
The Button
is not used to represent a data type. Instead the button widget is used to handle mouse clicks. The on_click
method of the Button
can be used to register function to be called when the button is clicked. The doc string of the on_click
can be seen below.
widgets.Button.on_click?
Since button clicks are stateless, they are transmitted from the front-end to the back-end using custom messages. By using the on_click
method, a button that prints a message when it has been clicked is shown below. To capture print
s (or any other kind of output including errors) and ensure it is displayed, be sure to send it to an Output
widget (or put the information you want to display into an HTML
widget).
button = widgets.Button(description="Click Me!")
output = widgets.Output()
display(button, output)
@output.capture()
def on_button_clicked(b):
print("Button clicked.")
button.on_click(on_button_clicked)