diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 9bbb817ec3de..dcb6c13efb75 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3122,8 +3122,8 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, limit unchanged. The left and right xlims may be passed as the tuple - (`left`, `right`) as the first positional argument (or as - the `left` keyword argument). + (*left*, *right*) as the first positional argument (or as + the *left* keyword argument). right : scalar, optional The right xlim in data coordinates. Passing *None* leaves the @@ -3169,7 +3169,7 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, >>> set_xlim(right=right_lim) Limits may be passed in reverse order to flip the direction of - the x-axis. For example, suppose ``x`` represents the number of + the x-axis. For example, suppose *x* represents the number of years before present. The x-axis limits might be set like the following so 5000 years ago is on the left of the plot and the present is on the right. @@ -3510,15 +3510,15 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, limit unchanged. The bottom and top ylims may be passed as the tuple - (`bottom`, `top`) as the first positional argument (or as - the `bottom` keyword argument). + (*bottom*, *top*) as the first positional argument (or as + the *bottom* keyword argument). top : scalar, optional The top ylim in data coordinates. Passing *None* leaves the limit unchanged. emit : bool, optional - Whether to notify observers of limit change (default: True). + Whether to notify observers of limit change (default: ``True``). auto : bool or None, optional Whether to turn on autoscaling of the y-axis. *True* turns on, diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index ff91ca887b22..815653927b6e 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -2,6 +2,7 @@ import types import numpy as np +import warnings from matplotlib.axes import Axes import matplotlib.axis as maxis @@ -57,7 +58,8 @@ def transform_non_affine(self, tr): t += self._axis.get_theta_offset() if self._use_rmin and self._axis is not None: - r = r - self._axis.get_rorigin() + r = (r - self._axis.get_rorigin()) * self._axis.get_rsign() + mask = r < 0 x[:] = np.where(mask, np.nan, r * np.cos(t)) y[:] = np.where(mask, np.nan, r * np.sin(t)) @@ -161,6 +163,7 @@ def transform_non_affine(self, xy): if self._use_rmin and self._axis is not None: r += self._axis.get_rorigin() + r *= self._axis.get_rsign() return np.concatenate((theta, r), 1) transform_non_affine.__doc__ = \ @@ -421,10 +424,9 @@ def __call__(self): # Ensure previous behaviour with full circle non-annular views. if self._axes: if _is_full_circle_rad(*self._axes.viewLim.intervalx): - rorigin = self._axes.get_rorigin() + rorigin = self._axes.get_rorigin() * self._axes.get_rsign() if self._axes.get_rmin() <= rorigin: show_all = False - if show_all: return self.base() else: @@ -444,7 +446,10 @@ def refresh(self): def view_limits(self, vmin, vmax): vmin, vmax = self.base.view_limits(vmin, vmax) - return mtransforms.nonsingular(min(0, vmin), vmax) + if vmax > vmin: + # this allows inverted r/y-lims + vmin = min(0, vmin) + return mtransforms.nonsingular(vmin, vmax) class _ThetaShift(mtransforms.ScaledTranslation): @@ -767,7 +772,6 @@ def __str__(self): def get_points(self): if self._invalid: points = self._viewLim.get_points().copy() - # Scale angular limits to work with Wedge. points[:, 0] *= 180 / np.pi if points[0, 0] > points[1, 0]: @@ -992,8 +996,8 @@ def draw(self, *args, **kwargs): thetamin, thetamax = np.rad2deg(self._realViewLim.intervalx) if thetamin > thetamax: thetamin, thetamax = thetamax, thetamin - rmin, rmax = self._realViewLim.intervaly - self.get_rorigin() - + rmin, rmax = ((self._realViewLim.intervaly - self.get_rorigin()) * + self.get_rsign()) if isinstance(self.patch, mpatches.Wedge): # Backwards-compatibility: Any subclassed Axes might override the # patch to not be the Wedge that PolarAxes uses. @@ -1160,12 +1164,83 @@ def set_rorigin(self, rorigin): def get_rorigin(self): return self._originViewLim.y0 - def set_rlim(self, *args, **kwargs): + def get_rsign(self): + return np.sign(self._originViewLim.y1 - self._originViewLim.y0) + + def set_rlim(self, bottom=None, top=None, emit=True, auto=False, **kwargs): + """ + See `~.polar.PolarAxes.set_ylim`. + """ if 'rmin' in kwargs: - kwargs['ymin'] = kwargs.pop('rmin') + if bottom is None: + bottom = kwargs.pop('rmin') + else: + raise ValueError('Cannot supply both positional "bottom"' + 'argument and kwarg "rmin"') if 'rmax' in kwargs: - kwargs['ymax'] = kwargs.pop('rmax') - return self.set_ylim(*args, **kwargs) + if top is None: + top = kwargs.pop('rmax') + else: + raise ValueError('Cannot supply both positional "top"' + 'argument and kwarg "rmax"') + return self.set_ylim(bottom=bottom, top=top, emit=emit, auto=auto, + **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. + + Parameters + ---------- + bottom : scalar, optional + The bottom limit (default: None, which leaves the bottom + limit unchanged). + The bottom and top ylims may be passed as the tuple + (*bottom*, *top*) as the first positional argument (or as + the *bottom* keyword argument). + + top : scalar, optional + The top limit (default: None, which leaves the top limit + unchanged). + + emit : bool, optional + Whether to notify observers of limit change (default: True). + + auto : bool or None, optional + Whether to turn on autoscaling of the y-axis. True turns on, + False turns off (default action), None leaves unchanged. + + ymin, ymax : scalar, optional + These arguments are deprecated and will be removed in a future + version. They are equivalent to *bottom* and *top* respectively, + and it is an error to pass both *ymin* and *bottom* or + *ymax* and *top*. + + Returns + ------- + ylimits : tuple + Returns the new y-axis limits as (*bottom*, *top*). + + """ + + 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 len(bottom) == 2: + top = bottom[1] + bottom = bottom[0] + + return super().set_ylim(bottom=bottom, top=top, emit=emit, auto=auto) def get_rlabel_position(self): """ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/polar_invertedylim.png b/lib/matplotlib/tests/baseline_images/test_axes/polar_invertedylim.png new file mode 100644 index 000000000000..f3a47e1db88c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/polar_invertedylim.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/polar_invertedylim_rorigin.png b/lib/matplotlib/tests/baseline_images/test_axes/polar_invertedylim_rorigin.png new file mode 100644 index 000000000000..ba0c4e882491 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/polar_invertedylim_rorigin.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 03f2a6de0bc1..f5007fbe8869 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -726,6 +726,23 @@ def test_polar_rorigin(): ax.set_rorigin(0.0) +@image_comparison(baseline_images=['polar_invertedylim'], style='default', + extensions=['png']) +def test_polar_invertedylim(): + fig = plt.figure() + ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True) + ax.set_ylim(2, 0) + + +@image_comparison(baseline_images=['polar_invertedylim_rorigin'], + style='default', extensions=['png']) +def test_polar_invertedylim_rorigin(): + fig = plt.figure() + ax = fig.add_axes([0.1, 0.1, 0.8, 0.8], polar=True) + ax.set_ylim(2, 0) + ax.set_rorigin(3) + + @image_comparison(baseline_images=['polar_theta_position'], style='default') def test_polar_theta_position(): r = np.arange(0, 3.0, 0.01)