Repository navigation
[Bug]: AttributeError: 'NoneType' object has no attribute 'dpi' after drawing and removing contours inside artist #21569
Description
Activity
- changed the title
[-][Bug]: AttributeError: 'NoneType' object has no attribute 'dpi' after removing contours[/-][+][Bug]: AttributeError: 'NoneType' object has no attribute 'dpi' after drawing and removing contours inside artist[/+]on Nov 8, 2021 Changing what's in the artist list to draw while drawing seems like a bad idea. There will have been a copy of the list made, sorted for zorder, removing non-visible participants, etc. It might even have been drawn already before you called
.remove.I agree w/ @QuLogic - its not clear what you are trying to accomplish here, but the following is how one would do this normally:
import numpy as np import matplotlib.pyplot as plt from matplotlib.artist import Artist class WCSAxesArtist(Artist): pass fig = plt.figure() ax = fig.add_subplot(1, 1, 1) artist = WCSAxesArtist() ax.add_artist(artist) artist._cset = artist.axes.contour(np.arange(100).reshape(10, 10), levels=10) plt.savefig('test.png') for line in artist._cset.collections: line.remove() artist._cset = artist.axes.contour(np.arange(100).reshape(10, 10), levels=10) plt.savefig('test.png')and that works as expected. I'm not quite sure how to get what you are after, but I don't think its a good idea to have
drawadding and deleting artists from the axes.- addedstatus: needs clarificationIssues that need more information to resolve.Issues that need more information to resolve.
on Nov 9, 2021 @jklymak - I agree it is a little strange - and rather than give a long explanation of how we ended up doing this in astropy.visualization, I'll try and investigate other/cleaner ways of doing this (the short version is that we are using contours to draw grid lines and we compute this and ticks, tick labels etc at draw time and we wrap everything up in a single artist).
I just wanted to make sure this wasn't a use case that should work and was bringing up an underlying memory management issue in Matplotlib.
Also note, this example failed with filled contours in v3.4.3, so the linked PR brought the return class from
contourinto line withcontourf.I guess the nicest thing to do would be to make that an axes subclass and then it will get drawn with other axes, rather than inside a parent axes list.
It would be interesting to understand why this broke if it worked before.
It is because we have changed the Collection type, and the new PathCollection intercepts draw and uses the
self.figure.dpiproperty, whereas the LineCollection doesn't reference the figure at all in its draw() routine.
matplotlib/lib/matplotlib/collections.py
Lines 973 to 975 in 162c073
def draw(self, renderer): self.set_sizes(self._sizes, self.figure.dpi) super().draw(renderer) A quick "fix" to make this use-case work could be adding in a
getattr(self.figure, "dpi", 72)to pass a default value intoset_sizes, but that does not seem ideal and would lead to whack-a-mole for finding these broken figure attributes in other areas as well.Everytime I see a reference to dpi this high up in the draw tree, I get suspicious that we are doing something wrong. Only the backends should need to know what their dpi is.
Not sure that fixing this would fix the problem though. For some reason there are objects kicking around that have been orphaned from their figure, but still exist in a list of valid artists. That just seems ripe for other bad issues. I assume it all gets cleaned up by the next draw?
Luckily
Axes.contouris very thin:matplotlib/lib/matplotlib/axes/_axes.py
Lines 6257 to 6271 in 3af610c
@_preprocess_data() @docstring.dedent_interpd def contour(self, *args, **kwargs): """ Plot contour lines. Call signature:: contour([X, Y,] Z, [levels], **kwargs) %(contour_doc)s """ kwargs['filled'] = False contours = mcontour.QuadContourSet(self, *args, **kwargs) self._request_autoscale_view() return contours On the downside,
QuadContourSetadds itself to the axes on init which makes it impossible to do something likedef draw(self, renderer): art = ContourArtist(...) art.set_axes(self.axes) # tell the artist about the Axes, but not the other way around! art.draw(renderer)
The call to
set_sizesis being used to update a the transform array (which is a Nx3x3 array, not aTranformobject) because for most sized collections the "user facing" unit is "square pixels" (and theself._factorscales for different shapes)
matplotlib/lib/matplotlib/collections.py
Lines 948 to 970 in 3af610c
def set_sizes(self, sizes, dpi=72.0): """ Set the sizes of each member of the collection. Parameters ---------- sizes : ndarray or None The size to set for each element of the collection. The value is the 'area' of the element. dpi : float, default: 72 The dpi of the canvas. """ if sizes is None: self._sizes = np.array([]) self._transforms = np.empty((0, 3, 3)) else: self._sizes = np.asarray(sizes) self._transforms = np.zeros((len(self._sizes), 3, 3)) scale = np.sqrt(self._sizes) * dpi / 72.0 * self._factor self._transforms[:, 0, 0] = scale self._transforms[:, 1, 1] = scale self._transforms[:, 2, 2] = 1.0 self.stale = True Unfortunately, dpi is not something that we expect on the base renderer (there is a
RendererBase.points_to_pixels, but I think that will give use different results for at least svg), but we do know that as part of the save we set the dpi on the figure (so the dpi transform is right).
I wonder if previously we were always drawing the "old" contours or if this is also fallout from unifying Artist storage into one list?
@astrofrog At this point we are not planning to hold 3.5.0 over this as you have a work-around on the astropy side and I'm not sure that this ever actually did what you wanted it to do...
Yes feel free to even close this if you like unless there are some underlying issues you think need fixing. Thanks everyone for your replies!
OK, I'm going to close this as while I think there is a bunch of things that we could do to work around this, none of them are quick and we have other reasons for wanting to do them.
Long term, this sort of thing (a contour plot that updates its levels as a function of view limits based on a fixed input dataset) is the sort of thing that should be possible (and maybe even easy) after the work we are planning to do with the ROSES grant.
Bug summary
I am running into issues with removing contour lines with Matplotlib 3.5.0rc1 - this worked with previous stable versions of Matplotlib, so it is a regression. I think this started failing with #19623.
The bug is triggered when contour lines are added and removed inside an artist. The example code below may appear a little convoluted, but it represents how the contours are drawn in WCSAxes, and the error happens the second time the figure is saved (once the contours have been removed and re-added).
Using objgraph reveals that after the contour lines are removed from the plot, there are residual references to the original PathCollection objects which are causing them to be drawn, but since
.figureis no longer set on them, the code crashes.Code for reproduction
Actual outcome
An error occurs the second time the figure is saved:
Expected outcome
No console output
Operating system
MacOS X
Matplotlib Version
3.5.0rc1
Matplotlib Backend
Agg
Python version
3.9.7
Jupyter version
No response
Other libraries
No response
Installation
pip
Conda channel
No response