ipywidget with matplotlib figure always shows two axes

1.2k views Asked by At

I am trying to create a ipywidget interface with a matplotlib figure that updates upon changing a slider. It works in principle, but it always creates an extra figure.

Here's the code:

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

# make a grid

x = np.linspace(0, 5, 100)
X, Y = np.meshgrid(x, x)

# create the layout with slider

out = widgets.Output(layout=widgets.Layout(height='300px', width = '400px', border='solid'))
slider = widgets.IntSlider(value=1, min=1, max=5)
w = widgets.VBox(children=(out, slider))

# axes to plot into

ax = plt.axes()

display(w)

def update(value):
    i = slider.value
    Z = np.exp(-(X / i)**2 - (Y / i)**2)
    ax.pcolormesh(x, x, Z, vmin=0, vmax=1, shading='auto')
    with out:
        clear_output(wait=True)
        display(ax.figure)

slider.observe(update)
update(None)

And here's the undesired output

output

The widget works, and only the upper output is updated, but I do not understand why the lower output also exists or how to get rid of it. Am I missing something obvious?

3

There are 3 answers

3
jayveesea On BEST ANSWER

You can use the widget backend, %matplotlib widget, which I think is designed for this. You'll need to put %matplotlib widget at the top (or before matplotlib stuff is brought in).

Update: Also some guidance form matplotlib here and below, emphasis added.

Note

To get the interactive functionality described here, you must be using an interactive backend. The default backend in notebooks, the inline backend, is not. backend_inline renders the figure once and inserts a static image into the notebook when the cell is executed. Because the images are static, they can not be panned / zoomed, take user input, or be updated from other cells.

Your example can be reduced to:

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

x = np.linspace(0, 5, 100)
X, Y = np.meshgrid(x, x)

slider = widgets.IntSlider(value=1, min=1, max=5)

ax = plt.axes()
display(slider)
def update(value):
    i = slider.value
    Z = np.exp(-(X / i)**2 - (Y / i)**2)
    ax.pcolormesh(x, x, Z, vmin=0, vmax=1, shading='auto')

slider.observe(update)
update(None)

enter image description here

0
Ianhi On

The answer by jayveesa is a really good way to avoid this output. I'd like to add an answer to the why of your question.

The widget works, and only the upper output is updated, but I do not understand why the lower output also exists or how to get rid of it. Am I missing something obvious?

An instructive thing to do here is to comment out the lines in update that display the figure


def update(value):
    i = slider.value
    Z = np.exp(-(X / i)**2 - (Y / i)**2)
    ax.pcolormesh(x, x, Z, vmin=0, vmax=1, shading='auto')
    # with out:
        # clear_output(wait=True)
        # display(ax.figure)
update(None)

if you do this you will see only one plot output. This is because when you use the the %matplotlib inline backend for jupyter notebooks then any figure that was created in a cell will be closed and displayed when the cell finishes running. (see explanation from a core matplotlib dev here: https://github.com/matplotlib/matplotlib/issues/18248#issuecomment-675177108). So the figure surrounded by a black outline is from the display(ax.figure) and the lower figure is from the figure that was created earlier.

0
Ania Wojciechowska On

I had exactly same problem and used %matplotlib notebook Not only I had single figure but I could also interact (have x,y position, rotate and change the angle of 3d plot) enter image description here

import ipywidgets as widgets
%matplotlib notebook
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')

ax.set_xlim(0, xij.max())
ax.set_ylim(0, yij.max())
ax.set_zlim(min_value, max_value)

def update_plot(frame=0):
    ax.clear()
    ax.set_xlim(0, xij.max())
    ax.set_ylim(0, yij.max())
    ax.set_zlim(min_value, max_value)
    surf = ax.plot_surface(xij, yij, data[frame], cmap=cm.coolwarm,
                   linewidth=0, antialiased=False)
 
widgets.interact(update_plot, frame=widgets.IntSlider(min=0, max=100, step=1, value=0))