diff --git a/doc/api/api_changes.rst b/doc/api/api_changes.rst index a17ef4971e52..abfa4247b34a 100644 --- a/doc/api/api_changes.rst +++ b/doc/api/api_changes.rst @@ -48,6 +48,23 @@ original location: - mstream -> `from matplotlib import stream as mstream` - mtable -> `from matplotlib import table as mtable` +* To accommodate adding asymmetric margins, additional optional arguments +were added to `axes.set_ymargins`, `axes.set_xmargins`, and `axes3D.set_zmargins` +and the return values of `axes.margins()` and `axes3D.margins()` were changed. + + - `axes.set_xmargin(m)` -> `axes.set_xmargin(left, right=None)` + - `axes.set_ymargin(m)` -> `axes.set_xmargin(bottom, top=None)` + - `axes3D.set_xmargin(m)` -> `axes.set_zmargin(down, up=None)` + +If `None` is passed as the second argument, symmetric margins are used +(original behavior). + + - `axes.margins()`, `plt.margins()` return value `(xmargin, ymargin)` -> + `((left, right), (bottom, top))` + - `axes3D.margins()` return value `(xmargin, ymargin)` -> + `((left, right), (bottom, top), (down, up))` + + .. _changes_in_1_3: Changes in 1.3.x diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index 24f09ac3f30b..d8bb992e8b64 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -29,8 +29,8 @@ New plotting features Support for datetime axes in 2d plots ````````````````````````````````````` Andrew Dawson added support for datetime axes to -:func:`~matplotlib.pyplot.contour`, :func:`~matplotlib.pyplot.contourf`, -:func:`~matplotlib.pyplot.pcolormesh` and :func:`~matplotlib.pyplot.pcolor`. +:func:`~matplotlib.pyplot.contour`, :func:`~matplotlib.pyplot.contourf`, +:func:`~matplotlib.pyplot.pcolormesh` and :func:`~matplotlib.pyplot.pcolor`. Date handling @@ -44,6 +44,15 @@ and :func:`matplotlib.dates.datestr2num`. Support is also added to the unit conversion interfaces :class:`matplotlib.dates.DateConverter` and :class:`matplotlib.units.Registry`. +Asymmetric margins +------------------ + +:func:`matplotlib.Axes.set_xmargin`, :func:`matplotlib.Axes.set_ymargin`, and +:func:`matplotlib.Axes3D.set_zmargin` now take two arguments to set asymmetric margins. +The return types on :func:`matplotlib.Axes.margins()` changed to a tuple of tuples to return +the asymmetric margins. + + .. _whats-new-1-3: new in matplotlib-1.3 diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 986d5fa6b52f..4dd7958af15a 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3292,11 +3292,24 @@ def scatter(self, x, y, s=20, c='b', marker='o', cmap=None, norm=None, # to data coords to get the exact bounding box for efficiency # reasons. It can be done right if this is deemed important. # Also, only bother with this padding if there is anything to draw. - if self._xmargin < 0.05 and x.size > 0: - self.set_xmargin(0.05) - if self._ymargin < 0.05 and x.size > 0: - self.set_ymargin(0.05) + # don't use the function, as it gets overloaded in Axes3D + (xm_left, xm_right) = (self._xmargin_left, self._xmargin_right) + # enforce minimum padding + if xm_left < 0.05 and x.size > 0: + xm_left = 0.05 + if xm_right < 0.05 and x.size > 0: + xm_right = 0.05 + + (ym_bot, ym_top) = (self._ymargin_bot, self._ymargin_top) + if ym_bot < 0.05 and y.size > 0: + ym_bot = 0.05 + + if ym_top < 0.05 and y.size > 0: + ym_top = 0.05 + + self.set_xmargin(xm_left, xm_right) + self.set_ymargin(ym_bot, ym_top) self.add_collection(collection) self.autoscale_view() diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 99f09156a006..bbdfc82ba223 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -870,8 +870,10 @@ def cla(self): self._autoscaleXon = True self._autoscaleYon = True - self._xmargin = rcParams['axes.xmargin'] - self._ymargin = rcParams['axes.ymargin'] + self._xmargin_left = rcParams['axes.xmargin'] + self._xmargin_right = rcParams['axes.xmargin'] + self._ymargin_top = rcParams['axes.ymargin'] + self._ymargin_bot = rcParams['axes.ymargin'] self._tight = False self._update_transScale() # needed? @@ -1734,31 +1736,60 @@ def set_autoscaley_on(self, b): """ self._autoscaleYon = b - def set_xmargin(self, m): + def set_xmargin(self, left, right=None): """ Set padding of X data limits prior to autoscaling. - *m* times the data interval will be added to each - end of that interval before it is used in autoscaling. + Parameters + ---------- + left : float in [0, 1] + The fraction of the data interval to add to the left + of the x-range used in autoscaling + + right : float in [0, 1] or None + The fraction of the data interval to add to the right + of the x-range used in autoscaling. + + If None, then symmetric margins are used (`right = left`) - accepts: float in range 0 to 1 """ - if m < 0 or m > 1: + if right is None: + right = left + + if right < 0 or right > 1: raise ValueError("margin must be in range 0 to 1") - self._xmargin = m + if left < 0 or left > 1: + raise ValueError("margin must be in range 0 to 1") + + self._xmargin_left = left + self._xmargin_right = right - def set_ymargin(self, m): + def set_ymargin(self, bottom, top=None): """ Set padding of Y data limits prior to autoscaling. - *m* times the data interval will be added to each - end of that interval before it is used in autoscaling. + Parameters + ---------- + bottom : float in [0, 1] + The fraction of the data interval to add to the bottom + of the y-range used in autoscaling + + top : float in [0, 1] or None + The fraction of the data interval to add to the top + of the y-range used in autoscaling. + + If None, then symmetric margins are used (`top = bottom`) - accepts: float in range 0 to 1 """ - if m < 0 or m > 1: - raise ValueError("margin must be in range 0 to 1") - self._ymargin = m + if top is None: + bottom = top + if bottom < 0 or bottom > 1: + raise ValueError("bottom margin must be in range 0 to 1") + if top < 0 or top > 1: + raise ValueError("top margin must be in range 0 to 1") + + self._ymargin_top = top + self._ymargin_bot = bottom def margins(self, *args, **kw): """ @@ -1768,7 +1799,7 @@ def margins(self, *args, **kw): margins() - returns xmargin, ymargin + returns (xmargin_left, xmargin_right), (ymargin_bottom, ymargin_top) :: @@ -1796,7 +1827,7 @@ def margins(self, *args, **kw): """ if not args and not kw: - return self._xmargin, self._ymargin + return (self._xmargin_left, self._xmargin_right), (self._ymargin_bot, self._ymargin_top) tight = kw.pop('tight', True) mx = kw.pop('x', None) @@ -1905,10 +1936,10 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): # Default nonsingular for, e.g., MaxNLocator x0, x1 = mtransforms.nonsingular(x0, x1, increasing=False, expander=0.05) - if self._xmargin > 0: - delta = (x1 - x0) * self._xmargin - x0 -= delta - x1 += delta + delta = (x1 - x0) + # if the margin is 0, then this is a no-op -> no need for if statements + x0 -= delta * self._xmargin_left + x1 += delta * self._xmargin_right if not _tight: x0, x1 = xlocator.view_limits(x0, x1) self.set_xbound(x0, x1) @@ -1924,10 +1955,9 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): except AttributeError: y0, y1 = mtransforms.nonsingular(y0, y1, increasing=False, expander=0.05) - if self._ymargin > 0: - delta = (y1 - y0) * self._ymargin - y0 -= delta - y1 += delta + delta = (y1 - y0) + y0 -= delta * self._ymargin_bot + y1 += delta * self._ymargin_top if not _tight: y0, y1 = ylocator.view_limits(y0, y1) self.set_ybound(y0, y1) @@ -3249,5 +3279,3 @@ def get_shared_x_axes(self): def get_shared_y_axes(self): 'Return a copy of the shared axes Grouper object for y axes' return self._shared_y_axes - - diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index dacf33794470..74cc4d16df68 100755 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -321,21 +321,35 @@ def set_autoscalez_on(self, b) : """ self._autoscalez_on = b - def set_zmargin(self, m) : + def set_zmargin(self, down, up=None): """ Set padding of Z data limits prior to autoscaling. - *m* times the data interval will be added to each - end of that interval before it is used in autoscaling. + Parameters + ---------- + down : float in [0, 1] + The fraction of the data interval to add to the bottom + of the z-range used in autoscaling - accepts: float in range 0 to 1 + up : float in [0, 1] or None + The fraction of the data interval to add to the top + of the z-range used in autoscaling. + + If None, then symmetric margins are used (`up = down`) .. versionadded :: 1.1.0 This function was added, but not tested. Please report any bugs. """ - if m < 0 or m > 1 : + if up is None: + down = up + + if down < 0 or down > 1: + raise ValueError("margin must be in range 0 to 1") + if up < 0 or up > 1: raise ValueError("margin must be in range 0 to 1") - self._zmargin = m + + self._zmargin_up = up + self._zmargin_down = down def margins(self, *args, **kw) : """ @@ -344,7 +358,9 @@ def margins(self, *args, **kw) : signatures:: margins() - returns xmargin, ymargin, zmargin + returns ((xmargin_left, xmargin_right), + (ymargin_bottom, ymargin_top), + (zmargin_down, mzargin_up)) :: @@ -374,7 +390,9 @@ def margins(self, *args, **kw) : This function was added, but not tested. Please report any bugs. """ if not args and not kw: - return self._xmargin, self._ymargin, self._zmargin + return ((self._xmargin_left, self._xmargin_right), + (self._ymargin_bot, self._ymargin_top), + (self._zmargin_down, self._zmargin_up)) tight = kw.pop('tight', True) mx = kw.pop('x', None) @@ -493,10 +511,11 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, except AttributeError: x0, x1 = mtransforms.nonsingular(x0, x1, increasing=False, expander=0.05) - if self._xmargin > 0: - delta = (x1 - x0) * self._xmargin - x0 -= delta - x1 += delta + delta = (x1 - x0) + # if the margin is 0, then this is a no-op -> no need for if statements + x0 -= delta * self._xmargin_left + x1 += delta * self._xmargin_right + if not _tight: x0, x1 = xlocator.view_limits(x0, x1) self.set_xbound(x0, x1) @@ -512,10 +531,10 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, except AttributeError: y0, y1 = mtransforms.nonsingular(y0, y1, increasing=False, expander=0.05) - if self._ymargin > 0: - delta = (y1 - y0) * self._ymargin - y0 -= delta - y1 += delta + delta = (y1 - y0) + y0 -= delta * self._ymargin_bot + y1 += delta * self._ymargin_top + if not _tight: y0, y1 = ylocator.view_limits(y0, y1) self.set_ybound(y0, y1) @@ -531,10 +550,9 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, except AttributeError: z0, z1 = mtransforms.nonsingular(z0, z1, increasing=False, expander=0.05) - if self._zmargin > 0: - delta = (z1 - z0) * self._zmargin - z0 -= delta - z1 += delta + delta = (z1 - z0) + z0 -= delta * self._zmargin_down + z1 += delta * self._zmargin_up if not _tight: z0, z1 = zlocator.view_limits(z0, z1) self.set_zbound(z0, z1) @@ -1040,7 +1058,8 @@ def cla(self): self.zaxis._set_scale('linear') self._autoscaleZon = True - self._zmargin = 0 + self._zmargin_up = 0 + self._zmargin_down = 0 Axes.cla(self) @@ -2185,8 +2204,12 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c='b', *args, **kwargs): is_2d = False art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir) - if self._zmargin < 0.05 and xs.size > 0: - self.set_zmargin(0.05) + zm_down, zm_up = self._zmargin_down, self._zmargin_up + if zm_down < 0.05 and xs.size > 0: + zm_down = 0.05 + if zm_up < 0.05 and xs.size > 0: + zm_up = 0.05 + self.set_zmargin(zm_down, zm_up) #FIXME: why is this necessary? if not is_2d: