From a802405069e887a394791ceded23499979b0761b Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 11 Sep 2020 01:42:27 +0200 Subject: [PATCH] Reuse InsetLocator to make twinned axes follow their parents. From a positioning PoV, a twinned axes is just like an inset axes whose position exactly matches the parent's position. Doing so removes the need for the heuristic in `Figure.subplots_adjust`/`GridSpec.update` where a non-gridspec-managed Axes would track the position of any Axes with which it shared either xaxis or yaxis, which was a proxy for twinning (per 721b949). This would cause incorrect behavior in rare cases such as ```python from pylab import * ax = subplot(221) axes([.6, .6, .3, .3], sharex=ax) subplots_adjust(left=0) ``` where the `subplots_adjust` call would make the second axes go on top of the first. --- lib/matplotlib/axes/_axes.py | 30 +++----------------------- lib/matplotlib/axes/_base.py | 30 +++++++++++++++++++++++++- lib/matplotlib/axes/_secondary_axes.py | 10 ++++----- lib/matplotlib/figure.py | 10 +-------- lib/matplotlib/gridspec.py | 13 +---------- lib/matplotlib/tests/test_axes.py | 9 ++++++++ 6 files changed, 47 insertions(+), 55 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index c303c4d215da..cec0ad4490a3 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -31,38 +31,14 @@ import matplotlib.tri as mtri import matplotlib.units as munits from matplotlib import _preprocess_data, rcParams -from matplotlib.axes._base import _AxesBase, _process_plot_format +from matplotlib.axes._base import ( + _AxesBase, _TransformedBoundsLocator, _process_plot_format) from matplotlib.axes._secondary_axes import SecondaryAxis from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer _log = logging.getLogger(__name__) -class _InsetLocator: - """ - Axes locator for `.Axes.inset_axes`. - - The locater is a callable object used in `.Axes.set_aspect` to compute the - axes location depending on the renderer. - """ - - def __init__(self, bounds, transform): - """ - *bounds* (a ``[l, b, w, h]`` rectangle) and *transform* together - specify the position of the inset axes. - """ - self._bounds = bounds - self._transform = transform - - def __call__(self, ax, renderer): - # Subtracting transFigure will typically rely on inverted(), freezing - # the transform; thus, this needs to be delayed until draw time as - # transFigure may otherwise change after this is evaluated. - return mtransforms.TransformedBbox( - mtransforms.Bbox.from_bounds(*self._bounds), - self._transform - ax.figure.transFigure) - - # The axes module contains all the wrappers to plotting functions. # All the other methods should go in the _AxesBase class. @@ -365,7 +341,7 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): kwargs.setdefault('label', 'inset_axes') # This puts the rectangle into figure-relative coordinates. - inset_locator = _InsetLocator(bounds, transform) + inset_locator = _TransformedBoundsLocator(bounds, transform) bounds = inset_locator(self, None).bounds inset_ax = Axes(self.figure, bounds, zorder=zorder, **kwargs) # this locator lets the axes move if in data coordinates. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 6db84e3a0836..a1ac56b30ef0 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -85,6 +85,31 @@ def wrapper(self, *args, **kwargs): setattr(owner, name, wrapper) +class _TransformedBoundsLocator: + """ + Axes locator for `.Axes.inset_axes` and similarly positioned axes. + + The locator is a callable object used in `.Axes.set_aspect` to compute the + axes location depending on the renderer. + """ + + def __init__(self, bounds, transform): + """ + *bounds* (a ``[l, b, w, h]`` rectangle) and *transform* together + specify the position of the inset axes. + """ + self._bounds = bounds + self._transform = transform + + def __call__(self, ax, renderer): + # Subtracting transFigure will typically rely on inverted(), freezing + # the transform; thus, this needs to be delayed until draw time as + # transFigure may otherwise change after this is evaluated. + return mtransforms.TransformedBbox( + mtransforms.Bbox.from_bounds(*self._bounds), + self._transform - ax.figure.transFigure) + + def _process_plot_format(fmt): """ Convert a MATLAB style color/line style format string to a (*linestyle*, @@ -4336,7 +4361,10 @@ def _make_twin_axes(self, *args, **kwargs): # Typically, SubplotBase._make_twin_axes is called instead of this. if 'sharex' in kwargs and 'sharey' in kwargs: raise ValueError("Twinned Axes may share only one axis") - ax2 = self.figure.add_axes(self.get_position(True), *args, **kwargs) + ax2 = self.figure.add_axes( + self.get_position(True), *args, **kwargs, + axes_locator=_TransformedBoundsLocator( + [0, 0, 1, 1], self.transAxes)) self.set_adjustable('datalim') ax2.set_adjustable('datalim') self._twinned_axes.join(self, ax2) diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index 73b46e565610..6ca243f0a2ca 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -3,8 +3,7 @@ import matplotlib.cbook as cbook import matplotlib.docstring as docstring import matplotlib.ticker as mticker -from matplotlib.axes import _axes -from matplotlib.axes._base import _AxesBase +from matplotlib.axes._base import _AxesBase, _TransformedBoundsLocator class SecondaryAxis(_AxesBase): @@ -114,13 +113,12 @@ def set_location(self, location): else: bounds = [self._pos, 0, 1e-10, 1] - secondary_locator = _axes._InsetLocator(bounds, self._parent.transAxes) - # this locator lets the axes move in the parent axes coordinates. # so it never needs to know where the parent is explicitly in # figure coordinates. - # it gets called in `ax.apply_aspect() (of all places) - self.set_axes_locator(secondary_locator) + # it gets called in ax.apply_aspect() (of all places) + self.set_axes_locator( + _TransformedBoundsLocator(bounds, self._parent.transAxes)) def apply_aspect(self, position=None): # docstring inherited. diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 34789932a8c0..b126003417b6 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2424,15 +2424,7 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None, "constrained_layout==False. ") self.subplotpars.update(left, bottom, right, top, wspace, hspace) for ax in self.axes: - if not isinstance(ax, SubplotBase): - # Check if sharing a subplots axis - if isinstance(ax._sharex, SubplotBase): - ax._sharex.update_params() - ax.set_position(ax._sharex.figbox) - elif isinstance(ax._sharey, SubplotBase): - ax._sharey.update_params() - ax.set_position(ax._sharey.figbox) - else: + if isinstance(ax, SubplotBase): ax.update_params() ax.set_position(ax.figbox) self.stale = True diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 391c409320c3..2956c2343619 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -486,18 +486,7 @@ def update(self, **kwargs): raise AttributeError(f"{k} is an unknown keyword") for figmanager in _pylab_helpers.Gcf.figs.values(): for ax in figmanager.canvas.figure.axes: - # copied from Figure.subplots_adjust - if not isinstance(ax, mpl.axes.SubplotBase): - # Check if sharing a subplots axis - if isinstance(ax._sharex, mpl.axes.SubplotBase): - if ax._sharex.get_subplotspec().get_gridspec() == self: - ax._sharex.update_params() - ax._set_position(ax._sharex.figbox) - elif isinstance(ax._sharey, mpl.axes.SubplotBase): - if ax._sharey.get_subplotspec().get_gridspec() == self: - ax._sharey.update_params() - ax._set_position(ax._sharey.figbox) - else: + if isinstance(ax, mpl.axes.SubplotBase): ss = ax.get_subplotspec().get_topmost_subplotspec() if ss.get_gridspec() == self: ax.update_params() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index a35f55a55db9..3cdfbd107101 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6513,3 +6513,12 @@ def test_multiplot_autoscale(): ax2.axhspan(-5, 5) xlim = ax1.get_xlim() assert np.allclose(xlim, [0.5, 4.5]) + + +def test_sharing_does_not_link_positions(): + fig = plt.figure() + ax0 = fig.add_subplot(221) + ax1 = fig.add_axes([.6, .6, .3, .3], sharex=ax0) + init_pos = ax1.get_position() + fig.subplots_adjust(left=0) + assert (ax1.get_position().get_points() == init_pos.get_points()).all()