From e59721eb67ccf6fb8656127335d7b4d5e1f80741 Mon Sep 17 00:00:00 2001 From: patquem Date: Fri, 25 Mar 2022 11:22:45 +0100 Subject: [PATCH 1/5] Provide axis('equal') for Axes3D Whats new for 3D plot equal aspect ratio Code review updates set_aspect('equal') now adopts current box aspect Update test image Update whats new Update whats new test image --- doc/users/next_whats_new/3d_plot_aspects.rst | 28 ++++++++++++++++++ lib/mpl_toolkits/mplot3d/axes3d.py | 26 +++++++++------- .../baseline_images/test_mplot3d/aspects.png | Bin 0 -> 37033 bytes lib/mpl_toolkits/tests/test_mplot3d.py | 22 ++++++++++++-- 4 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 doc/users/next_whats_new/3d_plot_aspects.rst create mode 100644 lib/mpl_toolkits/tests/baseline_images/test_mplot3d/aspects.png diff --git a/doc/users/next_whats_new/3d_plot_aspects.rst b/doc/users/next_whats_new/3d_plot_aspects.rst new file mode 100644 index 000000000000..689fdaf741b5 --- /dev/null +++ b/doc/users/next_whats_new/3d_plot_aspects.rst @@ -0,0 +1,28 @@ +Set equal aspect ratio for 3D plots +----------------------------------- + +Users can set the aspect ratio for the X, Y, Z axes of a 3D plot to be 'equal', +'equalxy', 'equalxz', or 'equalyz' rather than the default of 'auto'. + +.. plot:: + :include-source: true + + aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz') + fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'}) + + # Draw rectangular cuboid with side lengths [1, 1, 2] + r = [0, 1] + scale = np.array([1, 1, 2]) + pts = itertools.combinations(np.array(list(itertools.product(r, r, r))), 2) + for start, end in pts: + if np.sum(np.abs(start - end)) == r[1] - r[0]: + for ax in axs: + ax.plot3D(*zip(start*scale, end*scale), color='C0') + + # Set the aspect ratios + for i, ax in enumerate(axs): + ax.set_box_aspect((3, 4, 5)) + ax.set_aspect(aspects[i]) + ax.title(f"set_aspect('{aspects[i]}')") + + plt.show() diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index d50ad5235ccd..d317681040e4 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -272,22 +272,16 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): """ Set the aspect ratios. - Axes 3D does not current support any aspect but 'auto' which fills - the Axes with the data limits. - - To simulate having equal aspect in data space, set the ratio - of your data limits to match the value of `.get_box_aspect`. - To control box aspect ratios use `~.Axes3D.set_box_aspect`. - Parameters ---------- - aspect : {'auto'} + aspect : {'auto', 'equal'} Possible values: ========= ================================================== value description ========= ================================================== 'auto' automatic; fill the position rectangle with data. + 'equal' adapt the axes to have equal aspect ratios. ========= ================================================== adjustable : None @@ -321,14 +315,26 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): -------- mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect """ - if aspect != 'auto': + if aspect not in ('auto', 'equal'): raise NotImplementedError( "Axes3D currently only supports the aspect argument " - f"'auto'. You passed in {aspect!r}." + f"'auto' or 'equal'. You passed in {aspect!r}." ) super().set_aspect( aspect, adjustable=adjustable, anchor=anchor, share=share) + if aspect == 'equal': + v_intervals = np.vstack((self.xaxis.get_view_interval(), + self.yaxis.get_view_interval(), + self.zaxis.get_view_interval())) + mean = np.mean(v_intervals, axis=1) + delta = np.max(np.ptp(v_intervals, axis=1)) + deltas = delta * self._box_aspect / min(self._box_aspect) + + self.set_xlim3d(mean[0] - deltas[0] / 2., mean[0] + deltas[0] / 2.) + self.set_ylim3d(mean[1] - deltas[1] / 2., mean[1] + deltas[1] / 2.) + self.set_zlim3d(mean[2] - deltas[2] / 2., mean[2] + deltas[2] / 2.) + def set_box_aspect(self, aspect, *, zoom=1): """ Set the Axes box aspect. diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/aspects.png b/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/aspects.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb088e2d131a04afcadbe5ed50be42d3d38c1b3 GIT binary patch literal 37033 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

gp`chqe##e`?=5)4!42kMs>TXfwvvCv4f# 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

#cz2M=6Q20a9@*yFSr?mJ2L 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 Date: Thu, 14 Jul 2022 13:41:58 -0500 Subject: [PATCH 2/5] Add equalxy, equalyz, equalxz aspect ratios Update docstrings --- doc/users/next_whats_new/3d_plot_aspects.rst | 8 +++- lib/mpl_toolkits/mplot3d/axes3d.py | 47 ++++++++++++-------- lib/mpl_toolkits/tests/test_mplot3d.py | 37 +++++++-------- 3 files changed, 51 insertions(+), 41 deletions(-) diff --git a/doc/users/next_whats_new/3d_plot_aspects.rst b/doc/users/next_whats_new/3d_plot_aspects.rst index 689fdaf741b5..03aa427fca97 100644 --- a/doc/users/next_whats_new/3d_plot_aspects.rst +++ b/doc/users/next_whats_new/3d_plot_aspects.rst @@ -7,13 +7,17 @@ Users can set the aspect ratio for the X, Y, Z axes of a 3D plot to be 'equal', .. plot:: :include-source: true + import matplotlib.pyplot as plt + import numpy as np + from itertools import combinations, product + aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz') fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'}) # Draw rectangular cuboid with side lengths [1, 1, 2] r = [0, 1] scale = np.array([1, 1, 2]) - pts = itertools.combinations(np.array(list(itertools.product(r, r, r))), 2) + pts = combinations(np.array(list(product(r, r, r))), 2) for start, end in pts: if np.sum(np.abs(start - end)) == r[1] - r[0]: for ax in axs: @@ -23,6 +27,6 @@ Users can set the aspect ratio for the X, Y, Z axes of a 3D plot to be 'equal', for i, ax in enumerate(axs): ax.set_box_aspect((3, 4, 5)) ax.set_aspect(aspects[i]) - ax.title(f"set_aspect('{aspects[i]}')") + ax.set_title("set_aspect('{aspects[i]}')") plt.show() diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index d317681040e4..7e91afc85876 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -274,14 +274,17 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): Parameters ---------- - aspect : {'auto', 'equal'} + aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'} Possible values: ========= ================================================== value description ========= ================================================== 'auto' automatic; fill the position rectangle with data. - 'equal' adapt the axes to have equal aspect ratios. + 'equal' adapt all the axes to have equal aspect ratios. + 'equalxy' adapt the x and y axes to have equal aspect ratios. + 'equalxz' adapt the x and z axes to have equal aspect ratios. + 'equalyz' adapt the y and z axes to have equal aspect ratios. ========= ================================================== adjustable : None @@ -315,25 +318,33 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): -------- mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect """ - if aspect not in ('auto', 'equal'): - raise NotImplementedError( - "Axes3D currently only supports the aspect argument " - f"'auto' or 'equal'. You passed in {aspect!r}." - ) + _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'), + aspect=aspect) super().set_aspect( - aspect, adjustable=adjustable, anchor=anchor, share=share) - - if aspect == 'equal': - v_intervals = np.vstack((self.xaxis.get_view_interval(), - self.yaxis.get_view_interval(), - self.zaxis.get_view_interval())) - mean = np.mean(v_intervals, axis=1) - delta = np.max(np.ptp(v_intervals, axis=1)) + aspect='auto', adjustable=adjustable, anchor=anchor, share=share) + + if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): + if aspect == 'equal': + axis_indices = [0, 1, 2] + elif aspect == 'equalxy': + axis_indices = [0, 1] + elif aspect == 'equalxz': + axis_indices = [0, 2] + elif aspect == 'equalyz': + axis_indices = [1, 2] + + view_intervals = np.array([self.xaxis.get_view_interval(), + self.yaxis.get_view_interval(), + self.zaxis.get_view_interval()]) + mean = np.mean(view_intervals, axis=1) + delta = np.max(np.ptp(view_intervals, axis=1)) deltas = delta * self._box_aspect / min(self._box_aspect) - self.set_xlim3d(mean[0] - deltas[0] / 2., mean[0] + deltas[0] / 2.) - self.set_ylim3d(mean[1] - deltas[1] / 2., mean[1] + deltas[1] / 2.) - self.set_zlim3d(mean[2] - deltas[2] / 2., mean[2] + deltas[2] / 2.) + for i, set_lim in enumerate((self.set_xlim3d, + self.set_ylim3d, + self.set_zlim3d)): + if i in axis_indices: + set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.) def set_box_aspect(self, aspect, *, zoom=1): """ diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index e396844984b6..e6a232064a73 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -27,27 +27,22 @@ def test_invisible_axes(fig_test, fig_ref): ax.set_visible(False) -@mpl3d_image_comparison(['aspect_equal.png'], remove_text=False) -def test_aspect_equal(): - fig = plt.figure() - ax = fig.add_subplot(projection='3d') - - points = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], - [0, 0, 3], [1, 0, 3], [0, 1, 3], [1, 1, 3]]) - - x = np.asarray([coord[0] for coord in points]) - y = np.asarray([coord[1] for coord in points]) - z = np.asarray([coord[2] for coord in points]) - - def plot_edge(i, j): - ax.plot([x[i], x[j]], [y[i], y[j]], [z[i], z[j]], c='r') - - # hexaedron creation - plot_edge(0, 1), plot_edge(1, 3), plot_edge(3, 2), plot_edge(2, 0) - plot_edge(4, 5), plot_edge(5, 7), plot_edge(7, 6), plot_edge(6, 4) - plot_edge(0, 4), plot_edge(1, 5), plot_edge(3, 7), plot_edge(2, 6) - - ax.set_aspect('equal') +@mpl3d_image_comparison(['aspects.png'], remove_text=False) +def test_aspects(): + aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz') + fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'}) + + # Draw rectangular cuboid with side lengths [1, 1, 2] + r = [0, 1] + scale = np.array([1, 1, 2]) + pts = itertools.combinations(np.array(list(itertools.product(r, r, r))), 2) + for start, end in pts: + if np.sum(np.abs(start - end)) == r[1] - r[0]: + for ax in axs: + ax.plot3D(*zip(start*scale, end*scale)) + for i, ax in enumerate(axs): + ax.set_box_aspect((3, 4, 5)) + ax.set_aspect(aspects[i]) @mpl3d_image_comparison(['bar3d.png']) From da0e6070bba6e0821fae9220fd3ba462777f6fca Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Thu, 21 Jul 2022 19:04:28 -0600 Subject: [PATCH 3/5] Use all available plot space --- doc/users/next_whats_new/3d_plot_aspects.rst | 4 ++-- lib/mpl_toolkits/mplot3d/axes3d.py | 16 +++++++++------- lib/mpl_toolkits/tests/test_mplot3d.py | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/doc/users/next_whats_new/3d_plot_aspects.rst b/doc/users/next_whats_new/3d_plot_aspects.rst index 03aa427fca97..ebd7afae92ef 100644 --- a/doc/users/next_whats_new/3d_plot_aspects.rst +++ b/doc/users/next_whats_new/3d_plot_aspects.rst @@ -14,9 +14,9 @@ Users can set the aspect ratio for the X, Y, Z axes of a 3D plot to be 'equal', aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz') fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'}) - # Draw rectangular cuboid with side lengths [1, 1, 2] + # Draw rectangular cuboid with side lengths [1, 1, 5] r = [0, 1] - scale = np.array([1, 1, 2]) + scale = np.array([1, 1, 5]) pts = combinations(np.array(list(product(r, r, r))), 2) for start, end in pts: if np.sum(np.abs(start - end)) == r[1] - r[0]: diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 7e91afc85876..a0ebecd332ab 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -325,25 +325,27 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): if aspect == 'equal': - axis_indices = [0, 1, 2] + ax_indices = [0, 1, 2] elif aspect == 'equalxy': - axis_indices = [0, 1] + ax_indices = [0, 1] elif aspect == 'equalxz': - axis_indices = [0, 2] + ax_indices = [0, 2] elif aspect == 'equalyz': - axis_indices = [1, 2] + ax_indices = [1, 2] view_intervals = np.array([self.xaxis.get_view_interval(), self.yaxis.get_view_interval(), self.zaxis.get_view_interval()]) mean = np.mean(view_intervals, axis=1) - delta = np.max(np.ptp(view_intervals, axis=1)) - deltas = delta * self._box_aspect / min(self._box_aspect) + ptp = np.ptp(view_intervals, axis=1) + delta = max(ptp[ax_indices]) + scale = self._box_aspect[ptp == delta][0] + deltas = delta * self._box_aspect / scale for i, set_lim in enumerate((self.set_xlim3d, self.set_ylim3d, self.set_zlim3d)): - if i in axis_indices: + if i in ax_indices: set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.) def set_box_aspect(self, aspect, *, zoom=1): diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index e6a232064a73..91fee3dcfe38 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -32,9 +32,9 @@ def test_aspects(): aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz') fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'}) - # Draw rectangular cuboid with side lengths [1, 1, 2] + # Draw rectangular cuboid with side lengths [1, 1, 5] r = [0, 1] - scale = np.array([1, 1, 2]) + scale = np.array([1, 1, 5]) pts = itertools.combinations(np.array(list(itertools.product(r, r, r))), 2) for start, end in pts: if np.sum(np.abs(start - end)) == r[1] - r[0]: From 7141d2c5ad846065d54614fdab4d4aaadcc35d4f Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Thu, 21 Jul 2022 19:50:10 -0600 Subject: [PATCH 4/5] Update relevant gallery images --- examples/mplot3d/surface3d_2.py | 3 +++ examples/mplot3d/voxels_numpy_logo.py | 1 + examples/mplot3d/voxels_rgb.py | 1 + 3 files changed, 5 insertions(+) diff --git a/examples/mplot3d/surface3d_2.py b/examples/mplot3d/surface3d_2.py index d5cfca1905a3..bca9f1ca62e8 100644 --- a/examples/mplot3d/surface3d_2.py +++ b/examples/mplot3d/surface3d_2.py @@ -23,4 +23,7 @@ # Plot the surface ax.plot_surface(x, y, z) +# Set an equal aspect ratio +ax.set_aspect('equal') + plt.show() diff --git a/examples/mplot3d/voxels_numpy_logo.py b/examples/mplot3d/voxels_numpy_logo.py index 9aa72b31e290..8b790d073988 100644 --- a/examples/mplot3d/voxels_numpy_logo.py +++ b/examples/mplot3d/voxels_numpy_logo.py @@ -42,5 +42,6 @@ def explode(data): ax = plt.figure().add_subplot(projection='3d') ax.voxels(x, y, z, filled_2, facecolors=fcolors_2, edgecolors=ecolors_2) +ax.set_aspect('equal') plt.show() diff --git a/examples/mplot3d/voxels_rgb.py b/examples/mplot3d/voxels_rgb.py index a06cc10a5cc1..31bfcbca15a9 100644 --- a/examples/mplot3d/voxels_rgb.py +++ b/examples/mplot3d/voxels_rgb.py @@ -39,5 +39,6 @@ def midpoints(x): edgecolors=np.clip(2*colors - 0.5, 0, 1), # brighter linewidth=0.5) ax.set(xlabel='r', ylabel='g', zlabel='b') +ax.set_aspect('equal') plt.show() From 0bb940ef0b0296765048fc0de49b1759c4f5bdef Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Tue, 2 Aug 2022 21:17:35 -0600 Subject: [PATCH 5/5] Save aspect to axes --- lib/mpl_toolkits/mplot3d/axes3d.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index a0ebecd332ab..5319e38ff011 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -322,6 +322,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): aspect=aspect) super().set_aspect( aspect='auto', adjustable=adjustable, anchor=anchor, share=share) + self._aspect = aspect if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): if aspect == 'equal':