diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 891b1ca8b065..db8d8c5fe30b 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -64,7 +64,7 @@ ###################################################### def do_constrained_layout(fig, h_pad, w_pad, - hspace=None, wspace=None): + hspace=None, wspace=None, rect=(0, 0, 1, 1)): """ Do the constrained_layout. Called at draw time in ``figure.constrained_layout()`` @@ -87,6 +87,10 @@ def do_constrained_layout(fig, h_pad, w_pad, of 0.1 of the figure width between each column. If h/wspace < h/w_pad, then the pads are used instead. + rect : tuple of 4 floats + Rectangle in figure coordinates to perform constrained layout in + [left, bottom, width, height], each from 0-1. + Returns ------- layoutgrid : private debugging structure @@ -94,7 +98,7 @@ def do_constrained_layout(fig, h_pad, w_pad, renderer = get_renderer(fig) # make layoutgrid tree... - layoutgrids = make_layoutgrids(fig, None) + layoutgrids = make_layoutgrids(fig, None, rect=rect) if not layoutgrids['hasgrids']: _api.warn_external('There are no gridspecs with layoutgrids. ' 'Possibly did not call parent GridSpec with the' @@ -133,7 +137,7 @@ def do_constrained_layout(fig, h_pad, w_pad, return layoutgrids -def make_layoutgrids(fig, layoutgrids): +def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)): """ Make the layoutgrid tree. @@ -147,8 +151,9 @@ def make_layoutgrids(fig, layoutgrids): layoutgrids = dict() layoutgrids['hasgrids'] = False if not hasattr(fig, '_parent'): - # top figure - layoutgrids[fig] = mlayoutgrid.LayoutGrid(parent=None, name='figlb') + # top figure; pass rect as parent to allow user-specified + # margins + layoutgrids[fig] = mlayoutgrid.LayoutGrid(parent=rect, name='figlb') else: # subfigure gs = fig._subplotspec.get_gridspec() diff --git a/lib/matplotlib/_layoutgrid.py b/lib/matplotlib/_layoutgrid.py index 8b7b140f600b..90c7b3210e0d 100644 --- a/lib/matplotlib/_layoutgrid.py +++ b/lib/matplotlib/_layoutgrid.py @@ -39,7 +39,7 @@ def __init__(self, parent=None, parent_pos=(0, 0), self.parent_pos = parent_pos self.parent_inner = parent_inner self.name = name + seq_id() - if parent is not None: + if isinstance(parent, LayoutGrid): self.name = f'{parent.name}.{self.name}' self.nrows = nrows self.ncols = ncols @@ -51,8 +51,10 @@ def __init__(self, parent=None, parent_pos=(0, 0), self.width_ratios = np.ones(ncols) sn = self.name + '_' - if parent is None: - self.parent = None + if not isinstance(parent, LayoutGrid): + # parent can be a rect if not a LayoutGrid + # allows specifying a rectangle to contain the layout. + self.parent = parent self.solver = kiwi.Solver() else: self.parent = parent @@ -178,12 +180,13 @@ def parent_constraints(self): # parent's left, the last column right equal to the # parent's right... parent = self.parent - if parent is None: - hc = [self.lefts[0] == 0, - self.rights[-1] == 1, + if not isinstance(parent, LayoutGrid): + # specify a rectangle in figure coordinates + hc = [self.lefts[0] == parent[0], + self.rights[-1] == parent[0] + parent[2], # top and bottom reversed order... - self.tops[0] == 1, - self.bottoms[-1] == 0] + self.tops[0] == parent[1] + parent[3], + self.bottoms[-1] == parent[1]] else: rows, cols = self.parent_pos rows = np.atleast_1d(rows) diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py index aeaf44923d66..fa4281a2ba02 100644 --- a/lib/matplotlib/layout_engine.py +++ b/lib/matplotlib/layout_engine.py @@ -123,7 +123,7 @@ def __init__(self, *, pad=1.08, h_pad=None, w_pad=None, h_pad, w_pad : float Padding (height/width) between edges of adjacent subplots. Defaults to *pad*. - rect : tuple[float, float, float, float], optional + rect : tuple of 4 floats, optional (left, bottom, right, top) rectangle in normalized figure coordinates that the subplots (including labels) will fit into. Defaults to using the entire figure. @@ -179,7 +179,8 @@ class ConstrainedLayoutEngine(LayoutEngine): _colorbar_gridspec = False def __init__(self, *, h_pad=None, w_pad=None, - hspace=None, wspace=None, **kwargs): + hspace=None, wspace=None, rect=(0, 0, 1, 1), + **kwargs): """ Initialize ``constrained_layout`` settings. @@ -197,15 +198,20 @@ def __init__(self, *, h_pad=None, w_pad=None, If h/wspace < h/w_pad, then the pads are used instead. Default to :rc:`figure.constrained_layout.hspace` and :rc:`figure.constrained_layout.wspace`. + rect : tuple of 4 floats + Rectangle in figure coordinates to perform constrained layout in + (left, bottom, width, height), each from 0-1. """ super().__init__(**kwargs) # set the defaults: self.set(w_pad=mpl.rcParams['figure.constrained_layout.w_pad'], h_pad=mpl.rcParams['figure.constrained_layout.h_pad'], wspace=mpl.rcParams['figure.constrained_layout.wspace'], - hspace=mpl.rcParams['figure.constrained_layout.hspace']) + hspace=mpl.rcParams['figure.constrained_layout.hspace'], + rect=(0, 0, 1, 1)) # set anything that was passed in (None will be ignored): - self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace) + self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace, + rect=rect) def execute(self, fig): """ @@ -222,10 +228,11 @@ def execute(self, fig): return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad, wspace=self._params['wspace'], - hspace=self._params['hspace']) + hspace=self._params['hspace'], + rect=self._params['rect']) def set(self, *, h_pad=None, w_pad=None, - hspace=None, wspace=None): + hspace=None, wspace=None, rect=None): """ Set the pads for constrained_layout. @@ -243,6 +250,9 @@ def set(self, *, h_pad=None, w_pad=None, If h/wspace < h/w_pad, then the pads are used instead. Default to :rc:`figure.constrained_layout.hspace` and :rc:`figure.constrained_layout.wspace`. + rect : tuple of 4 floats + Rectangle in figure coordinates to perform constrained layout in + (left, bottom, width, height), each from 0-1. """ for td in self.set.__kwdefaults__: if locals()[td] is not None: diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index b3aea15adad3..bd84d744ac11 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -608,3 +608,21 @@ def test_discouraged_api(): def test_kwargs(): fig, ax = plt.subplots(constrained_layout={'h_pad': 0.02}) fig.draw_without_rendering() + + +def test_rect(): + fig, ax = plt.subplots(layout='constrained') + fig.get_layout_engine().set(rect=[0, 0, 0.5, 0.5]) + fig.draw_without_rendering() + ppos = ax.get_position() + assert ppos.x1 < 0.5 + assert ppos.y1 < 0.5 + + fig, ax = plt.subplots(layout='constrained') + fig.get_layout_engine().set(rect=[0.2, 0.2, 0.3, 0.3]) + fig.draw_without_rendering() + ppos = ax.get_position() + assert ppos.x1 < 0.5 + assert ppos.y1 < 0.5 + assert ppos.x0 > 0.2 + assert ppos.y0 > 0.2 diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 905d3a5e5a52..000000000000 --- a/pytest.ini +++ /dev/null @@ -1,7 +0,0 @@ -# Additional configuration is in matplotlib/testing/conftest.py. -[pytest] -minversion = 3.6 - -testpaths = lib -python_files = test_*.py -junit_family = xunit2 diff --git a/tests.py b/tests.py deleted file mode 100755 index 335fa860fcec..000000000000 --- a/tests.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -# -# This allows running the matplotlib tests from the command line: e.g. -# -# $ python tests.py -v -d -# -# The arguments are identical to the arguments accepted by pytest. -# -# See http://doc.pytest.org/ for a detailed description of these options. - -import sys -import argparse - - -if __name__ == '__main__': - try: - from matplotlib import test - except ImportError: - print('matplotlib.test could not be imported.\n\n' - 'Try a virtual env and `pip install -e .`') - sys.exit(-1) - - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument('--recursionlimit', type=int, default=None, - help='Specify recursionlimit for test run') - args, extra_args = parser.parse_known_args() - - print('Python byte-compilation optimization level:', sys.flags.optimize) - - if args.recursionlimit is not None: # Will trigger deprecation. - retcode = test(argv=extra_args, recursionlimit=args.recursionlimit) - else: - retcode = test(argv=extra_args) - sys.exit(retcode)