diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index a41160789fbd..480dc744c9e8 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -3774,7 +3774,11 @@ def scatter(self, x, y, s=20, c=None, marker='o', cmap=None, norm=None, self.set_ymargin(0.05) self.add_collection(collection) - self.autoscale_view() + try: + self.autoscale_view() + except NotImplementedError: + # This happens if the axes are not separable. + pass return collection diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index b63394713ce5..05e5baa02624 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1994,7 +1994,32 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): else: _tight = self._tight = bool(tight) - if scalex and self._autoscaleXon: + if not self._autoscaleXon: + scalex = False + if not self._autoscaleYon: + scaley = False + + if not scalex and not scaley: + return + + # In order to implement margins in a fully general way that respects + # all possible (separable) coordinate transformations, we need to do + # the autoscaling in three steps: + # 1. Compute the limits required to include the data points in this + # axis and any shared axes, without accounting for margins. + # 2. If there are nonzero margins, update self.viewLim, so that + # self.transLimits will compute the transformation required to map + # the new view limits to (0,1). Use self.transLimits.inverted() + # to calculate the view limits that correspond + # to (-self._xmargin, -self._ymargin) + # and (1 + self._xmargin, 1 + self._ymargin). + # 3. Finally, let the axis locators expand the view limits if + # necessary to align them with tick marks. + # It's marginally more efficient to do each step for both axes, before + # proceeding to the next step. + + # Step 1 + if scalex: xshared = self._shared_x_axes.get_siblings(self) dl = [ax.dataLim for ax in xshared] # ignore non-finite data limits if good limits exist @@ -2012,15 +2037,8 @@ 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 - if not _tight: - x0, x1 = xlocator.view_limits(x0, x1) - self.set_xbound(x0, x1) - if scaley and self._autoscaleYon: + if scaley: yshared = self._shared_y_axes.get_siblings(self) dl = [ax.dataLim for ax in yshared] # ignore non-finite data limits if good limits exist @@ -2036,10 +2054,40 @@ 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 + + # Step 2 + if self._xmargin > 0 or self._ymargin > 0: + # Handling the x and y transformations at the same time allows us + # to save a couple calls to invTrans.transform(). + try: + transLimits = self.transLimits + except AttributeError: + raise NotImplementedError( + 'margins are not implemented for non-separable axes') + else: + # Update bounds after checking for transLimits because if the + # axes are not separable, we should allow the caller to catch + # the NotImplementedError without there being any lasting + # changes to the view. + if scalex: + self.set_xbound(x0, x1) + if scaley: + self.set_ybound(y0, y1) + invTrans = (self.transScale + transLimits).inverted() + assert(x0 < x1 and y0 < y1) + p0 = invTrans.transform((-self._xmargin, -self._ymargin)) + p1 = invTrans.transform((1 + self._xmargin, 1 + self._ymargin)) + if scalex: + x0, x1 = p0[0], p1[0] + if scaley: + y0, y1 = p0[1], p1[1] + + # Step 3 + if scalex: + if not _tight: + x0, x1 = xlocator.view_limits(x0, x1) + self.set_xbound(x0, x1) + if scaley: if not _tight: y0, y1 = ylocator.view_limits(y0, y1) self.set_ybound(y0, y1) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 299e9e0aa21a..fec23cef24d9 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -3645,6 +3645,48 @@ def test_margins(): assert_equal(ax3.margins(), (1, 0.5)) +@cleanup +def test_view_limits_with_margins(): + '''Tests that autoscale_view correctly calculates the new view limits + with a nonzero margin, for various kinds of plots. + + The test uses a linear plot, a logarithmic plot, a symlog plot, + and a polar plot as an example of a nonlinear coordinate transformation.''' + + fig1, ax1 = plt.subplots(1, 1) + data = [0.1, 1] + ax1.plot(data, data) + ax1.margins(0.5) + np.testing.assert_array_almost_equal(ax1.get_xbound(), (-0.35, 1.45)) + np.testing.assert_array_almost_equal(ax1.get_ybound(), (-0.35, 1.45)) + + fig2, ax2 = plt.subplots(1, 1) + data = [0.1, 1] + ax2.loglog(data, data) + ax2.margins(0.5) + np.testing.assert_array_almost_equal(ax2.get_xbound(), (0.1 / np.sqrt(10), np.sqrt(10))) + np.testing.assert_array_almost_equal(ax2.get_ybound(), (0.1 / np.sqrt(10), np.sqrt(10))) + + fig3, ax3 = plt.subplots(1, 1) + DATAMAX = 10 + data = np.linspace(-DATAMAX, DATAMAX, 101) + ax3.plot(data, data) + BASE = 10 + ax3.set_xscale('symlog', linthreshx=1, linscalex=1, basex=BASE) + ax3.set_yscale('symlog', linthreshy=1, linscaley=1, basey=BASE) + MARGIN = 0.5 + ax3.margins(MARGIN) + # To understand the following line, see the implementation of + # matplotlib.scale.SymmetricalLogTransform.transform_non_affine() + BOUND = DATAMAX * BASE ** (2 * (1 + BASE / (BASE - 1)) * MARGIN) + np.testing.assert_array_almost_equal(ax3.get_xbound(), (-BOUND, BOUND)) + np.testing.assert_array_almost_equal(ax3.get_ybound(), (-BOUND, BOUND)) + + ax4 = plt.subplot(111, polar=True) + data = np.linspace(0, 2*np.pi, 51) + ax4.plot(data, data) + assert_raises(NotImplementedError, ax4.margins, 0.5) + @cleanup def test_length_one_hist(): fig, ax = plt.subplots() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy