matplotlib / ipympl

Matplotlib Jupyter Integration
https://matplotlib.org/ipympl/
BSD 3-Clause "New" or "Revised" License
1.57k stars 227 forks source link

Failed to execute 'drawImage' on 'CanvasRenderingContext2D' when Canvas instance is embedded in the ipywidgets layout #472

Open pyfrid opened 2 years ago

pyfrid commented 2 years ago

For my project, it is required to embed Canvas from the ipympl backend in the ipywidgets Layout or AppLayout. It worked perfectly with version 0.7.0, but with all newer versions the plot image is not rendered in the canvas with exception in Crome browser: Error setting state: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.

The minimum example:

import numpy as np
from matplotlib.figure import Figure
from ipympl.backend_nbagg import Canvas as Canvas, FigureManager

class PlotWidget(Canvas):

    __num__ = 0

    def __init__(self, **kwargs):
        Canvas.__init__(self, Figure(dpi=100, figsize=(6, 5.5)))
        self.resizable = False
        self.header_visible=False

        fig = self.figure
        ax = fig.add_subplot(1, 1, 1)
        self.lines, = ax.plot(np.linspace(0, 1, 100), np.linspace(0, 1, 100), 'o-', markersize=2, linewidth=2)

        manager = FigureManager(self, self.__num__)
        self.__num__+=1
        self.draw()

import ipywidgets.widgets as widgets
from ipywidgets import AppLayout, Layout

class Layout(AppLayout):

    def __init__(self, **kwargs):
        AppLayout.__init__(self, **kwargs)
        self.center = PlotWidget(**kwargs)
        if "pane_widths" not in kwargs: 
            self.pane_widths = [1, '650px', '0px']
        if "pane_heights" not in kwargs: 
            self.pane_heights = ['0px', '300px', '0px']

app = Layout()
display(app)
pyfrid commented 2 years ago

After some trial and error, it seems that problem is in the draw method. It should be draw_idle()

ianhi commented 2 years ago

Hi @pyfrid,

I don't know if I would consider subclassing the Canvas in this way to be supported. In general creating your own figures not via the pyplot interface can be fraught, and more so when you are in an interactive context like ipympl. Instead I would recommend creating your figure via fig, ax = plt.subplots() and then embedding the fig.canvas (which is an ipywidget) into the applayout. See here for an example: https://matplotlib.org/ipympl/examples/full-example.html#fixing-the-double-display-with-ioff

pyfrid commented 2 years ago

Hi @ianhi, Looking in the source code of the ipympl.backend, I don't see any reason, why it should not be supoorted? What should stop me to define figure instance inside the Canvas Subclass and update it when necessary? The backend class initializes FigureManager instance and Canvas for the given figure and calls display method with few checks and connections, which I skipped in this example, but in general it is the same way. Interesting is why the draw method doesn't work anymore?