jupyter-widgets / ipywidgets

Interactive Widgets for the Jupyter Notebook
https://ipywidgets.readthedocs.io
BSD 3-Clause "New" or "Revised" License
3.17k stars 950 forks source link

ipywidgets events produces many graphs #1919

Open franktoffel opened 6 years ago

franktoffel commented 6 years ago

I am trying to use widget events to make an interactive graph.

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

import ipywidgets as widgets

def myplot(n):
    x = np.linspace(-5, 5, 30)
    y = x**n

    fig, ax = plt.subplots(nrows=1, ncols=1);
    ax.plot(x, y)
    ax.set_xlabel('x')
    ax.set_ylabel('y')

    plt.show()

Interact works as expected (it changes the figure interactively):

widgets.interact(myplot, n=(0,5));

However the following snippet creates several figures that appear below as you interact with the slider.

n_widget = widgets.IntSlider(
                value=2,
                min=0,
                max=5)

def on_value_change(change):
    myplot(n=n_widget.value)

n_widget.observe(on_value_change)
display(n_widget)

Can I update the plot as if I were using widgets.interact()?


My current installation is with conda and Python 3.6 (windows machine).

ipywidgets                7.1.0                     
jupyter                   1.0.0              
jupyter_client            5.2.1                  
jupyter_console           5.2.0             
jupyter_core              4.4.0              
matplotlib                2.1.1             
notebook                  5.3.1               
numpy                     1.14.0     
franktoffel commented 6 years ago

Using IPython.display.display() and .clear_output() the slider disappears after the first interaction. The following code clears the old results but also the widget.

EDIT: This is expected after ipywidgets v.> 7.0 (see #1274)

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import display, clear_output
import ipywidgets as widgets

fig, ax = plt.subplots(nrows=1, ncols=1);
x = np.linspace(-5, 5, 30)
y = x**0

line, = ax.plot(x, y)
ax.set_xlabel('x')
ax.set_ylabel('y')

def myplot(n):
    line.set_ydata(x**n)
    ax.relim()
    ax.autoscale()
    display(fig)
    clear_output(wait=True)

#cell3
n_widget = widgets.IntSlider(
                value=2,
                min=0,
                max=5)

def on_value_change(change):
    myplot(n=n_widget.value)

n_widget.observe(on_value_change)
display(n_widget)
ImportanceOfBeingErnest commented 6 years ago

The actual issue/problem description may be a bit lost in the above bunches of code. So to summarize:

With ipywidgets 6.0 it was possible to diplay a widget and a resulting output from one cell. Then using clear_output the output could be removed or updated by subsequently calling display again.

This behaviour has changed in version 7.0. As the changelog tells us, clear_output will now clear everything, including the widget. This makes the previous solution unusable. However, it is not clear what alternative one should use to get the same behaviour back with ipywidgets 7.0.

So this is either a question for the recommended way of adding a persistent widget which can clear the output, or a request for adding back such solution in a future version.

kmader commented 6 years ago

If I understood your issue correctly I managed to solve it by using the following approach (https://github.com/kmader/Quantitative-Big-Imaging-2018/blob/127fd3497cbf37ac2ab89f7076fc1c81dcfb9f17/Exercises/ImageEnhancementPlayground.ipynb)

display(ipw.VBox([ipw.VBox([ipw.HBox([ipw.Label(value = 'Image Name:'), image_name]), 
                    ipw.HBox([ipw.Label(value = 'Noise Type:'), noise_func]),
                    ipw.HBox([ipw.Label(value = 'Noise Level:'), noise_level]),
                    ipw.HBox([ipw.Label(value = 'Filter Name:'), filter_func]),
                    ipw.HBox([ipw.Label(value = 'Filter Size:'), filter_size])        
                   ])]))
display(fig, display_id = 'nice_figure') # need to create figure before callbacks

def update_image(*args):
    show_results(m_axs, 
                 sample_images[image_name.value],
                 noise_func.value, 
                 noise_level.value, 
                 filter_func.value, 
                 {'size': filter_size.value
                 })
    update_display(display_id = 'nice_figure', obj = fig)
image_name.observe(update_image, names='value')
jasongrout commented 6 years ago

Great questions. The recommended way now to display and clear output inside a widget is to use an Output widget. We should add a note in the changelog.

Here's a brief example:

from ipywidgets import Output, IntSlider, VBox
from IPython.display import clear_output
out = Output()

slider = IntSlider()

def square(change):
    with out:
        clear_output()
        print(change.new*change.new)

slider.observe(square, 'value')
slider.value = 50
display(VBox([slider, out]))
jasongrout commented 6 years ago

(this will be even easier soon, when the output widget will have a decorator: https://github.com/jupyter-widgets/ipywidgets/pull/1934)

astrojuanlu commented 4 years ago

From the comments above, it's not clear to me what's the recommended way of updating a matplotlib plot inside a trait callback.

This works:

@interact(x=(0, 10))
def plot(x):
    plt.plot([1, 2, 3], [4, 5, 6])

But the equivalent using observe has the problem @franktoffel (¡hola! :wave:) described 2 years ago of many plots appearing:

# Many plots
def plot(change):
    plt.plot([1, 2, 3], [5, 4, 6])

slider = widgets.IntSlider(min=0, max=10, value=5)
slider.observe(plot, names="value")
display(slider)

and then using clear_output at the beginning, clears the slider itself:

# Slider disappears
def plot(change):
    clear_output()
    plt.plot([1, 2, 3], [5, 4, 6])

slider = widgets.IntSlider(min=0, max=10, value=5)
slider.observe(plot, names="value")
display(slider)

The "Flickering and jumping output" section of the documentation explains how to combine matplotlib with interactive, but observe is not mentioned.

The three examples that mention matplotlib, "Exploring the Lorenz System of Differential Equations", "Exploring Beat Frequencies using the Audio Object" and "Explore Random Graphs Using NetworkX" use interact or interactive, but observe is not explained.

There is some discussion at https://github.com/jupyter-widgets/ipywidgets/issues/1940 that uses Output as a context manager, but still it's not clear to me how to use it. I tried the code below, but the code widget is empty:

out = widgets.Output()
with out:
    fig, ax = plt.subplots()
ax.plot([1, 2], [1, -1])
# ---
# Another cell, output is empty
out

Is there a chance someone posts here a super short example on how to use a callback + observe + a matplotlib figure? I would be willing to open a pull request to add it to the docs... When I understand how to do it.

(Edit: Add one extra example)

astrojuanlu commented 4 years ago

Sort of incomplete self-answer: I adapted some code from this SO answer, however it still has a couple of weird things I can't explain about the effect of including (or not) plt.show() and/or display(fig):

import ipywidgets as widgets 
import matplotlib.pyplot as plt

out = widgets.Output(layout=widgets.Layout(height='300px'))

def f(change):
    with out:
        fig, ax = plt.subplots()
        ax.plot([0, 1],[0, 10])
        ax.set_title(change['new'])
        out.clear_output()  # Required, otherwise output area stays the same but plots get added
        # display(fig)  # Doesn't work as a replacement of plt.show()
        plt.show()  # If not here, multiple plots are shown!

w = widgets.IntSlider(min=0, max=10, value=5)
w.observe(f, names="value")
display(w, out)
gsteele13 commented 3 years ago

@astrojuanlu Thanks for the code, this has been causing me quite some frustration as I try to get interact to also work with a throttling / debouncing wrapper.

You code nearly reproduces the 1-liner equivalent of interact, but I do see noticeable "flashing".

Did you find a solution for this?

And does anyone know why interact somehow magically works without flashing? Eg.

x = np.linspace(0,1,100)

def update_plot(p=1):
    plt.plot(x, x**p)

interact(update_plot, p=(0,1,0.01))

gives no flashing, while the above does...

Thanks, Gary

astrojuanlu commented 3 years ago

Hi @gsteele13 , I haven't made any progress since my last comment. Best luck!

gsteele13 commented 3 years ago

Hi @astrojuanlu,

I've figured out how to reproduce the interactivity of "interact" without flashing: key was to use wait=True when clearing the output. This is simple minimal example:

import ipywidgets as widgets 
import matplotlib.pyplot as plt

out = widgets.Output(layout=widgets.Layout(height='300px'))

x = np.linspace(0,1,100)

def update_plot(w):
    with out:
        # Without clear_output(), figures get appended below each other inside
        # the output widget
        # Ah ha! Got it! I need wait=True!
        out.clear_output(wait=True)
        plt.plot(x, x**p_widget.value)  
        plt.show()

p_widget = widgets.FloatSlider(min=0, max=2, step=0.1, value = 1)
update_plot([])
p_widget.observe(update_plot)
display(p_widget, out)

Next step: debounce / throttle! :)

astrojuanlu commented 3 years ago

I see myself googling this a fair number of times in the near future... Would the snippet above fit somewhere in the documentation?

gsteele13 commented 3 years ago

I'm just looking at the docs now. What would be a good place to put this info?

There is some info about "flickering" here when using "interactive output":

https://ipywidgets.readthedocs.io/en/stable/examples/Using%20Interact.html?highlight=matplotlib#Flickering-and-jumping-output

But this is slightly different: it is about the building the interaction yourself using an output widget. In that sense, it probably belongs as a part of the output widget examples:

https://ipywidgets.readthedocs.io/en/latest/examples/Output%20Widget.html

Any thoughts? I have checked out the codebase: I'll maybe make some suggestions and a pull request?

gsteele13 commented 3 years ago

created my first pull request (ever...) :)