From bc43279e4719821931f013750ef8fa5286e57098 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 26 May 2020 23:00:30 -0400 Subject: [PATCH 01/12] MNT: make sure Axes3D methods handle sharez correctly --- lib/matplotlib/axes/_base.py | 4 -- lib/mpl_toolkits/mplot3d/axes3d.py | 83 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 2f5f4ce7b718..da9ddeebe6d4 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1339,10 +1339,6 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): if cbook._str_equal(aspect, 'equal'): aspect = 1 if not cbook._str_equal(aspect, 'auto'): - if self.name == '3d': - raise NotImplementedError( - 'It is not currently possible to manually set the aspect ' - 'on 3D axes') aspect = float(aspect) # raise ValueError if necessary if share: diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 9ac10918604a..229785dd4716 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -24,6 +24,7 @@ import matplotlib.colors as mcolors import matplotlib.docstring as docstring import matplotlib.scale as mscale +import matplotlib.transforms as mtransforms from matplotlib.axes import Axes, rcParams from matplotlib.axes._base import _axis_method_wrapper from matplotlib.transforms import Bbox @@ -261,10 +262,92 @@ def tunit_edges(self, vals=None, M=None): (tc[7], tc[4])] return edges + def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): + """ + Set the aspect of the axis scaling. + + Parameters + ---------- + aspect : {'auto'} + Possible values: + + ========= ================================================== + value description + ========= ================================================== + 'auto' automatic; fill the position rectangle with data. + ========= ================================================== + + adjustable : None or {'box', 'datalim'}, optional + If not ``None``, this defines which parameter will be adjusted to + meet the required aspect. See `.set_adjustable` for further + details. + + Currently ignored by Axes3D + + anchor : None or str or 2-tuple of float, optional + If not ``None``, this defines where the Axes will be drawn if there + is extra space due to aspect constraints. The most common way to + to specify the anchor are abbreviations of cardinal directions: + + ===== ===================== + value description + ===== ===================== + 'C' centered + 'SW' lower left corner + 'S' middle of bottom edge + 'SE' lower right corner + etc. + ===== ===================== + + See `.set_anchor` for further details. + + share : bool, default: False + If ``True``, apply the settings to all shared Axes. + + """ + if aspect != 'auto': + raise NotImplementedError( + "Axes3D currently only support the aspect arguments " + f"'auto'. You passed in {aspect!r}." + ) + + if share: + axes = {*self._shared_x_axes.get_siblings(self), + *self._shared_y_axes.get_siblings(self), + *self._shared_z_axes.get_siblings(self), + } + else: + axes = {self} + + for ax in axes: + ax._aspect = aspect + ax.stale = True + + if anchor is not None: + self.set_anchor(anchor, share=share) + + def set_anchor(self, anchor, share=False): + # docstring inherited + if not (anchor in mtransforms.Bbox.coefs or len(anchor) == 2): + raise ValueError('argument must be among %s' % + ', '.join(mtransforms.Bbox.coefs)) + if share: + axes = {*self._shared_x_axes.get_siblings(self), + *self._shared_y_axes.get_siblings(self), + *self._shared_z_axes.get_siblings(self), + } + else: + axes = {self} + for ax in axes: + ax._anchor = anchor + ax.stale = True + def apply_aspect(self, position=None): if position is None: position = self.get_position(original=True) + aspect = self.get_aspect() + # in the superclass, we would go through and actually deal with axis # scales and box/datalim. Those are all irrelevant - all we need to do # is make sure our coordinate system is square. From b9f9d2ee06d4cd1d72bfca312660a5b55d265f31 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 26 May 2020 23:40:48 -0400 Subject: [PATCH 02/12] ENH: implement get_tightbbox on Axis3D --- lib/mpl_toolkits/mplot3d/axes3d.py | 21 ++++++++++++ lib/mpl_toolkits/mplot3d/axis3d.py | 55 ++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 229785dd4716..7c8815e5a52a 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2853,6 +2853,27 @@ def permutation_matrices(n): return polygons + def get_tightbbox(self, renderer, call_axes_locator=True, + bbox_extra_artists=None, *, for_layout_only=False): + ret = super().get_tightbbox(renderer, + call_axes_locator=call_axes_locator, + bbox_extra_artists=bbox_extra_artists, + for_layout_only=for_layout_only) + batch = [ret] + if self._axis3don: + for axis in self._get_axis_list(): + if axis.get_visible(): + try: + axis_bb = axis.get_tightbbox( + renderer, + for_layout_only=for_layout_only + ) + except TypeError: + # in case downstream library has redefined axis: + axis_bb = axis.get_tightbbox(renderer) + if axis_bb: + batch.append(axis_bb) + return mtransforms.Bbox.union(batch) docstring.interpd.update(Axes3D=artist.kwdoc(Axes3D)) docstring.dedent_interpd(Axes3D.__init__) diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index c3ca45b5df80..767f0a81842e 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -3,7 +3,7 @@ # Parts rewritten by Reinier Heeres import numpy as np - +import matplotlib.transforms as mtransforms from matplotlib import ( artist, lines as mlines, axis as maxis, patches as mpatches, rcParams) from . import art3d, proj3d @@ -398,12 +398,53 @@ def draw(self, renderer): renderer.close_group('axis3d') self.stale = False - # TODO: Get this to work properly when mplot3d supports - # the transforms framework. - def get_tightbbox(self, renderer): - # Currently returns None so that Axis.get_tightbbox - # doesn't return junk info. - return None + # TODO: Get this to work (more) properly when mplot3d supports the + # transforms framework. + def get_tightbbox(self, renderer, *, for_layout_only=False): + # inherited docstring + if not self.get_visible(): + return + # We have to directly access the internal data structures + # (and hope they are up to date) because at draw time we + # shift the ticks and their labels around in (x, y) space + # based on the projection, the current view port, and their + # position in 3D space. If we extend the transforms framework + # into 3D we would not need to do this different book keeping + # than we do in the normal axis + major_locs = self.get_majorticklocs() + minor_locs = self.get_minorticklocs() + + ticks = [*self.get_minor_ticks(len(minor_locs)), + *self.get_major_ticks(len(major_locs))] + view_low, view_high = self.get_view_interval() + if view_low > view_high: + view_low, view_high = view_high, view_low + interval_t = self.get_transform().transform([view_low, view_high]) + + ticks_to_draw = [] + for tick in ticks: + try: + loc_t = self.get_transform().transform(tick.get_loc()) + except AssertionError: + # transforms.transform doesn't allow masked values but + # some scales might make them, so we need this try/except. + pass + else: + if mtransforms._interval_contains_close(interval_t, loc_t): + ticks_to_draw.append(tick) + + ticks = ticks_to_draw + + bb_1, bb_2 = self._get_tick_bboxes(ticks, renderer) + other = [] + + if self.line.get_visible(): + other.append(self.line.get_window_extent(renderer)) + if (self.label.get_visible() and not for_layout_only and + self.label.get_text()): + other.append(self.label.get_window_extent(renderer)) + + return mtransforms.Bbox.union([*bb_1, *bb_2, *other]) @property def d_interval(self): From 8c2529ad55892cc6a228e32c005d676c55a207bf Mon Sep 17 00:00:00 2001 From: Eric Wieser Date: Tue, 18 Jul 2017 10:26:20 +0100 Subject: [PATCH 03/12] ENH: Add support for Axes3D.set_pb_aspect This only keeps the pb related changes from the original commit. --- lib/mpl_toolkits/mplot3d/axes3d.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 7c8815e5a52a..1f0f649f0651 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -91,6 +91,11 @@ def __init__( self.zz_viewLim = Bbox.unit() self.xy_dataLim = Bbox.unit() self.zz_dataLim = Bbox.unit() + if 'pb_aspect' in kwargs: + self.pb_aspect = np.asarray(kwargs['pb_aspect']) + else: + # chosen for similarity with the previous initial view + self.pb_aspect = np.array([4, 4, 3]) / 3.5 # inhibit autoscale_view until the axes are defined # they can't be defined until Axes.__init__ has been called self.view_init(self.initial_elev, self.initial_azim) @@ -342,6 +347,9 @@ def set_anchor(self, anchor, share=False): ax._anchor = anchor ax.stale = True + def set_pb_aspect(self, pb_aspect, zoom=1): + self.pb_aspect = pb_aspect * 1.8 * zoom / proj3d.mod(pb_aspect) + def apply_aspect(self, position=None): if position is None: position = self.get_position(original=True) @@ -966,7 +974,12 @@ def set_proj_type(self, proj_type): def get_proj(self): """Create the projection matrix from the current viewing position.""" # chosen for similarity with the initial view before gh-8896 - pb_aspect = np.array([4, 4, 3]) / 3.5 + + # elev stores the elevation angle in the z plane + # azim stores the azimuth angle in the x,y plane + # + # dist is the distance of the eye viewing point from the object + # point. relev, razim = np.pi * self.elev/180, np.pi * self.azim/180 @@ -977,10 +990,10 @@ def get_proj(self): # transform to uniform world coordinates 0-1, 0-1, 0-1 worldM = proj3d.world_transformation(xmin, xmax, ymin, ymax, - zmin, zmax, pb_aspect=pb_aspect) + zmin, zmax, pb_aspect=self.pb_aspect) # look into the middle of the new coordinates - R = pb_aspect / 2 + R = self.pb_aspect / 2 xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist From 6915f697f444075681406ce6bfa4b2e908821ff7 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 27 May 2020 16:02:30 -0400 Subject: [PATCH 04/12] ENH: use box_aspect to support setting pb_aspect closes #17172 Instead of adding a new method, reuse `box_aspect` expecting a 3 vector. The very long float is to keep the tests passing and preserve the behavior currently on master branch. --- .../2019-03-25-mplot3d-projection.rst | 11 +++- lib/mpl_toolkits/mplot3d/axes3d.py | 61 ++++++++++++++---- .../test_mplot3d/equal_box_aspect.png | Bin 0 -> 55467 bytes lib/mpl_toolkits/tests/test_mplot3d.py | 33 ++++++++++ 4 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 lib/mpl_toolkits/tests/baseline_images/test_mplot3d/equal_box_aspect.png diff --git a/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst b/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst index d60a4a921c45..f8e72fbdfab7 100644 --- a/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst +++ b/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst @@ -4,6 +4,13 @@ Plots made with :class:`~mpl_toolkits.mplot3d.axes3d.Axes3D` were previously stretched to fit a square bounding box. As this stretching was done after the projection from 3D to 2D, it resulted in distorted images if non-square -bounding boxes were used. +bounding boxes were used. As of 3.3, this no longer occurs. -As of this release, this no longer occurs. +Currently modes of setting the aspect (via +`~mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect`), in data space, are +not supported for Axes3D but maybe in the future. If you want 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 these +ratios use the `~mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect` +method which accepts th ratios at as a 3-tuple of X:Y:Z. The default +aspect ratio is 4:4:3. diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 1f0f649f0651..4f680eb70d3d 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -53,6 +53,7 @@ class Axes3D(Axes): def __init__( self, fig, rect=None, *args, azim=-60, elev=30, sharez=None, proj_type='persp', + box_aspect=None, **kwargs): """ Parameters @@ -91,11 +92,7 @@ def __init__( self.zz_viewLim = Bbox.unit() self.xy_dataLim = Bbox.unit() self.zz_dataLim = Bbox.unit() - if 'pb_aspect' in kwargs: - self.pb_aspect = np.asarray(kwargs['pb_aspect']) - else: - # chosen for similarity with the previous initial view - self.pb_aspect = np.array([4, 4, 3]) / 3.5 + # inhibit autoscale_view until the axes are defined # they can't be defined until Axes.__init__ has been called self.view_init(self.initial_elev, self.initial_azim) @@ -105,7 +102,9 @@ def __init__( self._shared_z_axes.join(self, sharez) self._adjustable = 'datalim' - super().__init__(fig, rect, frameon=True, *args, **kwargs) + super().__init__( + fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs + ) # Disable drawing of axes by base class super().set_axis_off() # Enable drawing of axes by Axes3D class @@ -309,6 +308,9 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): share : bool, default: False If ``True``, apply the settings to all shared Axes. + See Also + -------- + mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect """ if aspect != 'auto': raise NotImplementedError( @@ -347,8 +349,43 @@ def set_anchor(self, anchor, share=False): ax._anchor = anchor ax.stale = True - def set_pb_aspect(self, pb_aspect, zoom=1): - self.pb_aspect = pb_aspect * 1.8 * zoom / proj3d.mod(pb_aspect) + def set_box_aspect(self, aspect, zoom=1): + """ + Set the axes box aspect. + + The box aspect is the ratio of the axes height to the axes width in + physical units. This is not to be confused with the data + aspect, set via `~.Axes.set_aspect`. + + Parameters + ---------- + aspect : 3-tuple of floats on None + Changes the physical dimensions of the Axes, such that the ratio + of the size of the axis in physical units is x:y:z + + The input will be normalized to a unit vector. + + If None, it is approximately :: + + ax.set_box_aspect(aspect=(4, 4, 3), zoom=1) + + zoom : float + Control the "zoom" of the + + See Also + -------- + mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect + for a description of aspect handling. + """ + if aspect is None: + aspect = np.asarray((4, 4, 3), dtype=float) + else: + aspect = np.asarray(aspect, dtype=float) + # default scale tuned to match the mpl32 appearance. + aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect) + + self._box_aspect = aspect + self.stale = True def apply_aspect(self, position=None): if position is None: @@ -426,6 +463,7 @@ def get_axis_position(self): return xhigh, yhigh, zhigh def _on_units_changed(self, scalex=False, scaley=False, scalez=False): + """ Callback for processing changes to axis units. @@ -973,8 +1011,6 @@ def set_proj_type(self, proj_type): def get_proj(self): """Create the projection matrix from the current viewing position.""" - # chosen for similarity with the initial view before gh-8896 - # elev stores the elevation angle in the z plane # azim stores the azimuth angle in the x,y plane # @@ -990,10 +1026,11 @@ def get_proj(self): # transform to uniform world coordinates 0-1, 0-1, 0-1 worldM = proj3d.world_transformation(xmin, xmax, ymin, ymax, - zmin, zmax, pb_aspect=self.pb_aspect) + zmin, zmax, + pb_aspect=self._box_aspect) # look into the middle of the new coordinates - R = self.pb_aspect / 2 + R = self._box_aspect / 2 xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/equal_box_aspect.png b/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/equal_box_aspect.png new file mode 100644 index 0000000000000000000000000000000000000000..5f6dd6ea3099c3bc562a6ce2b763ab40f54021b1 GIT binary patch literal 55467 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

G?_A=Ut+NEy6 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 Uc0NC1qy;|iu160&IT(21KMN)}(f|Me literal 0 HcmV?d00001 diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index dc441568cc5c..57ba5cc37bea 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -961,3 +961,36 @@ def test_minor_ticks(): ax.set_yticklabels(["third"], minor=True) ax.set_zticks([0.50], minor=True) ax.set_zticklabels(["half"], minor=True) + + +@image_comparison(["equal_box_aspect.png"], style="mpl20") +def test_equal_box_aspect(): + from itertools import product, combinations + + fig = plt.figure() + ax = fig.add_subplot(111, projection="3d") + + # Make data + u = np.linspace(0, 2 * np.pi, 100) + v = np.linspace(0, np.pi, 100) + x = np.outer(np.cos(u), np.sin(v)) + y = np.outer(np.sin(u), np.sin(v)) + z = np.outer(np.ones(np.size(u)), np.cos(v)) + + # Plot the surface + ax.plot_surface(x, y, z) + + # draw cube + r = [-1, 1] + for s, e in combinations(np.array(list(product(r, r, r))), 2): + if np.sum(np.abs(s - e)) == r[1] - r[0]: + ax.plot3D(*zip(s, e), color="b") + + # Make axes limits + xyzlim = np.array([ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()]).T + XYZlim = [min(xyzlim[0]), max(xyzlim[1])] + ax.set_xlim3d(XYZlim) + ax.set_ylim3d(XYZlim) + ax.set_zlim3d(XYZlim) + ax.axis('off') + ax.set_box_aspect((1, 1, 1)) From c0ab395d9ada0244f08954597b9be3004b847653 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 30 May 2020 17:31:59 -0400 Subject: [PATCH 05/12] FIX: support bbox_inches='tight' for box_aspect Axes The way that bbox_inches='tight' is implemented we need to ensure that we do not try to adjust the aspect during the draw (because we have temporarily de-coupled the reported figure size from the transforms which results in the being distorted). Previously we did not have a way to fix the aspect ratio in screen space of the Axes (only the aspect ratio in dataspace) however in 3.3 we gained this ability for both Axes (#14917) and Axes3D (#8896 / #16472). Rather than add an aspect value to `set_aspect` to handle this case, in the tight_bbox code we monkey-patch the `apply_aspect` method with a no-op function and then restore it when we are done. Previously we would set the aspect to "auto" and restore it in the same places. closes #16463. --- .../test_figure/tightbbox_box_aspect.svg | 403 ++++++++++++++++++ lib/matplotlib/tests/test_figure.py | 13 + lib/matplotlib/tight_bbox.py | 14 +- 3 files changed, 425 insertions(+), 5 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_figure/tightbbox_box_aspect.svg diff --git a/lib/matplotlib/tests/baseline_images/test_figure/tightbbox_box_aspect.svg b/lib/matplotlib/tests/baseline_images/test_figure/tightbbox_box_aspect.svg new file mode 100644 index 000000000000..7ac69c1a1daa --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_figure/tightbbox_box_aspect.svg @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index b2f39e81b961..5d508db78e7b 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -566,3 +566,16 @@ def test_add_subplot_twotuple(): assert ax4.get_subplotspec().colspan == range(0, 2) with pytest.raises(IndexError): fig.add_subplot(3, 2, (6, 3)) + + +@image_comparison(['tightbbox_box_aspect.svg'], style='mpl20', + savefig_kwarg={'bbox_inches': 'tight', + 'facecolor': 'teal'}, + remove_text=True) +def test_tightbbox_box_aspect(): + fig = plt.figure() + gs = fig.add_gridspec(1, 2) + ax1 = fig.add_subplot(gs[0, 0]) + ax2 = fig.add_subplot(gs[0, 1], projection='3d') + ax1.set_box_aspect(.5) + ax2.set_box_aspect((2, 1, 1)) diff --git a/lib/matplotlib/tight_bbox.py b/lib/matplotlib/tight_bbox.py index d05ce7ec9135..43a9643d3383 100644 --- a/lib/matplotlib/tight_bbox.py +++ b/lib/matplotlib/tight_bbox.py @@ -15,6 +15,8 @@ def adjust_bbox(fig, bbox_inches, fixed_dpi=None): changes, the scale of the original figure is conserved. A function which restores the original values are returned. """ + def no_op_apply_aspect(position=None): + return origBbox = fig.bbox origBboxInches = fig.bbox_inches @@ -23,22 +25,24 @@ def adjust_bbox(fig, bbox_inches, fixed_dpi=None): fig.set_tight_layout(False) - asp_list = [] locator_list = [] for ax in fig.axes: pos = ax.get_position(original=False).frozen() locator_list.append(ax.get_axes_locator()) - asp_list.append(ax.get_aspect()) def _l(a, r, pos=pos): return pos ax.set_axes_locator(_l) - ax.set_aspect("auto") + # override the method that enforces the aspect ratio + # on the Axes + ax.apply_aspect = no_op_apply_aspect def restore_bbox(): - for ax, asp, loc in zip(fig.axes, asp_list, locator_list): - ax.set_aspect(asp) + for ax, loc in zip(fig.axes, locator_list): ax.set_axes_locator(loc) + # delete our no-op function which un-hides the + # method + del ax.apply_aspect fig.bbox = origBbox fig.bbox_inches = origBboxInches From b021ff4f301f2f55e8daf0e0af1f9c1245116f29 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 30 May 2020 18:18:07 -0400 Subject: [PATCH 06/12] MNT: suggestions from review Co-authored-by: Elliott Sales de Andrade --- .../next_whats_new/2019-03-25-mplot3d-projection.rst | 4 ++-- lib/mpl_toolkits/mplot3d/axes3d.py | 10 +++++----- lib/mpl_toolkits/mplot3d/axis3d.py | 1 + lib/mpl_toolkits/tests/test_mplot3d.py | 6 ++++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst b/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst index f8e72fbdfab7..9168fd6dce64 100644 --- a/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst +++ b/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst @@ -8,9 +8,9 @@ bounding boxes were used. As of 3.3, this no longer occurs. Currently modes of setting the aspect (via `~mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect`), in data space, are -not supported for Axes3D but maybe in the future. If you want to +not supported for Axes3D but may be in the future. If you want 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 these ratios use the `~mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect` -method which accepts th ratios at as a 3-tuple of X:Y:Z. The default +method which accepts th ratios as a 3-tuple of X:Y:Z. The default aspect ratio is 4:4:3. diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 4f680eb70d3d..188b12c72e93 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -349,7 +349,7 @@ def set_anchor(self, anchor, share=False): ax._anchor = anchor ax.stale = True - def set_box_aspect(self, aspect, zoom=1): + def set_box_aspect(self, aspect, *, zoom=1): """ Set the axes box aspect. @@ -357,6 +357,9 @@ def set_box_aspect(self, aspect, zoom=1): physical units. This is not to be confused with the data aspect, set via `~.Axes.set_aspect`. + The *zoom* is a Axes3D only parameter that controls the overall + size of the Axes3D in the figure. + Parameters ---------- aspect : 3-tuple of floats on None @@ -370,7 +373,7 @@ def set_box_aspect(self, aspect, zoom=1): ax.set_box_aspect(aspect=(4, 4, 3), zoom=1) zoom : float - Control the "zoom" of the + Control overall size of the Axes3D in the figure. See Also -------- @@ -391,8 +394,6 @@ def apply_aspect(self, position=None): if position is None: position = self.get_position(original=True) - aspect = self.get_aspect() - # in the superclass, we would go through and actually deal with axis # scales and box/datalim. Those are all irrelevant - all we need to do # is make sure our coordinate system is square. @@ -463,7 +464,6 @@ def get_axis_position(self): return xhigh, yhigh, zhigh def _on_units_changed(self, scalex=False, scaley=False, scalez=False): - """ Callback for processing changes to axis units. diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index 767f0a81842e..ddb2e863b306 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -3,6 +3,7 @@ # Parts rewritten by Reinier Heeres import numpy as np + import matplotlib.transforms as mtransforms from matplotlib import ( artist, lines as mlines, axis as maxis, patches as mpatches, rcParams) diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index 57ba5cc37bea..fcc225053632 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -975,7 +975,7 @@ def test_equal_box_aspect(): v = np.linspace(0, np.pi, 100) x = np.outer(np.cos(u), np.sin(v)) y = np.outer(np.sin(u), np.sin(v)) - z = np.outer(np.ones(np.size(u)), np.cos(v)) + z = np.outer(np.ones_like(u), np.cos(v)) # Plot the surface ax.plot_surface(x, y, z) @@ -987,7 +987,9 @@ def test_equal_box_aspect(): ax.plot3D(*zip(s, e), color="b") # Make axes limits - xyzlim = np.array([ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()]).T + xyzlim = np.column_stack( + [ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()] + ) XYZlim = [min(xyzlim[0]), max(xyzlim[1])] ax.set_xlim3d(XYZlim) ax.set_ylim3d(XYZlim) From 8e6e83caed2a83b7fc7c00ba210b0bab1a89ec9d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 3 Jun 2020 18:13:12 -0400 Subject: [PATCH 07/12] DOC: correct typos and markup Co-authored-by: Elliott Sales de Andrade --- doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst | 6 +++--- lib/matplotlib/tight_bbox.py | 2 +- lib/mpl_toolkits/mplot3d/axes3d.py | 6 +++--- lib/mpl_toolkits/mplot3d/axis3d.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst b/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst index 9168fd6dce64..fdc16761c713 100644 --- a/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst +++ b/doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst @@ -6,11 +6,11 @@ stretched to fit a square bounding box. As this stretching was done after the projection from 3D to 2D, it resulted in distorted images if non-square bounding boxes were used. As of 3.3, this no longer occurs. -Currently modes of setting the aspect (via -`~mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect`), in data space, are +Currently, modes of setting the aspect (via +`~mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect`) in data space are not supported for Axes3D but may be in the future. If you want 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 these ratios use the `~mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect` -method which accepts th ratios as a 3-tuple of X:Y:Z. The default +method which accepts the ratios as a 3-tuple of X:Y:Z. The default aspect ratio is 4:4:3. diff --git a/lib/matplotlib/tight_bbox.py b/lib/matplotlib/tight_bbox.py index 43a9643d3383..8a88a1c23f5a 100644 --- a/lib/matplotlib/tight_bbox.py +++ b/lib/matplotlib/tight_bbox.py @@ -41,7 +41,7 @@ def restore_bbox(): for ax, loc in zip(fig.axes, locator_list): ax.set_axes_locator(loc) # delete our no-op function which un-hides the - # method + # original method del ax.apply_aspect fig.bbox = origBbox diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 188b12c72e93..086d5b52325f 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -282,14 +282,14 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): ========= ================================================== adjustable : None or {'box', 'datalim'}, optional - If not ``None``, this defines which parameter will be adjusted to + If not *None*, this defines which parameter will be adjusted to meet the required aspect. See `.set_adjustable` for further details. Currently ignored by Axes3D anchor : None or str or 2-tuple of float, optional - If not ``None``, this defines where the Axes will be drawn if there + If not *None*, this defines where the Axes will be drawn if there is extra space due to aspect constraints. The most common way to to specify the anchor are abbreviations of cardinal directions: @@ -357,7 +357,7 @@ def set_box_aspect(self, aspect, *, zoom=1): physical units. This is not to be confused with the data aspect, set via `~.Axes.set_aspect`. - The *zoom* is a Axes3D only parameter that controls the overall + The *zoom* is an Axes3D-only parameter that controls the overall size of the Axes3D in the figure. Parameters diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index ddb2e863b306..b232405e25d6 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -427,7 +427,7 @@ def get_tightbbox(self, renderer, *, for_layout_only=False): try: loc_t = self.get_transform().transform(tick.get_loc()) except AssertionError: - # transforms.transform doesn't allow masked values but + # Transform.transform doesn't allow masked values but # some scales might make them, so we need this try/except. pass else: From 7cde8deb7bc3d25cdd7fc7f64f77f14de25064dd Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 3 Jun 2020 22:11:48 -0400 Subject: [PATCH 08/12] DOC: clearify and simplify docstring --- lib/mpl_toolkits/mplot3d/axes3d.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 086d5b52325f..80df42790d00 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -362,15 +362,11 @@ def set_box_aspect(self, aspect, *, zoom=1): Parameters ---------- - aspect : 3-tuple of floats on None - Changes the physical dimensions of the Axes, such that the ratio - of the size of the axis in physical units is x:y:z + aspect : 3-tuple of floats or None + Changes the physical dimensions of the Axes3D, such that the ratio + of the axis lengths in physical units is x:y:z. - The input will be normalized to a unit vector. - - If None, it is approximately :: - - ax.set_box_aspect(aspect=(4, 4, 3), zoom=1) + If None, defaults to 4:4:3 zoom : float Control overall size of the Axes3D in the figure. From 4c0b944cfd22a046d4a590c407fc960f9ed106b9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 4 Jun 2020 21:49:42 -0400 Subject: [PATCH 09/12] DOC: clarify docs on Axes3D.set_aspect and Axes3D.set_box_aspect --- lib/mpl_toolkits/mplot3d/axes3d.py | 38 +++++++++++++++++------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 80df42790d00..05b5ae334593 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -268,7 +268,14 @@ def tunit_edges(self, vals=None, M=None): def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): """ - Set the aspect of the axis scaling. + 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 ---------- @@ -281,13 +288,13 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): 'auto' automatic; fill the position rectangle with data. ========= ================================================== - adjustable : None or {'box', 'datalim'}, optional + adjustable : None + Currently ignored by Axes3D + If not *None*, this defines which parameter will be adjusted to meet the required aspect. See `.set_adjustable` for further details. - Currently ignored by Axes3D - anchor : None or str or 2-tuple of float, optional If not *None*, this defines where the Axes will be drawn if there is extra space due to aspect constraints. The most common way to @@ -314,7 +321,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): """ if aspect != 'auto': raise NotImplementedError( - "Axes3D currently only support the aspect arguments " + "Axes3D currently only supports the aspect argument " f"'auto'. You passed in {aspect!r}." ) @@ -353,28 +360,27 @@ def set_box_aspect(self, aspect, *, zoom=1): """ Set the axes box aspect. - The box aspect is the ratio of the axes height to the axes width in - physical units. This is not to be confused with the data - aspect, set via `~.Axes.set_aspect`. + The box aspect is the ratio of height to width in display + units for each face of the box when viewed perpendicular to + that face. This is not to be confused with the data aspect + (which for Axes3D is always 'auto'). The default ratios are + 4:4:3 (x:y:z). + + To simulate having equal aspect in data space, set the box + aspect to match your data range in each dimension. - The *zoom* is an Axes3D-only parameter that controls the overall - size of the Axes3D in the figure. + *zoom* controls the overall size of the Axes3D in the figure. Parameters ---------- aspect : 3-tuple of floats or None Changes the physical dimensions of the Axes3D, such that the ratio - of the axis lengths in physical units is x:y:z. + of the axis lengths in display units is x:y:z. If None, defaults to 4:4:3 zoom : float Control overall size of the Axes3D in the figure. - - See Also - -------- - mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect - for a description of aspect handling. """ if aspect is None: aspect = np.asarray((4, 4, 3), dtype=float) From eea3d1a31eb3f6c0180952740caec3e5cb539d49 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 4 Jun 2020 21:50:10 -0400 Subject: [PATCH 10/12] MNT: do minimal input validation in Axes3D.set_box_aspect --- lib/mpl_toolkits/mplot3d/axes3d.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 05b5ae334593..325faf4d9bca 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -385,7 +385,13 @@ def set_box_aspect(self, aspect, *, zoom=1): if aspect is None: aspect = np.asarray((4, 4, 3), dtype=float) else: + orig_aspect = aspect aspect = np.asarray(aspect, dtype=float) + if aspect.shape != (3,): + raise ValueError( + "You must pass a 3-tuple that can be cast to floats. " + f"You passed {orig_aspect!r}" + ) # default scale tuned to match the mpl32 appearance. aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect) From 910bfe5ec9ef65de54ddfa7cdd5f40d395f33c81 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 5 Jun 2020 15:00:58 -0400 Subject: [PATCH 11/12] FIX: correct text of error message Co-authored-by: Elliott Sales de Andrade --- lib/mpl_toolkits/mplot3d/axes3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 325faf4d9bca..584e12461d6d 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -343,7 +343,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): def set_anchor(self, anchor, share=False): # docstring inherited if not (anchor in mtransforms.Bbox.coefs or len(anchor) == 2): - raise ValueError('argument must be among %s' % + raise ValueError('anchor must be among %s' % ', '.join(mtransforms.Bbox.coefs)) if share: axes = {*self._shared_x_axes.get_siblings(self), From 8ee34a98a12418fa111836bbd641026b0cebac8a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 5 Jun 2020 15:03:33 -0400 Subject: [PATCH 12/12] MNT: be more careful about handling pre-monkey patched Axes --- lib/matplotlib/tight_bbox.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tight_bbox.py b/lib/matplotlib/tight_bbox.py index 8a88a1c23f5a..06f944c2b5bd 100644 --- a/lib/matplotlib/tight_bbox.py +++ b/lib/matplotlib/tight_bbox.py @@ -24,8 +24,9 @@ def no_op_apply_aspect(position=None): _boxout = fig.transFigure._boxout fig.set_tight_layout(False) - + old_aspect = [] locator_list = [] + sentinel = object() for ax in fig.axes: pos = ax.get_position(original=False).frozen() locator_list.append(ax.get_axes_locator()) @@ -35,14 +36,21 @@ def _l(a, r, pos=pos): ax.set_axes_locator(_l) # override the method that enforces the aspect ratio # on the Axes + if 'apply_aspect' in ax.__dict__: + old_aspect.append(ax.apply_aspect) + else: + old_aspect.append(sentinel) ax.apply_aspect = no_op_apply_aspect def restore_bbox(): - for ax, loc in zip(fig.axes, locator_list): + for ax, loc, aspect in zip(fig.axes, locator_list, old_aspect): ax.set_axes_locator(loc) - # delete our no-op function which un-hides the - # original method - del ax.apply_aspect + if aspect is sentinel: + # delete our no-op function which un-hides the + # original method + del ax.apply_aspect + else: + ax.apply_aspect = aspect fig.bbox = origBbox fig.bbox_inches = origBboxInches