Host_subplot and artist.remove()

Dear matplotlib community,

I am facing an annoying issue in combining host_subplot with artist.remove(). This code is an enormous simplification of the app I am porting to Python 3:

from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.figure import Figure
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as AA

import numpy as np

import wx


class CanvasFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, -1, 'CanvasFrame', size=(550, 350))

        self.figure = Figure()
        self.axes = host_subplot(111, axes_class=AA.Axes, figure=self.figure)
        t = np.arange(0.0, 3.0, 0.01)
        s = np.sin(2 * np.pi * t)

        self.axes.plot(t, s)
        self.patch = self.axes.fill_between(t, 0, s/2.0, facecolor='r', edgecolor='r', 
                                            alpha=0.2, linewidth=1.5, interpolate=True)
        self.canvas = FigureCanvas(self, -1, self.figure)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.EXPAND)
        self.SetSizer(self.sizer)
        self.Fit()

        button = wx.Button(self, -1, 'Remove Collection')
        self.sizer.Add(button, 0)
        button.Bind(wx.EVT_BUTTON, self.OnRemoveCollection)


    def OnRemoveCollection(self, event):
    
        if self.patch is not None:
            self.patch.remove()
            del self.patch
            self.patch = None
            
            self.canvas.draw()



# Alternatively you could use:
class App(wx.App):
    def OnInit(self):
        """Create the main window and insert the custom frame."""
        frame = CanvasFrame()
        frame.Show(True)

        return True


if __name__ == "__main__":
    app = App()
    app.MainLoop()

Shows a very simple plot embedded in wx. There is a button with the label “Remove Collection” at the bottom left of the window. Clicking on it - to try and remove the collection stored in the self.patch attribute from the axes - results in the following:

Traceback (most recent call last):
  File "C:\Users\J0514162\MyProjects\mpl_remove_collections.py", line 42, in OnRemoveCollection
    self.canvas.draw()
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\backends\backend_wxagg.py", line 29, in draw
    FigureCanvasAgg.draw(self)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\backends\backend_agg.py", line 436, in draw
    self.figure.draw(self.renderer)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\artist.py", line 73, in draw_wrapper
    result = draw(artist, renderer, *args, **kwargs)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\artist.py", line 50, in draw_wrapper
    return draw(artist, renderer)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\figure.py", line 2810, in draw
    mimage._draw_list_compositing_images(
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\image.py", line 132, in _draw_list_compositing_images
    a.draw(renderer)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\mpl_toolkits\axes_grid1\parasite_axes.py", line 212, in draw
    super().draw(renderer)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\artist.py", line 50, in draw_wrapper
    return draw(artist, renderer)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\axes\_base.py", line 3082, in draw
    mimage._draw_list_compositing_images(
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\image.py", line 132, in _draw_list_compositing_images
    a.draw(renderer)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\artist.py", line 50, in draw_wrapper
    return draw(artist, renderer)
  File "C:\Users\J0514162\WinPython39\WPy64-39100\python-3.9.10.amd64\lib\site-packages\matplotlib\collections.py", line 990, in draw
    self.set_sizes(self._sizes, self.figure.dpi)
AttributeError: 'NoneType' object has no attribute 'dpi'

Using a standard add_subplot() rather than host_subplot() works all right, but I really do need host_subplot()…

Could anyone please point to my mistake? This is on Windows 10 64 bit, Python 3.9.10, matplotlib 3.5.1.

EDIT: this of course used to work in old versions of matplotlib (1.X) on Python 2.

Thank you in advance.

Andrea.

1.x → 3.5 is a huge jump!

ok, I understand this and it is very subtle:

  1. when we add an Artist to an Axes we add a attribute _remove_method for be called when it is removed
  2. for most artists (and Collections in particular) we set this to art._remove_method = self._children.remove matplotlib/_base.py at a5f5af30abf09e62bd70c4dbbf4af921d73ab88a · matplotlib/matplotlib · GitHub (where self is the Axes object)
  3. when we call remove we call this method and then clean up a a bunch of state we know we set matplotlib/artist.py at a5f5af30abf09e62bd70c4dbbf4af921d73ab88a · matplotlib/matplotlib · GitHub
  4. as part of the draw method HostAxes adds and then removes artists from self._children matplotlib/parasite_axes.py at a5f5af30abf09e62bd70c4dbbf4af921d73ab88a · matplotlib/matplotlib · GitHub (this is OK because it is a mixin with the Axes base classes)
  5. it does this in a way that puts a new list object as self._children
  6. when we remove the Collection it is removed from a now un-used list and the list that is used at draw time still contains the (now intentionally broken) Artist instance.

A minimal reproduction case is

import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as AA
fig = plt.figure()
ax = host_subplot(111, axes_class=AA.Axes, figure=fig)
col = ax.fill_between(range(5), 0, range(5))
fig.canvas.draw()
col.remove()
print('removed')
fig.canvas.draw()

I’m working on a PR to fix this upstream, but adding

if col in ax._children:
    ax._children.remove(col)

(with all of the caveats about buyer-be-ware of using private API and defensive programming around versions! Something like if hasattr(self, '_children') and col in self._children is a better test) should fix the problem.

xref FIX: do not replace the Axes._children list object by tacaswell · Pull Request #24677 · matplotlib/matplotlib · GitHub

Hi Thomas,

Thank you for identifying and fixing the issue so quickly! In the meanwhile, just today I managed to bend fig.add_subplot to do what I wanted, but I think it’s nice that the issue got fixed :blush:.

Yes, 1.X → 3.5 is a big jump. We are porting a very large legacy application from Python 2 to 3, and surprisingly enough there were very few incompatibilities between the two versions of matplotlib - compared to the size of the code. That was a nice surprise :blush:.

I think there’s only one annoying remaining issue, which is very hard to track down as it happens only after many simulations (one after the other). We have a separate thread doing heavy calculations and it updates the GUI periodically via wx.CallAfter, which in turns does some matplotlib plotting of animated lines and blitting of previous backgrounds of the axes. After a number of simulations (not always the same) we either get Python completely stuck (nothing happening at all) or we get a hard crash and the whole python comes down with an access violation in canvas.draw().

It’s going to be next to impossible to replicate in a simple app, so I already know that I’m going to spend months in tracking down the issue - is it because of Python 3.9 being different from 2.7? Is it wxPython? Is it matplotlib? Multi-threading? No clue.

Sorry for the OT, if anyone has any idea on how to debug something like that, I’m all ears :blush:.

Thank you again for your super fast response.

Andrea.

Sorry for the very slow response!

I do not know the thread semantics of Wx well enough to be sure, but what thread does wx.CallAfter run the callback on? If it is not on the main thread I would switch to what ever wx mechanism would put it on the main thread.

I would also suggest replacing all of the draw with draw_idle.

There is (effectively) a double-buffer process going on (mpl has a rastered version of the figure, all of the renderer.draw_XYZ methods update that raster and then at some point we copy that raster from the mpl side to an Image widget of some sort on the GUI side. Working through how exactly that works (I know the Qt version decently well but not the wx version) is probably the second thing I would look at. If you are alternating between a crash and a deadlock my guess is that there is a race condition with 2 locks that have overlapping responsibilities…