matplotlib / matplotlib

matplotlib: plotting with Python
https://matplotlib.org/stable/
20.28k stars 7.65k forks source link

[Bug]: matplotlib==3.5.2 breaks ipywidgets #23229

Closed TomBugnon closed 2 years ago

TomBugnon commented 2 years ago

Bug summary

After updating matplotlib to 3.5.2 (from 3.5.1) the display/updating of widgets created with ipywidgets is broken, at least for certain types of plots. Update of print statements in response to moving the widgets works fine but the plot is not modified.

To reproduce:

ipykernel jupyterlab-widgets ipywidgets numpy ipympl matplotlib==3.5.2

Code for reproduction

from IPython.display import display
from ipywidgets import (
    FloatSlider,
    HBox,
    VBox,
    Layout,
    fixed,
    interactive_output,
)

class MiniRaster:
    def __init__(
        self,
    ):
        self._trains = [
            np.linspace(0, 10, 100), 
            np.linspace(0, 10, 1000)
        ]

    def _interact(self, ax, linewidth, xmax):
        if ax is None:
            fig, ax = plt.subplots()
        ax.cla()
        ax.eventplot(
            self._trains,
            linewidth=linewidth
        )
        plt.xlim(0, xmax)

        print('linewidth = ', linewidth)
        print('xmax = ', xmax)
        return ax

    def interact(self):
        fig, ax = plt.subplots()

        linewidth_slider = FloatSlider(
            min=0,
            max=10,
            step=1,
            value=1,
            description="width=",
            continuous_update=False,
            layout=Layout(width="95%"),
        )

        xmax_slider = FloatSlider(
            min=1,
            max=20,
            step=1,
            value=10,
            description="xmax=",
            continuous_update=False,
            layout=Layout(width="95%"),
        )

        ui = VBox([linewidth_slider, xmax_slider])

        out = interactive_output(
        # return interactive_output(
            self._interact,
            {
                "ax": fixed(ax),
                "linewidth": linewidth_slider,
                "xmax": xmax_slider
            },
        )
        display(ui, out)

raster = MiniRaster()

%matplotlib widget
raster.interact()

Actual outcome

image Print statements follow the widgets, not the figure

Expected outcome

When pinning matplotlib version to 3.5.1, both the print statement and the figure follow the widgets

image

Additional information

This doesn't happen for all types of figures/widgets. I'm at a loss as to why, but this snippet works fine for me for both versions

import ipywidgets as widgets

m = widgets.FloatSlider(min=-5,max=5,step=0.5, description="Slope")
c = widgets.FloatSlider(min=-5,max=5,step=0.5, description="Intercept")

# An HBox lays out its children horizontally
ui = widgets.HBox([m, c])

def plot(m, c):
    x = np.random.rand(10)
    y = m *x + c
    plt.cla()
    plt.plot(x,y)
    plt.show()

out = widgets.interactive_output(plot, {'m': m, 'c': c})

display(out, ui)

I had the same behavior on python3.7 and 3.10, and on VSCode jupyter and jupyter notebook.

conda == 4.11.0

Operating system

Ubuntu

Matplotlib Version

3.5.2

Matplotlib Backend

module://ipympl.backend_nbagg

Python version

3.10.4

Jupyter version

6.4.12

Installation

pip

tacaswell commented 2 years ago

If you add a plt.ion() in just after %matplotlib widget does it work again?

Can you try editing your post, the layout markup is not quite right.

TomBugnon commented 2 years ago

Yes it works again with plt.ion() ..! Is it a bug or should I have been better reading the doc? :)

ianhi commented 2 years ago

@TomBugnon in general I would try to avoid mixing interactive_output with %matplotlib widget, instead you should display(fig.canvas) see more about this here: https://matplotlib.org/ipympl/examples/full-example.html#interacting-with-other-widgets. There is some complex behavior in interactive_output that is tailored for the inline backend which expects a new figure to be created everytime, whereas here you are updating the same figure

In your case you can just include the fig.canvas in the VBox like so: ui = VBox([linewidth_slider, xmax_slider, fig.canvas])

tacaswell commented 2 years ago

xref https://github.com/matplotlib/ipympl/issues/469 and this should be fixed by https://github.com/matplotlib/matplotlib/pull/23057

Erotemic commented 2 years ago

I think I'm experiencing the same issue. A downgrade to 3.5.1 resolves the issue for me, but I'm not using "widget", I'm using the "qt" backend.

I've been using matplotlib + terminal IPython for whats coming close to a decade, so I'm not doing any fancy new Jupyter notebook stuff. I just open my terminal and start making figures. The following code when pasted into IPython breaks for me on matplotlib 3.5.2:

import matplotlib as mpl
import IPython
ipython = IPython.get_ipython()
ipython.run_line_magic('matplotlib', 'qt')
backend = 'Qt5Agg'
mpl.use(backend)

from matplotlib import pyplot as plt
import numpy as np

img1 = np.random.rand(128, 128)
img2 = img1.copy()
img2[:, 0:64] = 0

# Showing the initial image works fine.
plt.imshow(img1)

# Should update the previous figure to show a new image
plt.imshow(img2)

Hopefully the patch fixes this case too, but I figure I'd comment because this MWE is a bit simpler than the previous and might be easier to integrate into a regression test.

Tested on CPython 3.10.5 with IPython 8.4.0, and CPython 3.9.9 with IPython 8.3.0 in Ubuntu 22.04, all installed via pyenv and pip.

jklymak commented 2 years ago

@Erotemic please open a new issue explaining exactly what has broken for you.