Skip to content

[Bug] Error: 'PathCollection' object has no attribute 'do_3d_projection' when doing contourf in 3d with extend = 'both' #21016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
heavysink opened this issue Sep 8, 2021 · 3 comments · Fixed by #21026

Comments

@heavysink
Copy link

heavysink commented Sep 8, 2021

Bug summary

When doing contourf in 3D with given levels and extend = 'both', matplotlib will raise error 'PathCollection' object has no attribute 'do_3d_projection'.

Code for reproduction

from mpl_toolkits.mplot3d import axes3d
import matplotlib.pyplot as plt
from matplotlib import cm
import numpy as np

ax = plt.figure().add_subplot(projection='3d')
X, Y, Z = axes3d.get_test_data(0.1)
levels = np.arange(-70.0,70.0,1.0)

# Plot projections of the contours for each dimension.  By choosing offsets
# that match the appropriate axes limits, the projected contours will sit on
# the 'walls' of the graph
cset = ax.contourf(X, Y, Z, zdir='z', offset=-100, levels = levels, extend='both', cmap=cm.coolwarm)

ax.set_xlim(-40, 40)
ax.set_ylim(-40, 40)
ax.set_zlim(-100, 100)

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')

plt.show()

Actual outcome

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/usr/lib/python3.9/site-packages/IPython/core/formatters.py in __call__(self, obj)
    339                 pass
    340             else:
--> 341                 return printer(obj)
    342             # Finally look for special method names
    343             method = get_real_method(obj, self.print_method)

/usr/lib/python3.9/site-packages/IPython/core/pylabtools.py in <lambda>(fig)
    248 
    249     if 'png' in formats:
--> 250         png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs))
    251     if 'retina' in formats or 'png2x' in formats:
    252         png_formatter.for_type(Figure, lambda fig: retina_figure(fig, **kwargs))

/usr/lib/python3.9/site-packages/IPython/core/pylabtools.py in print_figure(fig, fmt, bbox_inches, **kwargs)
    132         FigureCanvasBase(fig)
    133 
--> 134     fig.canvas.print_figure(bytes_io, **kw)
    135     data = bytes_io.getvalue()
    136     if fmt == 'svg':

/usr/lib/python3.9/site-packages/matplotlib/backend_bases.py in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
   2228                        else suppress())
   2229                 with ctx:
-> 2230                     self.figure.draw(renderer)
   2231 
   2232             if bbox_inches:

/usr/lib/python3.9/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     72     @wraps(draw)
     73     def draw_wrapper(artist, renderer, *args, **kwargs):
---> 74         result = draw(artist, renderer, *args, **kwargs)
     75         if renderer._rasterizing:
     76             renderer.stop_rasterizing()

/usr/lib/python3.9/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

/usr/lib/python3.9/site-packages/matplotlib/figure.py in draw(self, renderer)
   2778 
   2779             self.patch.draw(renderer)
-> 2780             mimage._draw_list_compositing_images(
   2781                 renderer, self, artists, self.suppressComposite)
   2782 

/usr/lib/python3.9/site-packages/matplotlib/image.py in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
    130     if not_composite or not has_images:
    131         for a in artists:
--> 132             a.draw(renderer)
    133     else:
    134         # Composite any adjacent images together

/usr/lib/python3.9/site-packages/matplotlib/artist.py in draw_wrapper(artist, renderer, *args, **kwargs)
     49                 renderer.start_filter()
     50 
---> 51             return draw(artist, renderer, *args, **kwargs)
     52         finally:
     53             if artist.get_agg_filter() is not None:

/usr/lib/python3.9/site-packages/mpl_toolkits/mplot3d/axes3d.py in draw(self, renderer)
    483                                 for axis in self._get_axis_list()) + 1
    484             for i, col in enumerate(
--> 485                     sorted(self.collections,
    486                            key=do_3d_projection,
    487                            reverse=True)):

/usr/lib/python3.9/site-packages/mpl_toolkits/mplot3d/axes3d.py in do_3d_projection(artist)
    476                     "do_3d_projection() was deprecated in Matplotlib "
    477                     "%(since)s and will be removed %(removal)s.")
--> 478                 return artist.do_3d_projection(renderer)
    479 
    480             # Calculate projection of collections and patches and zorder them.

AttributeError: 'PathCollection' object has no attribute 'do_3d_projection'

Expected outcome

Removing extend='both' the code works fine

2021-09-08_01-58

Operating system

Arch

Matplotlib Version

3.4.2

Matplotlib Backend

module://matplotlib_inline.backend_inline

Python version

python 3.9.6

Jupyter version

3.1.6

Other libraries

No response

Installation

Linux package manager (Debian/Fedora/etc.)

Conda channel

No response

@dstansby
Copy link
Member

dstansby commented Sep 8, 2021

Weird... this is broken at least as far back as 3.1.x, so I suspect has always been broken. I've had a bit of a dig, but not having much luck finding the root cause.

@QuLogic
Copy link
Member

QuLogic commented Sep 9, 2021

contourf uses a PathCollection, contour uses a LineCollection, though both now use a PathCollection on master. The Axes3D attempts to convert a filled contour to 3D as a PolyCollection; this apparently does nothing with a PathCollection. I don't know how long that has been out of sync. Similarly, unfilled contours on master should be broken since they've changed types, but I guess it happens to work because those are at least closer types.

@QuLogic
Copy link
Member

QuLogic commented Sep 9, 2021

OK, types aside, it is able to convert them reasonably well. The problem is here:

def add_contourf_set(self, cset, zdir='z', offset=None):
zdir = '-' + zdir
for z, linec in zip(cset.levels, cset.collections):
if offset is not None:
z = offset
art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir)
linec.set_sort_zpos(z)

For a filled contour (with extend?), there is one more collection than there are levels, so the last one is not converted to 3D.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants