#!/usr/bin/env python # coding: utf-8 # # Widgets Events # # A deeper dive into working with the callbacks and events when widget values change. # In[ ]: import ipywidgets as widgets from IPython.display import display from traitlets import HasTraits # ## Traitlets events # Every widget inherits from [traitlets.HasTraits](https://traitlets.readthedocs.io/en/stable/api.html#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](https://traitlets.readthedocs.io/en/stable/using_traitlets.html#using-traitlets) # # 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 instance # - `old` : the old value of the modified trait attribute # - `new` : the new value of the modified trait attribute # - `name` : the name of the modified trait attribute. # ### Registering callbacks to trait changes in the kernel # # 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` # # In[ ]: # 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" # ## Importance of `names` argument # # That didn't work! And where did our error messages go? # # # ### Seeing error messages in callbacks # # 1. Jupyterlab Log console # 2. `widgets.Output()` # In[ ]: # 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. # In[ ]: # 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. # In[ ]: slider.value = .3 # #### What are the valid arguments to `names`? # Any named trait that the widget has can be observed. You can see what traits a widget has with the `.traits()` method # In[ ]: 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. # In[ ]: 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" # ## Exercise # # # Using `observe` and the `Output` widget print out the reverse of the text string in the `Textarea` widget below. # In[ ]: text = widgets.Textarea() output = widgets.Output() # ... # ... display(text, output) # In[ ]: # %load solutions/observe-reverse.py # ### Should you use `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. # # In[ ]: 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. # In[ ]: 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])) # ### Exercise # # Using `widgets.link` with the `transform` argument and the two functions to make both text boxes update when the other is modified. # In[ ]: 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) # In[ ]: # %load solutions/temperature-link.py # ## Advanced Widget Linking # 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 end # - While the linking happens in the frontend the Python objects still have their values updated. # ### dlink in the kernel (ie. in Python) # # The first method is to use the `link` and `dlink`. This only works if we are interacting with a live kernel. # In[ ]: 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`. # In[ ]: dl.unlink() # ### Linking widgets attributes from the client side # 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. # In[ ]: 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')) # In[ ]: 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. # In[ ]: l.unlink() dl.unlink() # ### The difference between `link` and `jslink` # # **Python Linking** # # Pros: # 1. Allows transformation of values # # Cons: # 1. Worse performance # 2. Does not persist when kernel is not running. # # **Client Linking** # # Pros: # 1. Works without a kernel # 2. Faster GUI updates # # Cons: # 1. No transformations # # For more details on the difference see the [documentation](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#The-difference-between-linking-in-the-kernel-and-linking-in-the-client). # In[ ]: 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')) # ### Continuous vs delayed updates # # 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. # In[ ]: 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`: # # - Sliders # - `Text` # - `Textarea` # # Widgets defaulting to `continuous_updateFalse`: # # - Textboxes for entering numbers (e.g. `IntText`) # ## Special events # 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. # In[ ]: get_ipython().run_line_magic('pinfo', 'widgets.Button.on_click') # ### Example # 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). # In[ ]: 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)