diff --git a/doc/users/next_whats_new/axes3d_computed_zorder.rst b/doc/users/next_whats_new/axes3d_computed_zorder.rst new file mode 100644 index 000000000000..d40d9334988b --- /dev/null +++ b/doc/users/next_whats_new/axes3d_computed_zorder.rst @@ -0,0 +1,6 @@ +Axes3D now allows manual control of draw order +---------------------------------------------- + +The :class:`~mpl_toolkits.mplot3d.axes3d.Axes3D` class now has +``computed_zorder`` parameter. When set to False, Artists are drawn using their +``zorder`` attribute. diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 44fffb740539..9060e4543aae 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -49,7 +49,7 @@ class Axes3D(Axes): def __init__( self, fig, rect=None, *args, azim=-60, elev=30, sharez=None, proj_type='persp', - box_aspect=None, + box_aspect=None, computed_zorder=True, **kwargs): """ Parameters @@ -66,6 +66,15 @@ def __init__( Other axes to share z-limits with. proj_type : {'persp', 'ortho'} The projection type, default 'persp'. + computed_zorder : bool, default: True + If True, the draw order is computed based on the average position + of the `.Artist`s along the view direction. + Set to False if you want to manually control the order in which + Artists are drawn on top of each other using their *zorder* + attribute. This can be used for fine-tuning if the automatic order + does not produce the desired result. Note however, that a manual + zorder will only be correct for a limited view angle. If the figure + is rotated by the user, it will look wrong from certain angles. auto_add_to_figure : bool, default: True Prior to Matplotlib 3.4 Axes3D would add themselves to their host Figure on init. Other Axes class do not @@ -79,11 +88,6 @@ def __init__( Other optional keyword arguments: %(Axes3D_kwdoc)s - - Notes - ----- - .. versionadded:: 1.2.1 - The *sharez* parameter. """ if rect is None: @@ -92,6 +96,7 @@ def __init__( self.initial_azim = azim self.initial_elev = elev self.set_proj_type(proj_type) + self.computed_zorder = computed_zorder self.xy_viewLim = Bbox.unit() self.zz_viewLim = Bbox.unit() @@ -477,20 +482,26 @@ def do_3d_projection(artist): "%(since)s and will be removed %(removal)s.") return artist.do_3d_projection(renderer) - # Calculate projection of collections and patches and zorder them. - # Make sure they are drawn above the grids. - zorder_offset = max(axis.get_zorder() - for axis in self._get_axis_list()) + 1 - for i, col in enumerate( - sorted(self.collections, - key=do_3d_projection, - reverse=True)): - col.zorder = zorder_offset + i - for i, patch in enumerate( - sorted(self.patches, - key=do_3d_projection, - reverse=True)): - patch.zorder = zorder_offset + i + if self.computed_zorder: + # Calculate projection of collections and patches and zorder + # them. Make sure they are drawn above the grids. + zorder_offset = max(axis.get_zorder() + for axis in self._get_axis_list()) + 1 + for i, col in enumerate( + sorted(self.collections, + key=do_3d_projection, + reverse=True)): + col.zorder = zorder_offset + i + for i, patch in enumerate( + sorted(self.patches, + key=do_3d_projection, + reverse=True)): + patch.zorder = zorder_offset + i + else: + for col in self.collections: + col.do_3d_projection() + for patch in self.patches: + patch.do_3d_projection() if self._axis3don: # Draw panes first @@ -3505,6 +3516,7 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', stem3D = stem + docstring.interpd.update(Axes3D_kwdoc=artist.kwdoc(Axes3D)) docstring.dedent_interpd(Axes3D.__init__) diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/computed_zorder.png b/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/computed_zorder.png new file mode 100644 index 000000000000..887d409b72c7 Binary files /dev/null and b/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/computed_zorder.png differ diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index cfcc383db1a4..1fcb5b500069 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -1425,3 +1425,86 @@ def test_subfigure_simple(): sf = fig.subfigures(1, 2) ax = sf[0].add_subplot(1, 1, 1, projection='3d') ax = sf[1].add_subplot(1, 1, 1, projection='3d', label='other') + + +@image_comparison(baseline_images=['computed_zorder'], remove_text=True, + extensions=['png']) +def test_computed_zorder(): + fig = plt.figure() + ax1 = fig.add_subplot(221, projection='3d') + ax2 = fig.add_subplot(222, projection='3d') + ax2.computed_zorder = False + + # create a horizontal plane + corners = ((0, 0, 0), (0, 5, 0), (5, 5, 0), (5, 0, 0)) + for ax in (ax1, ax2): + tri = art3d.Poly3DCollection([corners], + facecolors='white', + edgecolors='black', + zorder=1) + ax.add_collection3d(tri) + + # plot a vector + ax.plot((2, 2), (2, 2), (0, 4), c='red', zorder=2) + + # plot some points + ax.scatter((3, 3), (1, 3), (1, 3), c='red', zorder=10) + + ax.set_xlim((0, 5.0)) + ax.set_ylim((0, 5.0)) + ax.set_zlim((0, 2.5)) + + ax3 = fig.add_subplot(223, projection='3d') + ax4 = fig.add_subplot(224, projection='3d') + ax4.computed_zorder = False + + dim = 10 + X, Y = np.meshgrid((-dim, dim), (-dim, dim)) + Z = np.zeros((2, 2)) + + angle = 0.5 + X2, Y2 = np.meshgrid((-dim, dim), (0, dim)) + Z2 = Y2 * angle + X3, Y3 = np.meshgrid((-dim, dim), (-dim, 0)) + Z3 = Y3 * angle + + r = 7 + M = 1000 + th = np.linspace(0, 2 * np.pi, M) + x, y, z = r * np.cos(th), r * np.sin(th), angle * r * np.sin(th) + for ax in (ax3, ax4): + ax.plot_surface(X2, Y3, Z3, + color='blue', + alpha=0.5, + linewidth=0, + zorder=-1) + ax.plot(x[y < 0], y[y < 0], z[y < 0], + lw=5, + linestyle='--', + color='green', + zorder=0) + + ax.plot_surface(X, Y, Z, + color='red', + alpha=0.5, + linewidth=0, + zorder=1) + + ax.plot(r * np.sin(th), r * np.cos(th), np.zeros(M), + lw=5, + linestyle='--', + color='black', + zorder=2) + + ax.plot_surface(X2, Y2, Z2, + color='blue', + alpha=0.5, + linewidth=0, + zorder=3) + + ax.plot(x[y > 0], y[y > 0], z[y > 0], lw=5, + linestyle='--', + color='green', + zorder=4) + ax.view_init(azim=-20, elev=20) + ax.axis('off')