Search code examples
pythonjupyteripywidgets

Implement a "reset" button when using @interact decorator in Jupyter


I'm trying to do a simple button to "reset" widgets to certain default values. I'm using the @interact decorator in Jupyter Lab environment. The problem is that the widgets identifiers have their values copied to the same identifiers used as float variables inside the function and therefore I cannot access them anymore within this new scope. Here is a short example (not working):

import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, Button

@interact(starts_at=(0, np.pi*0.9, np.pi*0.1), ends_at=(np.pi, 2*np.pi, np.pi*0.1))
def plot_graph(starts_at=0, ends_at=2*np.pi):
    
    def on_button_clicked(_):
        # instructions when clicking the button (this cannot work)
        starts_at = 0
        ends_at = 2*np.pi
        
    button = Button(description="Reset")
    button.on_click(on_button_clicked)
    display(button)
    
    f = lambda x : sum(1/a*np.sin(a*x + np.pi/a) for a in range(1,6))
    x = np.linspace(0, 2*np.pi, 1000)
    plt.plot(x, f(x))
    plt.xlim([starts_at, ends_at])

Does anybody know how to send to the scope of the decorated function a reference to the original widget objects? I'll be accepting also simple ways of implementing a button to reset those sliders.

:-D

Edit: corrected text flow


Solution

  • To accomplish this you'll have to use the more manual interactive_output function. That function allows you to pre-create the widgets and then pass them in:

    import ipywidgets as widgets
    import numpy as np
    import matplotlib.pyplot as plt
    
    start_slider = widgets.FloatSlider(
                                val = 0,
                                min = 0,
                                max = np.pi*0.9,
                                step = np.pi*0.1,
                                description = 'Starts at'
                                )
    end_slider = widgets.FloatSlider(
                                val = np.pi,
                                min = np.pi,
                                max = 2*np.pi,
                                step = np.pi*0.1,
                                description = 'Ends at'
                                )
    def on_button_clicked(_):
        start_slider.value = 0
        end_slider.value = 2*np.pi
    
    button = Button(description="Reset")
    button.on_click(on_button_clicked)
    def plot_graph(starts_at=0, ends_at=2*np.pi):
        f = lambda x : sum(1/a*np.sin(a*x + np.pi/a) for a in range(1,6))
        x = np.linspace(0, 2*np.pi, 1000)
        plt.plot(x, f(x))
        plt.xlim([starts_at, ends_at])
    
    display(widgets.VBox([start_slider, end_slider, button]))
    widgets.interactive_output(plot_graph, {'starts_at': start_slider, 'ends_at':end_slider})
    

    However, this will regenerate the plot entirely everytime you update it which can lead to a choppy experience. So you can also re-write this to use the matplotlib methods like .set_data if you use an interactive matplotlib backend in the notebook. So if you were to use ipympl you could follow the examples in this example notebook.

    Via another library

    I wrote a library mpl-interactions to make it easier to control matplotlib plots using ipywidgets sliders. It provides a function analogous to ipywidgets.interact in that it handles creating the widgets for you, but it has the advantage of being matplotlib focused so all you need to provide is the data. More about the differences to ipywidgets here

    %matplotlib ipympl
    import mpl_interactions.ipyplot as iplt
    import matplotlib.pyplot as plt
    import numpy as np
    import ipywidgets as widgets
    
    def plot_graph(starts_at=0, ends_at=2*np.pi):
        x = np.linspace(starts_at, ends_at, 1000)
        f = lambda x : sum(1/a*np.sin(a*x + np.pi/a) for a in range(1,6))
        return np.array([x, f(x)]).T
    
    
    fig, ax = plt.subplots()
    button = widgets.Button(description = 'reset')
    display(button)
    controls = iplt.plot(plot_graph, starts_at = (0, np.pi), ends_at = (np.pi, 2*np.pi), xlim='auto', parametric=True)
    def on_click(event):
        for hbox in controls.controls.values():
            slider = hbox.children[0]
            slider.value = slider.min
    button.on_click(on_click)