From 346b516b75bb701bd7c02ac168284a624eb678b6 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 23 Oct 2021 14:05:50 +0200 Subject: [PATCH 1/2] Factor out common limits handling for x/y/z axes. The change in set_top_view is necessary because super().set_xlim no longer goes to 2D semantics (it's on the Axis.set_view_limits that one would need to do a supercall), but in any case the old behavior was actually more buggy because it would e.g. confuse data-limits sharex and 2D projection sharex; e.g. `subplots(1, 2, sharex=True, subplot_kw={"projection": "3d"})` was previously wrong. As a side effect, also fixes `invert_yaxis`/`invert_zaxis` on 3d axises (because they just go to the base `Axis.set_inverted`). --- lib/matplotlib/axes/_base.py | 154 ++------------------------ lib/matplotlib/axis.py | 110 +++++++++++++++--- lib/matplotlib/projections/polar.py | 18 +-- lib/matplotlib/tests/test_axes.py | 4 +- lib/mpl_toolkits/mplot3d/axes3d.py | 166 +++------------------------- 5 files changed, 121 insertions(+), 331 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index a16f50cba600..18f09d0ae8ae 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3654,9 +3654,8 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, False turns off, None leaves unchanged. xmin, xmax : float, optional - They are equivalent to left and right respectively, - and it is an error to pass both *xmin* and *left* or - *xmax* and *right*. + They are equivalent to left and right respectively, and it is an + error to pass both *xmin* and *left* or *xmax* and *right*. Returns ------- @@ -3691,76 +3690,10 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, present is on the right. >>> set_xlim(5000, 0) - """ - if right is None and np.iterable(left): - left, right = left - if xmin is not None: - if left is not None: - raise TypeError('Cannot pass both `xmin` and `left`') - left = xmin - if xmax is not None: - if right is not None: - raise TypeError('Cannot pass both `xmax` and `right`') - right = xmax - - self._process_unit_info([("x", (left, right))], convert=False) - left = self._validate_converted_limits(left, self.convert_xunits) - right = self._validate_converted_limits(right, self.convert_xunits) - - if left is None or right is None: - # Axes init calls set_xlim(0, 1) before get_xlim() can be called, - # so only grab the limits if we really need them. - old_left, old_right = self.get_xlim() - if left is None: - left = old_left - if right is None: - right = old_right - - if self.get_xscale() == 'log' and (left <= 0 or right <= 0): - # Axes init calls set_xlim(0, 1) before get_xlim() can be called, - # so only grab the limits if we really need them. - old_left, old_right = self.get_xlim() - if left <= 0: - _api.warn_external( - 'Attempted to set non-positive left xlim on a ' - 'log-scaled axis.\n' - 'Invalid limit will be ignored.') - left = old_left - if right <= 0: - _api.warn_external( - 'Attempted to set non-positive right xlim on a ' - 'log-scaled axis.\n' - 'Invalid limit will be ignored.') - right = old_right - if left == right: - _api.warn_external( - f"Attempting to set identical left == right == {left} results " - f"in singular transformations; automatically expanding.") - reverse = left > right - left, right = self.xaxis.get_major_locator().nonsingular(left, right) - left, right = self.xaxis.limit_range_for_scale(left, right) - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - left, right = sorted([left, right], reverse=bool(reverse)) - - self._viewLim.intervalx = (left, right) - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_axes["x"].get_siblings(self): - ax._stale_viewlims["x"] = False - if auto is not None: - self._autoscaleXon = bool(auto) - - if emit: - self.callbacks.process('xlim_changed', self) - # Call all of the other x-axes that are shared with this one - for other in self._shared_axes["x"].get_siblings(self): - if other is not self: - other.set_xlim(self.viewLim.intervalx, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return left, right + return self.xaxis._set_lim( + left, right, xmin, xmax, emit=emit, auto=auto, + names=("left", "right")) get_xscale = _axis_method_wrapper("xaxis", "get_scale") @@ -3985,9 +3918,8 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, *False* turns off, *None* leaves unchanged. ymin, ymax : float, optional - They are equivalent to bottom and top respectively, - and it is an error to pass both *ymin* and *bottom* or - *ymax* and *top*. + They are equivalent to bottom and top respectively, and it is an + error to pass both *ymin* and *bottom* or *ymax* and *top*. Returns ------- @@ -4023,75 +3955,9 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, >>> set_ylim(5000, 0) """ - if top is None and np.iterable(bottom): - bottom, top = bottom - if ymin is not None: - if bottom is not None: - raise TypeError('Cannot pass both `ymin` and `bottom`') - bottom = ymin - if ymax is not None: - if top is not None: - raise TypeError('Cannot pass both `ymax` and `top`') - top = ymax - - self._process_unit_info([("y", (bottom, top))], convert=False) - bottom = self._validate_converted_limits(bottom, self.convert_yunits) - top = self._validate_converted_limits(top, self.convert_yunits) - - if bottom is None or top is None: - # Axes init calls set_ylim(0, 1) before get_ylim() can be called, - # so only grab the limits if we really need them. - old_bottom, old_top = self.get_ylim() - if bottom is None: - bottom = old_bottom - if top is None: - top = old_top - - if self.get_yscale() == 'log' and (bottom <= 0 or top <= 0): - # Axes init calls set_xlim(0, 1) before get_xlim() can be called, - # so only grab the limits if we really need them. - old_bottom, old_top = self.get_ylim() - if bottom <= 0: - _api.warn_external( - 'Attempted to set non-positive bottom ylim on a ' - 'log-scaled axis.\n' - 'Invalid limit will be ignored.') - bottom = old_bottom - if top <= 0: - _api.warn_external( - 'Attempted to set non-positive top ylim on a ' - 'log-scaled axis.\n' - 'Invalid limit will be ignored.') - top = old_top - if bottom == top: - _api.warn_external( - f"Attempting to set identical bottom == top == {bottom} " - f"results in singular transformations; automatically " - f"expanding.") - reverse = bottom > top - bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) - bottom, top = self.yaxis.limit_range_for_scale(bottom, top) - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - bottom, top = sorted([bottom, top], reverse=bool(reverse)) - - self._viewLim.intervaly = (bottom, top) - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_axes["y"].get_siblings(self): - ax._stale_viewlims["y"] = False - if auto is not None: - self._autoscaleYon = bool(auto) - - if emit: - self.callbacks.process('ylim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_axes["y"].get_siblings(self): - if other is not self: - other.set_ylim(self.viewLim.intervaly, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return bottom, top + return self.yaxis._set_lim( + bottom, top, ymin, ymax, emit=emit, auto=auto, + names=("bottom", "top")) get_yscale = _axis_method_wrapper("yaxis", "get_scale") diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index c257358ed1e3..a0219a78421d 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1004,10 +1004,9 @@ def set_inverted(self, inverted): the top for the y-axis; the "inverse" direction is increasing to the left for the x-axis and to the bottom for the y-axis. """ - # Currently, must be implemented in subclasses using set_xlim/set_ylim - # rather than generically using set_view_interval, so that shared - # axes get updated as well. - raise NotImplementedError('Derived must override') + a, b = self.get_view_interval() + # cast to bool to avoid bad interaction between python 3.8 and np.bool_ + self._set_lim(*sorted((a, b), reverse=bool(inverted)), auto=None) def set_default_intervals(self): """ @@ -1023,6 +1022,97 @@ def set_default_intervals(self): # attribute, and the derived code below will check for that # and use it if it's available (else just use 0..1) + def _set_lim(self, v0, v1, alt0=None, alt1=None, *, + emit=True, auto, names=("", "")): + """ + Set view limits. + + This method is a helper for the Axes ``set_xlim``, ``set_ylim``, and + ``set_zlim`` methods. This docstring uses names corresponding to + ``set_xlim`` for simplicity. + + *names* is the pair of the names of the first two parameters of the + Axes method (e.g., "left" and "right"). They are only used to generate + error messages; and can be empty if the limits are known to be valid. + + Other parameters are directly forwarded from the Axes limits setter: + *v0*, *v1*, *alt0*, and *alt1* map to *left*, *right*, *xmin*, and + *xmax* respectively; *emit* and *auto* are used as is. + """ + v0name, v1name = names # The value names. + name, = [name for name, axis in self.axes._get_axis_map().items() + if axis is self] # The axis name. + + if v1 is None and np.iterable(v0): + v0, v1 = v0 + if alt0 is not None: + if v0 is not None: + raise TypeError( + f"Cannot pass both {v0name!r} and '{name}lim'") + v0 = alt0 + if alt1 is not None: + if v1 is not None: + raise TypeError( + f"Cannot pass both {v1name!r} and '{name}lim'") + v1 = alt1 + + self.axes._process_unit_info([(name, (v0, v1))], convert=False) + v0 = self.axes._validate_converted_limits(v0, self.convert_units) + v1 = self.axes._validate_converted_limits(v1, self.convert_units) + + if v0 is None or v1 is None: + # Axes init calls set_xlim(0, 1) before get_xlim() can be called, + # so only grab the limits if we really need them. + old0, old1 = self.get_view_interval() + if v0 is None: + v0 = old0 + if v1 is None: + v1 = old1 + + if self.get_scale() == 'log' and (v0 <= 0 or v1 <= 0): + # Axes init calls set_xlim(0, 1) before get_xlim() can be called, + # so only grab the limits if we really need them. + old0, old1 = self.get_view_interval() + if v0 <= 0: + _api.warn_external( + f"Attempt to set non-positive {v0name} {name}lim on a " + f"log-scaled axis will be ignored.") + v0 = old0 + if v1 <= 0: + _api.warn_external( + f"Attempt to set non-positive {v1name} {name}lim on a " + f"log-scaled axis will be ignored.") + v1 = old1 + if v0 == v1: + _api.warn_external( + f"Attempting to set identical {v0name} == {v1name} == {v0} " + f"makes transformation singular; automatically expanding.") + reverse = v0 > v1 + v0, v1 = self.get_major_locator().nonsingular(v0, v1) + v0, v1 = self.limit_range_for_scale(v0, v1) + # cast to bool to avoid bad interaction between python 3.8 and np.bool_ + v0, v1 = sorted([v0, v1], reverse=bool(reverse)) + + self.set_view_interval(v0, v1, ignore=True) + # Mark viewlims as no longer stale without triggering an autoscale. + for ax in self.axes._shared_axes[name].get_siblings(self.axes): + ax._stale_viewlims[name] = False + if auto is not None: + setattr(self.axes, f"_autoscale{name.upper()}on", bool(auto)) + + if emit: + self.axes.callbacks.process(f"{name}lim_changed", self.axes) + # Call all of the other axes that are shared with this one + for other in self.axes._shared_axes[name].get_siblings(self.axes): + if other is not self.axes: + other._get_axis_map()[name]._set_lim( + v0, v1, emit=False, auto=auto, names=names) + if other.figure != self.figure: + other.figure.canvas.draw_idle() + + self.stale = True + return v0, v1 + def _set_artist_props(self, a): if a is None: return @@ -2242,12 +2332,6 @@ def get_ticks_position(self): def get_minpos(self): return self.axes.dataLim.minposx - def set_inverted(self, inverted): - # docstring inherited - a, b = self.get_view_interval() - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - self.axes.set_xlim(sorted((a, b), reverse=bool(inverted)), auto=None) - def set_default_intervals(self): # docstring inherited # only change view if dataLim has not changed and user has @@ -2500,12 +2584,6 @@ def get_ticks_position(self): def get_minpos(self): return self.axes.dataLim.minposy - def set_inverted(self, inverted): - # docstring inherited - a, b = self.get_view_interval() - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - self.axes.set_ylim(sorted((a, b), reverse=bool(inverted)), auto=None) - def set_default_intervals(self): # docstring inherited # only change view if dataLim has not changed and user has diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 6a0ec4f5a6a6..924dd22fbe61 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -1194,7 +1194,7 @@ def set_rlim(self, bottom=None, top=None, emit=True, auto=False, **kwargs): def set_ylim(self, bottom=None, top=None, emit=True, auto=False, *, ymin=None, ymax=None): """ - Set the data limits for the radial axis. + Set the view limits for the radial axis. Parameters ---------- @@ -1227,21 +1227,7 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, bottom, top : (float, float) The new y-axis limits in data coordinates. """ - if ymin is not None: - if bottom is not None: - raise ValueError('Cannot supply both positional "bottom" ' - 'argument and kwarg "ymin"') - else: - bottom = ymin - if ymax is not None: - if top is not None: - raise ValueError('Cannot supply both positional "top" ' - 'argument and kwarg "ymax"') - else: - top = ymax - if top is None and np.iterable(bottom): - bottom, top = bottom[0], bottom[1] - return super().set_ylim(bottom=bottom, top=top, emit=emit, auto=auto) + return super().set_ylim(bottom, top, emit, auto, ymin=ymin, ymax=ymax) def get_rlabel_position(self): """ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 2731c1b06659..0f3c7d4825d4 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2591,10 +2591,10 @@ def test_log_scales_no_data(): def test_log_scales_invalid(): fig, ax = plt.subplots() ax.set_xscale('log') - with pytest.warns(UserWarning, match='Attempted to set non-positive'): + with pytest.warns(UserWarning, match='Attempt to set non-positive'): ax.set_xlim(-1, 10) ax.set_yscale('log') - with pytest.warns(UserWarning, match='Attempted to set non-positive'): + with pytest.warns(UserWarning, match='Attempt to set non-positive'): ax.set_ylim(-1, 10) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 5e30af283b1c..639ae3b49689 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -210,10 +210,10 @@ def set_top_view(self): xdw = 0.9 / self._dist ydwl = 0.95 / self._dist ydw = 0.9 / self._dist - # This is purposely using the 2D Axes's set_xlim and set_ylim, - # because we are trying to place our viewing pane. - super().set_xlim(-xdwl, xdw, auto=None) - super().set_ylim(-ydwl, ydw, auto=None) + # Set the viewing pane. + self.viewLim.intervalx = (-xdwl, xdw) + self.viewLim.intervaly = (-ydwl, ydw) + self.stale = True def _init_axis(self): """Init 3D axes; overrides creation of regular X/Y axes.""" @@ -666,55 +666,9 @@ def set_xlim3d(self, left=None, right=None, emit=True, auto=False, See `.Axes.set_xlim` for full documentation. """ - if right is None and np.iterable(left): - left, right = left - if xmin is not None: - if left is not None: - raise TypeError('Cannot pass both `xmin` and `left`') - left = xmin - if xmax is not None: - if right is not None: - raise TypeError('Cannot pass both `xmax` and `right`') - right = xmax - - self._process_unit_info([("x", (left, right))], convert=False) - left = self._validate_converted_limits(left, self.convert_xunits) - right = self._validate_converted_limits(right, self.convert_xunits) - - old_left, old_right = self.get_xlim() - if left is None: - left = old_left - if right is None: - right = old_right - - if left == right: - _api.warn_external( - f"Attempting to set identical left == right == {left} results " - f"in singular transformations; automatically expanding.") - reverse = left > right - left, right = self.xaxis.get_major_locator().nonsingular(left, right) - left, right = self.xaxis.limit_range_for_scale(left, right) - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - left, right = sorted([left, right], reverse=bool(reverse)) - self.xy_viewLim.intervalx = (left, right) - - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_axes["x"].get_siblings(self): - ax._stale_viewlims["x"] = False - if auto is not None: - self._autoscaleXon = bool(auto) - - if emit: - self.callbacks.process('xlim_changed', self) - # Call all of the other x-axes that are shared with this one - for other in self._shared_axes["x"].get_siblings(self): - if other is not self: - other.set_xlim(self.xy_viewLim.intervalx, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return left, right + return self.xaxis._set_lim( + left, right, xmin, xmax, emit=emit, auto=auto, + names=("left", "right")) def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, *, ymin=None, ymax=None): @@ -723,56 +677,9 @@ def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, See `.Axes.set_ylim` for full documentation. """ - if top is None and np.iterable(bottom): - bottom, top = bottom - if ymin is not None: - if bottom is not None: - raise TypeError('Cannot pass both `ymin` and `bottom`') - bottom = ymin - if ymax is not None: - if top is not None: - raise TypeError('Cannot pass both `ymax` and `top`') - top = ymax - - self._process_unit_info([("y", (bottom, top))], convert=False) - bottom = self._validate_converted_limits(bottom, self.convert_yunits) - top = self._validate_converted_limits(top, self.convert_yunits) - - old_bottom, old_top = self.get_ylim() - if bottom is None: - bottom = old_bottom - if top is None: - top = old_top - - if bottom == top: - _api.warn_external( - f"Attempting to set identical bottom == top == {bottom} " - f"results in singular transformations; automatically " - f"expanding.") - swapped = bottom > top - bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) - bottom, top = self.yaxis.limit_range_for_scale(bottom, top) - if swapped: - bottom, top = top, bottom - self.xy_viewLim.intervaly = (bottom, top) - - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_axes["y"].get_siblings(self): - ax._stale_viewlims["y"] = False - if auto is not None: - self._autoscaleYon = bool(auto) - - if emit: - self.callbacks.process('ylim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_axes["y"].get_siblings(self): - if other is not self: - other.set_ylim(self.xy_viewLim.intervaly, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return bottom, top + return self.yaxis._set_lim( + bottom, top, ymin, ymax, emit=emit, auto=auto, + names=("bottom", "top")) def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, *, zmin=None, zmax=None): @@ -781,56 +688,9 @@ def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, See `.Axes.set_ylim` for full documentation """ - if top is None and np.iterable(bottom): - bottom, top = bottom - if zmin is not None: - if bottom is not None: - raise TypeError('Cannot pass both `zmin` and `bottom`') - bottom = zmin - if zmax is not None: - if top is not None: - raise TypeError('Cannot pass both `zmax` and `top`') - top = zmax - - self._process_unit_info([("z", (bottom, top))], convert=False) - bottom = self._validate_converted_limits(bottom, self.convert_zunits) - top = self._validate_converted_limits(top, self.convert_zunits) - - old_bottom, old_top = self.get_zlim() - if bottom is None: - bottom = old_bottom - if top is None: - top = old_top - - if bottom == top: - _api.warn_external( - f"Attempting to set identical bottom == top == {bottom} " - f"results in singular transformations; automatically " - f"expanding.") - swapped = bottom > top - bottom, top = self.zaxis.get_major_locator().nonsingular(bottom, top) - bottom, top = self.zaxis.limit_range_for_scale(bottom, top) - if swapped: - bottom, top = top, bottom - self.zz_viewLim.intervalx = (bottom, top) - - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_axes["z"].get_siblings(self): - ax._stale_viewlims["z"] = False - if auto is not None: - self._autoscaleZon = bool(auto) - - if emit: - self.callbacks.process('zlim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_axes["z"].get_siblings(self): - if other is not self: - other.set_zlim(self.zz_viewLim.intervalx, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return bottom, top + return self.zaxis._set_lim( + bottom, top, zmin, zmax, emit=emit, auto=auto, + names=("bottom", "top")) def get_xlim3d(self): return tuple(self.xy_viewLim.intervalx) From abd1f1a13ba1146543f41282fab45e79b2f7bcdd Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 5 Feb 2022 21:35:03 +0100 Subject: [PATCH 2/2] Redupe parameters dealiasing for clarity. The "canonical" property names for Axes3D were switched from xlim3d/ylim3d to xlim/ylim so that Axes3D can directly inherit the setters from 2D Axes. --- lib/matplotlib/axes/_base.py | 28 +++++++++++--- lib/matplotlib/axis.py | 54 ++++++++++----------------- lib/matplotlib/tests/test_dates.py | 4 +- lib/matplotlib/tests/test_image.py | 2 +- lib/mpl_toolkits/mplot3d/axes3d.py | 59 +++++++++++++----------------- 5 files changed, 69 insertions(+), 78 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 18f09d0ae8ae..64b34f3dc3cd 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3691,9 +3691,17 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, >>> set_xlim(5000, 0) """ - return self.xaxis._set_lim( - left, right, xmin, xmax, emit=emit, auto=auto, - names=("left", "right")) + if right is None and np.iterable(left): + left, right = left + if xmin is not None: + if left is not None: + raise TypeError("Cannot pass both 'left' and 'xmin'") + left = xmin + if xmax is not None: + if right is not None: + raise TypeError("Cannot pass both 'right' and 'xmax'") + right = xmax + return self.xaxis._set_lim(left, right, emit=emit, auto=auto) get_xscale = _axis_method_wrapper("xaxis", "get_scale") @@ -3955,9 +3963,17 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, >>> set_ylim(5000, 0) """ - return self.yaxis._set_lim( - bottom, top, ymin, ymax, emit=emit, auto=auto, - names=("bottom", "top")) + if top is None and np.iterable(bottom): + bottom, top = bottom + if ymin is not None: + if bottom is not None: + raise TypeError("Cannot pass both 'bottom' and 'ymin'") + bottom = ymin + if ymax is not None: + if top is not None: + raise TypeError("Cannot pass both 'top' and 'ymax'") + top = ymax + return self.yaxis._set_lim(bottom, top, emit=emit, auto=auto) get_yscale = _axis_method_wrapper("yaxis", "get_scale") diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index a0219a78421d..27a2ae98096d 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1022,40 +1022,27 @@ def set_default_intervals(self): # attribute, and the derived code below will check for that # and use it if it's available (else just use 0..1) - def _set_lim(self, v0, v1, alt0=None, alt1=None, *, - emit=True, auto, names=("", "")): + def _set_lim(self, v0, v1, *, emit=True, auto): """ Set view limits. This method is a helper for the Axes ``set_xlim``, ``set_ylim``, and - ``set_zlim`` methods. This docstring uses names corresponding to - ``set_xlim`` for simplicity. + ``set_zlim`` methods. - *names* is the pair of the names of the first two parameters of the - Axes method (e.g., "left" and "right"). They are only used to generate - error messages; and can be empty if the limits are known to be valid. - - Other parameters are directly forwarded from the Axes limits setter: - *v0*, *v1*, *alt0*, and *alt1* map to *left*, *right*, *xmin*, and - *xmax* respectively; *emit* and *auto* are used as is. + Parameters + ---------- + v0, v1 : float + The view limits. (Passing *v0* as a (low, high) pair is not + supported; normalization must occur in the Axes setters.) + emit : bool, default: True + Whether to notify observers of limit change. + auto : bool or None, default: False + Whether to turn on autoscaling of the x-axis. True turns on, False + turns off, None leaves unchanged. """ - v0name, v1name = names # The value names. name, = [name for name, axis in self.axes._get_axis_map().items() if axis is self] # The axis name. - if v1 is None and np.iterable(v0): - v0, v1 = v0 - if alt0 is not None: - if v0 is not None: - raise TypeError( - f"Cannot pass both {v0name!r} and '{name}lim'") - v0 = alt0 - if alt1 is not None: - if v1 is not None: - raise TypeError( - f"Cannot pass both {v1name!r} and '{name}lim'") - v1 = alt1 - self.axes._process_unit_info([(name, (v0, v1))], convert=False) v0 = self.axes._validate_converted_limits(v0, self.convert_units) v1 = self.axes._validate_converted_limits(v1, self.convert_units) @@ -1074,23 +1061,20 @@ def _set_lim(self, v0, v1, alt0=None, alt1=None, *, # so only grab the limits if we really need them. old0, old1 = self.get_view_interval() if v0 <= 0: - _api.warn_external( - f"Attempt to set non-positive {v0name} {name}lim on a " - f"log-scaled axis will be ignored.") + _api.warn_external(f"Attempt to set non-positive {name}lim on " + f"a log-scaled axis will be ignored.") v0 = old0 if v1 <= 0: - _api.warn_external( - f"Attempt to set non-positive {v1name} {name}lim on a " - f"log-scaled axis will be ignored.") + _api.warn_external(f"Attempt to set non-positive {name}lim on " + f"a log-scaled axis will be ignored.") v1 = old1 if v0 == v1: _api.warn_external( - f"Attempting to set identical {v0name} == {v1name} == {v0} " + f"Attempting to set identical low and high {name}lims " f"makes transformation singular; automatically expanding.") - reverse = v0 > v1 + reverse = bool(v0 > v1) # explicit cast needed for python3.8+np.bool_. v0, v1 = self.get_major_locator().nonsingular(v0, v1) v0, v1 = self.limit_range_for_scale(v0, v1) - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ v0, v1 = sorted([v0, v1], reverse=bool(reverse)) self.set_view_interval(v0, v1, ignore=True) @@ -1106,7 +1090,7 @@ def _set_lim(self, v0, v1, alt0=None, alt1=None, *, for other in self.axes._shared_axes[name].get_siblings(self.axes): if other is not self.axes: other._get_axis_map()[name]._set_lim( - v0, v1, emit=False, auto=auto, names=names) + v0, v1, emit=False, auto=auto) if other.figure != self.figure: other.figure.canvas.draw_idle() diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 6db30bdaef33..eafced95f00b 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -182,8 +182,8 @@ def test_too_many_date_ticks(caplog): with pytest.warns(UserWarning) as rec: ax.set_xlim((t0, tf), auto=True) assert len(rec) == 1 - assert \ - 'Attempting to set identical left == right' in str(rec[0].message) + assert ('Attempting to set identical low and high xlims' + in str(rec[0].message)) ax.plot([], []) ax.xaxis.set_major_locator(mdates.DayLocator()) v = ax.xaxis.get_major_locator()() diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 396b7838cd36..e8e90b768085 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -977,7 +977,7 @@ def test_imshow_bignumbers_real(): def test_empty_imshow(make_norm): fig, ax = plt.subplots() with pytest.warns(UserWarning, - match="Attempting to set identical left == right"): + match="Attempting to set identical low and high xlims"): im = ax.imshow([[]], norm=make_norm()) im.set_extent([-5, 5, -5, 5]) fig.canvas.draw() diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 639ae3b49689..f96b16f7a167 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -42,7 +42,7 @@ @docstring.interpd @cbook._define_aliases({ - "xlim3d": ["xlim"], "ylim3d": ["ylim"], "zlim3d": ["zlim"]}) + "xlim": ["xlim3d"], "ylim": ["ylim3d"], "zlim": ["zlim3d"]}) class Axes3D(Axes): """ 3D Axes object. @@ -659,48 +659,39 @@ def get_w_lims(self): minz, maxz = self.get_zlim3d() return minx, maxx, miny, maxy, minz, maxz - def set_xlim3d(self, left=None, right=None, emit=True, auto=False, - *, xmin=None, xmax=None): - """ - Set 3D x limits. - - See `.Axes.set_xlim` for full documentation. - """ - return self.xaxis._set_lim( - left, right, xmin, xmax, emit=emit, auto=auto, - names=("left", "right")) - - def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, - *, ymin=None, ymax=None): - """ - Set 3D y limits. - - See `.Axes.set_ylim` for full documentation. - """ - return self.yaxis._set_lim( - bottom, top, ymin, ymax, emit=emit, auto=auto, - names=("bottom", "top")) - - def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, - *, zmin=None, zmax=None): + # set_xlim, set_ylim are directly inherited from base Axes. + def set_zlim(self, bottom=None, top=None, emit=True, auto=False, + *, zmin=None, zmax=None): """ Set 3D z limits. See `.Axes.set_ylim` for full documentation """ - return self.zaxis._set_lim( - bottom, top, zmin, zmax, emit=emit, auto=auto, - names=("bottom", "top")) - - def get_xlim3d(self): + if top is None and np.iterable(bottom): + bottom, top = bottom + if zmin is not None: + if bottom is not None: + raise TypeError("Cannot pass both 'bottom' and 'zmin'") + bottom = zmin + if zmax is not None: + if top is not None: + raise TypeError("Cannot pass both 'top' and 'zmax'") + top = zmax + return self.zaxis._set_lim(bottom, top, emit=emit, auto=auto) + + set_xlim3d = maxes.Axes.set_xlim + set_ylim3d = maxes.Axes.set_ylim + set_zlim3d = set_zlim + + def get_xlim(self): + # docstring inherited return tuple(self.xy_viewLim.intervalx) - get_xlim3d.__doc__ = maxes.Axes.get_xlim.__doc__ - def get_ylim3d(self): + def get_ylim(self): + # docstring inherited return tuple(self.xy_viewLim.intervaly) - get_ylim3d.__doc__ = maxes.Axes.get_ylim.__doc__ - def get_zlim3d(self): + def get_zlim(self): """Get 3D z limits.""" return tuple(self.zz_viewLim.intervalx)