diff --git a/doc/users/next_whats_new/mplot3d_modification.rst b/doc/users/next_whats_new/mplot3d_modification.rst new file mode 100644 index 000000000000..0ed3735a0e3b --- /dev/null +++ b/doc/users/next_whats_new/mplot3d_modification.rst @@ -0,0 +1,8 @@ +3D Collection properties are now modifiable +------------------------------------------- + +Previously, properties of a 3D Collection that were used for 3D effects (e.g., +colors were modified to produce depth shading) could not be changed after it +was created. + +Now it is possible to modify all properties of 3D Collections at any time. diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index e936f4f28577..d930b52966c9 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -403,6 +403,32 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): super().__init__(*args, **kwargs) self.set_3d_properties(zs, zdir) + def get_depthshade(self): + return self._depthshade + + def set_depthshade(self, depthshade): + """ + Set whether depth shading is performed on collection members. + + Parameters + ---------- + depthshade : bool + Whether to shade the patches in order to give the appearance of + depth. + """ + self._depthshade = depthshade + self.stale = True + + def set_facecolor(self, c): + # docstring inherited + super().set_facecolor(c) + self._facecolor3d = self.get_facecolor() + + def set_edgecolor(self, c): + # docstring inherited + super().set_edgecolor(c) + self._edgecolor3d = self.get_edgecolor() + def set_sort_zpos(self, val): """Set the position to use for z-sorting.""" self._sort_zpos = val @@ -430,13 +456,13 @@ def do_3d_projection(self, renderer): fcs = (_zalpha(self._facecolor3d, vzs) if self._depthshade else self._facecolor3d) fcs = mcolors.to_rgba_array(fcs, self._alpha) - self.set_facecolors(fcs) + super().set_facecolor(fcs) ecs = (_zalpha(self._edgecolor3d, vzs) if self._depthshade else self._edgecolor3d) ecs = mcolors.to_rgba_array(ecs, self._alpha) - self.set_edgecolors(ecs) - PatchCollection.set_offsets(self, np.column_stack([vxs, vys])) + super().set_edgecolor(ecs) + super().set_offsets(np.column_stack([vxs, vys])) if vzs.size > 0: return min(vzs) @@ -491,6 +517,42 @@ def set_3d_properties(self, zs, zdir): self._linewidth3d = self.get_linewidth() self.stale = True + def get_depthshade(self): + return self._depthshade + + def set_depthshade(self, depthshade): + """ + Set whether depth shading is performed on collection members. + + Parameters + ---------- + depthshade : bool + Whether to shade the patches in order to give the appearance of + depth. + """ + self._depthshade = depthshade + self.stale = True + + def set_facecolor(self, c): + # docstring inherited + super().set_facecolor(c) + self._facecolor3d = self.get_facecolor() + + def set_edgecolor(self, c): + # docstring inherited + super().set_edgecolor(c) + self._edgecolor3d = self.get_edgecolor() + + def set_sizes(self, sizes, dpi=72.0): + # docstring inherited + super().set_sizes(sizes, dpi=dpi) + self._sizes3d = self.get_sizes() + + def set_linewidth(self, lw): + # docstring inherited + super().set_linewidth(lw) + self._linewidth3d = self.get_linewidth() + def do_3d_projection(self, renderer): xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) @@ -524,10 +586,10 @@ def do_3d_projection(self, renderer): fcs = mcolors.to_rgba_array(fcs, self._alpha) ecs = mcolors.to_rgba_array(ecs, self._alpha) - self.set_edgecolors(ecs) - self.set_facecolors(fcs) - self.set_sizes(sizes) - self.set_linewidth(lws) + super().set_edgecolor(ecs) + super().set_facecolor(fcs) + super().set_sizes(sizes) + super().set_linewidth(lws) PathCollection.set_offsets(self, vps) @@ -722,10 +784,12 @@ def do_3d_projection(self, renderer): return np.nan def set_facecolor(self, colors): + # docstring inherited super().set_facecolor(colors) self._facecolors3d = PolyCollection.get_facecolor(self) def set_edgecolor(self, colors): + # docstring inherited super().set_edgecolor(colors) self._edgecolors3d = PolyCollection.get_edgecolor(self) diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index b68c250496ed..8a6e5228296f 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -243,6 +243,27 @@ def test_scatter3d_color(): color='b', marker='s') +@check_figures_equal(extensions=['png']) +def test_scatter3d_modification(fig_ref, fig_test): + # Changing Path3DCollection properties post-creation should work correctly. + ax_test = fig_test.add_subplot(projection='3d') + c = ax_test.scatter(np.arange(10), np.arange(10), np.arange(10), + marker='o') + c.set_facecolor('C1') + c.set_edgecolor('C2') + c.set_alpha([0.3, 0.7] * 5) + assert c.get_depthshade() + c.set_depthshade(False) + assert not c.get_depthshade() + c.set_sizes(np.full(10, 75)) + c.set_linewidths(3) + + ax_ref = fig_ref.add_subplot(projection='3d') + ax_ref.scatter(np.arange(10), np.arange(10), np.arange(10), marker='o', + facecolor='C1', edgecolor='C2', alpha=[0.3, 0.7] * 5, + depthshade=False, s=75, linewidths=3) + + @pytest.mark.parametrize('depthshade', [True, False]) @check_figures_equal(extensions=['png']) def test_scatter3d_sorting(fig_ref, fig_test, depthshade): @@ -526,6 +547,30 @@ def test_quiver3d_masked(): ax.quiver(x, y, z, u, v, w, length=0.1, pivot='tip', normalize=True) +@check_figures_equal(extensions=['png']) +def test_patch_collection_modification(fig_test, fig_ref): + # Test that modifying Patch3DCollection properties after creation works. + patch = Circle((0, 0), 0.05) + c = art3d.Patch3DCollection([patch], linewidths=3) + + ax_test = fig_test.add_subplot(projection='3d') + ax_test.add_collection3d(c) + c.set_edgecolor('C2') + c.set_facecolor('C3') + c.set_alpha(0.7) + assert c.get_depthshade() + c.set_depthshade(False) + assert not c.get_depthshade() + + patch = Circle((0, 0), 0.05) + c = art3d.Patch3DCollection([patch], linewidths=3, + edgecolor='C2', facecolor='C3', alpha=0.7, + depthshade=False) + + ax_ref = fig_ref.add_subplot(projection='3d') + ax_ref.add_collection3d(c) + + @mpl3d_image_comparison(['poly3dcollection_closed.png']) def test_poly3dcollection_closed(): fig = plt.figure() @@ -558,8 +603,10 @@ def test_poly3dcollection_alpha(): c1 = art3d.Poly3DCollection([poly1], linewidths=3, edgecolor='k', facecolor=(0.5, 0.5, 1), closed=True) c1.set_alpha(0.5) - c2 = art3d.Poly3DCollection([poly2], linewidths=3, edgecolor='k', - facecolor=(1, 0.5, 0.5), closed=False) + c2 = art3d.Poly3DCollection([poly2], linewidths=3, closed=False) + # Post-creation modification should work. + c2.set_facecolor((1, 0.5, 0.5)) + c2.set_edgecolor('k') c2.set_alpha(0.5) ax.add_collection3d(c1) ax.add_collection3d(c2)