From ec4dfbc3c83866f487ff0bc9c87b0d43a1c02b22 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 13 Sep 2021 16:15:11 +0200 Subject: [PATCH 1/2] ENH: implement and use base layout_engine for more flexible layout. --- doc/api/index.rst | 1 + doc/api/layout_engine_api.rst | 9 + .../next_api_changes/behavior/20426-JK.rst | 5 + doc/users/next_whats_new/layout_engine.rst | 7 + lib/matplotlib/_constrained_layout.py | 4 +- lib/matplotlib/_tight_bbox.py | 7 +- lib/matplotlib/backend_bases.py | 6 +- lib/matplotlib/figure.py | 231 ++++++++++------ lib/matplotlib/layout_engine.py | 248 ++++++++++++++++++ .../tests/test_constrainedlayout.py | 103 +++++--- lib/matplotlib/tests/test_figure.py | 61 +++-- lib/matplotlib/tests/test_tightlayout.py | 17 +- .../intermediate/constrainedlayout_guide.py | 83 +++--- 13 files changed, 587 insertions(+), 195 deletions(-) create mode 100644 doc/api/layout_engine_api.rst create mode 100644 doc/api/next_api_changes/behavior/20426-JK.rst create mode 100644 doc/users/next_whats_new/layout_engine.rst create mode 100644 lib/matplotlib/layout_engine.py diff --git a/doc/api/index.rst b/doc/api/index.rst index 1f9048787e69..b5c3d1622ead 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -66,6 +66,7 @@ Alphabetical list of modules: fontconfig_pattern_api.rst gridspec_api.rst image_api.rst + layout_engine_api.rst legend_api.rst legend_handler_api.rst lines_api.rst diff --git a/doc/api/layout_engine_api.rst b/doc/api/layout_engine_api.rst new file mode 100644 index 000000000000..8890061e0979 --- /dev/null +++ b/doc/api/layout_engine_api.rst @@ -0,0 +1,9 @@ +**************************** +``matplotlib.layout_engine`` +**************************** + +.. currentmodule:: matplotlib.layout_engine + +.. automodule:: matplotlib.layout_engine + :members: + :inherited-members: diff --git a/doc/api/next_api_changes/behavior/20426-JK.rst b/doc/api/next_api_changes/behavior/20426-JK.rst new file mode 100644 index 000000000000..cc849c796eac --- /dev/null +++ b/doc/api/next_api_changes/behavior/20426-JK.rst @@ -0,0 +1,5 @@ +Incompatible layout engines raise +--------------------------------- +``tight_layout`` and ``constrained_layout`` are incompatible if +a colorbar has been added to the figure. Invoking the incompatible layout +engine used to warn, but now raises with a ``RuntimeError``. diff --git a/doc/users/next_whats_new/layout_engine.rst b/doc/users/next_whats_new/layout_engine.rst new file mode 100644 index 000000000000..d4f6a752d69e --- /dev/null +++ b/doc/users/next_whats_new/layout_engine.rst @@ -0,0 +1,7 @@ +New ``layout_engine`` module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Matplotlib ships with ``tight_layout`` and ``constrained_layout`` layout +engines. A new ``layout_engine`` module is provided to allow downstream +libraries to write their own layout engines and `~.figure.Figure` objects can +now take a `.LayoutEngine` subclass as an argument to the *layout* parameter. \ No newline at end of file diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 70f595c989ff..7743ca809c52 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -18,6 +18,7 @@ import numpy as np from matplotlib import _api, artist as martist +from matplotlib.backend_bases import _get_renderer import matplotlib.transforms as mtransforms import matplotlib._layoutgrid as mlayoutgrid @@ -62,7 +63,7 @@ ###################################################### -def do_constrained_layout(fig, renderer, h_pad, w_pad, +def do_constrained_layout(fig, h_pad, w_pad, hspace=None, wspace=None): """ Do the constrained_layout. Called at draw time in @@ -91,6 +92,7 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, layoutgrid : private debugging structure """ + renderer = _get_renderer(fig) # make layoutgrid tree... layoutgrids = make_layoutgrids(fig, None) if not layoutgrids['hasgrids']: diff --git a/lib/matplotlib/_tight_bbox.py b/lib/matplotlib/_tight_bbox.py index 2a73624f0535..b2147b960735 100644 --- a/lib/matplotlib/_tight_bbox.py +++ b/lib/matplotlib/_tight_bbox.py @@ -17,11 +17,10 @@ def adjust_bbox(fig, bbox_inches, fixed_dpi=None): """ origBbox = fig.bbox origBboxInches = fig.bbox_inches - orig_tight_layout = fig.get_tight_layout() + orig_layout = fig.get_layout_engine() + fig.set_layout_engine(None) _boxout = fig.transFigure._boxout - fig.set_tight_layout(False) - old_aspect = [] locator_list = [] sentinel = object() @@ -47,7 +46,7 @@ def restore_bbox(): fig.bbox = origBbox fig.bbox_inches = origBboxInches - fig.set_tight_layout(orig_tight_layout) + fig.set_layout_engine(orig_layout) fig.transFigure._boxout = _boxout fig.transFigure.invalidate() fig.patch.set_bounds(0, 0, 1, 1) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 2bb397068160..99b7f7a17586 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2226,7 +2226,7 @@ def print_figure( if bbox_inches is None: bbox_inches = rcParams['savefig.bbox'] - if (self.figure.get_constrained_layout() or + if (self.figure.get_layout_engine() is not None or bbox_inches == "tight"): # we need to trigger a draw before printing to make sure # CL works. "tight" also needs a draw to get the right @@ -2255,8 +2255,8 @@ def print_figure( else: _bbox_inches_restore = None - # we have already done CL above, so turn it off: - stack.enter_context(self.figure._cm_set(constrained_layout=False)) + # we have already done layout above, so turn it off: + stack.enter_context(self.figure._cm_set(layout_engine=None)) try: # _get_renderer may change the figure dpi (as vector formats # force the figure dpi to 72), so we need to set it again here. diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index defe20bd0209..a4b52e7c89bf 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -34,6 +34,8 @@ from matplotlib.axes import Axes, SubplotBase, subplot_class_factory from matplotlib.gridspec import GridSpec +from matplotlib.layout_engine import (ConstrainedLayoutEngine, + TightLayoutEngine, LayoutEngine) import matplotlib.legend as mlegend from matplotlib.patches import Rectangle from matplotlib.text import Text @@ -1134,12 +1136,14 @@ def colorbar( if ax is None: ax = getattr(mappable, "axes", self.gca()) + if (self.get_layout_engine() is not None and + not self.get_layout_engine().colorbar_gridspec): + use_gridspec = False # Store the value of gca so that we can set it back later on. if cax is None: current_ax = self.gca() userax = False - if (use_gridspec and isinstance(ax, SubplotBase) - and not self.get_constrained_layout()): + if (use_gridspec and isinstance(ax, SubplotBase)): cax, kwargs = cbar.make_axes_gridspec(ax, **kwargs) else: cax, kwargs = cbar.make_axes(ax, **kwargs) @@ -1187,12 +1191,13 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None, The height of the padding between subplots, as a fraction of the average Axes height. """ - if self.get_constrained_layout(): - self.set_constrained_layout(False) + if (self.get_layout_engine() is not None and + not self.get_layout_engine().adjust_compatible): _api.warn_external( - "This figure was using constrained_layout, but that is " + "This figure was using a layout engine that is " "incompatible with subplots_adjust and/or tight_layout; " - "disabling constrained_layout.") + "not calling subplots_adjust.") + return self.subplotpars.update(left, bottom, right, top, wspace, hspace) for ax in self.axes: if hasattr(ax, 'get_subplotspec'): @@ -2078,6 +2083,9 @@ def get_constrained_layout_pads(self, relative=False): """ return self._parent.get_constrained_layout_pads(relative=relative) + def get_layout_engine(self): + return self._parent.get_layout_engine() + @property def axes(self): """ @@ -2206,10 +2214,11 @@ def __init__(self, The use of this parameter is discouraged. Please use ``layout='constrained'`` instead. - layout : {'constrained', 'tight'}, optional, default: None + layout : {'constrained', 'tight', `.LayoutEngine`, None}, optional The layout mechanism for positioning of plot elements to avoid overlapping Axes decorations (labels, ticks, etc). Note that layout managers can have significant performance penalties. + Defaults to *None*. - 'constrained': The constrained layout solver adjusts axes sizes to avoid overlapping axes decorations. Can handle complex plot @@ -2223,6 +2232,11 @@ def __init__(self, decorations do not overlap. See `.Figure.set_tight_layout` for further details. + - A `.LayoutEngine` instance. Builtin layout classes are + `.ConstrainedLayoutEngine` and `.TightLayoutEngine`, more easily + accessible by 'constrained' and 'tight'. Passing an instance + allows third parties to provide their own layout engine. + If not given, fall back to using the parameters *tight_layout* and *constrained_layout*, including their config defaults :rc:`figure.autolayout` and :rc:`figure.constrained_layout.use`. @@ -2234,24 +2248,34 @@ def __init__(self, %(Figure:kwdoc)s """ super().__init__(**kwargs) + self._layout_engine = None if layout is not None: - if tight_layout is not None: + if (tight_layout is not None): _api.warn_external( - "The Figure parameters 'layout' and 'tight_layout' " - "cannot be used together. Please use 'layout' only.") - if constrained_layout is not None: + "The Figure parameters 'layout' and 'tight_layout' cannot " + "be used together. Please use 'layout' only.") + if (constrained_layout is not None): _api.warn_external( "The Figure parameters 'layout' and 'constrained_layout' " "cannot be used together. Please use 'layout' only.") - if layout == 'constrained': - tight_layout = False - constrained_layout = True - elif layout == 'tight': - tight_layout = True - constrained_layout = False - else: - _api.check_in_list(['constrained', 'tight'], layout=layout) + self.set_layout_engine(layout=layout) + elif tight_layout is not None: + if constrained_layout is not None: + _api.warn_external( + "The Figure parameters 'tight_layout' and " + "'constrained_layout' cannot be used together. Please use " + "'layout' parameter") + self.set_layout_engine(layout='tight') + if isinstance(tight_layout, dict): + self.get_layout_engine().set(**tight_layout) + elif constrained_layout is not None: + self.set_layout_engine(layout='constrained') + if isinstance(constrained_layout, dict): + self.get_layout_engine().set(**constrained_layout) + else: + # everything is None, so use default: + self.set_layout_engine(layout=layout) self.callbacks = cbook.CallbackRegistry() # Callbacks traditionally associated with the canvas (and exposed with @@ -2302,20 +2326,72 @@ def __init__(self, self.subplotpars = subplotpars - # constrained_layout: - self._constrained = False - - self.set_tight_layout(tight_layout) - self._axstack = _AxesStack() # track all figure axes and current axes self.clf() self._cachedRenderer = None - self.set_constrained_layout(constrained_layout) - # list of child gridspecs for this figure self._gridspecs = [] + def _check_layout_engines_compat(self, old, new): + """ + Helper for set_layout engine + + If the figure has used the old engine and added a colorbar then the + value of colorbar_gridspec must be the same on the new engine. + """ + if old is None or old.colorbar_gridspec == new.colorbar_gridspec: + return True + # colorbar layout different, so check if any colorbars are on the + # figure... + for ax in self.axes: + if hasattr(ax, '_colorbar'): + # colorbars list themselvs as a colorbar. + return False + return True + + def set_layout_engine(self, layout=None, **kwargs): + """ + Set the layout engine for this figure. + + Parameters + ---------- + layout: {'constrained', 'tight'} or `~.LayoutEngine` + 'constrained' will use `~.ConstrainedLayoutEngine`, 'tight' will + use `~.TightLayoutEngine`. Users and libraries can define their + own layout engines as well. + kwargs: dict + The keyword arguments are passed to the layout engine to set things + like padding and margin sizes. Only used if *layout* is a string. + """ + if layout is None: + if mpl.rcParams['figure.autolayout']: + layout = 'tight' + elif mpl.rcParams['figure.constrained_layout.use']: + layout = 'constrained' + else: + self._layout_engine = None + return + if layout == 'tight': + new_layout_engine = TightLayoutEngine(**kwargs) + elif layout == 'constrained': + new_layout_engine = ConstrainedLayoutEngine(**kwargs) + elif isinstance(layout, LayoutEngine): + new_layout_engine = layout + else: + raise ValueError(f"Invalid value for 'layout': {layout!r}") + + if self._check_layout_engines_compat(self._layout_engine, + new_layout_engine): + self._layout_engine = new_layout_engine + else: + raise RuntimeError('Colorbar layout of new layout engine not ' + 'compatible with old engine, and a colorbar ' + 'has been created. Engine not changed.') + + def get_layout_engine(self): + return self._layout_engine + # TODO: I'd like to dynamically add the _repr_html_ method # to the figure in the right context, but then IPython doesn't # use it, for some reason. @@ -2405,8 +2481,9 @@ def _set_dpi(self, dpi, forward=True): def get_tight_layout(self): """Return whether `.tight_layout` is called when drawing.""" - return self._tight + return isinstance(self.get_layout_engine(), TightLayoutEngine) + @_api.deprecated("3.6", alternative="set_layout_engine") def set_tight_layout(self, tight): """ Set whether and how `.tight_layout` is called when drawing. @@ -2421,8 +2498,9 @@ def set_tight_layout(self, tight): """ if tight is None: tight = mpl.rcParams['figure.autolayout'] - self._tight = bool(tight) - self._tight_parameters = tight if isinstance(tight, dict) else {} + _tight_parameters = tight if isinstance(tight, dict) else {} + if bool(tight): + self.set_layout_engine(TightLayoutEngine(**_tight_parameters)) self.stale = True def get_constrained_layout(self): @@ -2431,8 +2509,9 @@ def get_constrained_layout(self): See :doc:`/tutorials/intermediate/constrainedlayout_guide`. """ - return self._constrained + return isinstance(self.get_layout_engine(), ConstrainedLayoutEngine) + @_api.deprecated("3.6", alternative="set_layout_engine('constrained')") def set_constrained_layout(self, constrained): """ Set whether ``constrained_layout`` is used upon drawing. If None, @@ -2449,22 +2528,17 @@ def set_constrained_layout(self, constrained): ---------- constrained : bool or dict or None """ - self._constrained_layout_pads = dict() - self._constrained_layout_pads['w_pad'] = None - self._constrained_layout_pads['h_pad'] = None - self._constrained_layout_pads['wspace'] = None - self._constrained_layout_pads['hspace'] = None if constrained is None: constrained = mpl.rcParams['figure.constrained_layout.use'] - self._constrained = bool(constrained) - if isinstance(constrained, dict): - self.set_constrained_layout_pads(**constrained) - else: - self.set_constrained_layout_pads() + _constrained = bool(constrained) + _parameters = constrained if isinstance(constrained, dict) else {} + if _constrained: + self.set_layout_engine(ConstrainedLayoutEngine(**_parameters)) self.stale = True - def set_constrained_layout_pads(self, *, w_pad=None, h_pad=None, - wspace=None, hspace=None): + @_api.deprecated( + "3.6", alternative="figure.get_layout_engine().set()") + def set_constrained_layout_pads(self, **kwargs): """ Set padding for ``constrained_layout``. @@ -2492,21 +2566,17 @@ def set_constrained_layout_pads(self, *, w_pad=None, h_pad=None, subplot width. The total padding ends up being h_pad + hspace. """ + if isinstance(self.get_layout_engine(), ConstrainedLayoutEngine): + self.get_layout_engine().set(**kwargs) - for name, size in zip(['w_pad', 'h_pad', 'wspace', 'hspace'], - [w_pad, h_pad, wspace, hspace]): - if size is not None: - self._constrained_layout_pads[name] = size - else: - self._constrained_layout_pads[name] = ( - mpl.rcParams[f'figure.constrained_layout.{name}']) - + @_api.deprecated("3.6", alternative="fig.get_layout_engine().get_info()") def get_constrained_layout_pads(self, relative=False): """ Get padding for ``constrained_layout``. Returns a list of ``w_pad, h_pad`` in inches and ``wspace`` and ``hspace`` as fractions of the subplot. + All values are None if ``constrained_layout`` is not used. See :doc:`/tutorials/intermediate/constrainedlayout_guide`. @@ -2515,13 +2585,16 @@ def get_constrained_layout_pads(self, relative=False): relative : bool If `True`, then convert from inches to figure relative. """ - w_pad = self._constrained_layout_pads['w_pad'] - h_pad = self._constrained_layout_pads['h_pad'] - wspace = self._constrained_layout_pads['wspace'] - hspace = self._constrained_layout_pads['hspace'] + if not isinstance(self.get_layout_engine(), ConstrainedLayoutEngine): + return None, None, None, None + info = self.get_layout_engine().get_info() + w_pad = info['w_pad'] + h_pad = info['h_pad'] + wspace = info['wspace'] + hspace = info['hspace'] if relative and (w_pad is not None or h_pad is not None): - renderer = _get_renderer(self) + renderer = _get_renderer(self).dpi dpi = renderer.dpi w_pad = w_pad * dpi / renderer.width h_pad = h_pad * dpi / renderer.height @@ -2792,14 +2865,11 @@ def draw(self, renderer): return artists = self._get_draw_artists(renderer) - try: renderer.open_group('figure', gid=self.get_gid()) - if self.get_constrained_layout() and self.axes: - self.execute_constrained_layout(renderer) - if self.get_tight_layout() and self.axes: + if self.axes and self.get_layout_engine() is not None: try: - self.tight_layout(**self._tight_parameters) + self.get_layout_engine().execute(self) except ValueError: pass # ValueError can occur when resizing a window. @@ -3132,6 +3202,7 @@ def handler(ev): return None if event is None else event.name == "key_press_event" + @_api.deprecated("3.6", alternative="figure.get_layout_engine().execute()") def execute_constrained_layout(self, renderer=None): """ Use ``layoutgrid`` to determine pos positions within Axes. @@ -3142,20 +3213,9 @@ def execute_constrained_layout(self, renderer=None): ------- layoutgrid : private debugging object """ - - from matplotlib._constrained_layout import do_constrained_layout - - _log.debug('Executing constrainedlayout') - w_pad, h_pad, wspace, hspace = self.get_constrained_layout_pads() - # convert to unit-relative lengths - fig = self - width, height = fig.get_size_inches() - w_pad = w_pad / width - h_pad = h_pad / height - if renderer is None: - renderer = _get_renderer(fig) - return do_constrained_layout(fig, renderer, h_pad, w_pad, - hspace, wspace) + if not isinstance(self.get_layout_engine(), ConstrainedLayoutEngine): + return None + return self.get_layout_engine().execute(self) def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None): """ @@ -3179,24 +3239,25 @@ def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None): See Also -------- - .Figure.set_tight_layout + .Figure.set_layout_engine .pyplot.tight_layout """ - from contextlib import nullcontext - from ._tight_layout import ( - get_subplotspec_list, get_tight_layout_figure) + from ._tight_layout import get_subplotspec_list subplotspec_list = get_subplotspec_list(self.axes) if None in subplotspec_list: _api.warn_external("This figure includes Axes that are not " "compatible with tight_layout, so results " "might be incorrect.") - renderer = _get_renderer(self) - with getattr(renderer, "_draw_disabled", nullcontext)(): - kwargs = get_tight_layout_figure( - self, self.axes, subplotspec_list, renderer, - pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) - if kwargs: - self.subplots_adjust(**kwargs) + # note that here we do not permanently set the figures engine to + # tight_layout but rather just perform the layout in place and remove + # any previous engines. + engine = TightLayoutEngine(pad=pad, h_pad=h_pad, w_pad=w_pad, + rect=rect) + try: + self.set_layout_engine(engine) + engine.execute(self) + finally: + self.set_layout_engine(None) def figaspect(arg): diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py new file mode 100644 index 000000000000..4d7927b86771 --- /dev/null +++ b/lib/matplotlib/layout_engine.py @@ -0,0 +1,248 @@ +""" +Classes to layout elements in a `.Figure`. + +Figures have a ``layout_engine`` property that holds a subclass of +`~.LayoutEngine` defined here (or *None* for no layout). At draw time +``figure.get_layout_engine().execute()`` is called, the goal of which is +usually to rearrange Axes on the figure to produce a pleasing layout. This is +like a ``draw`` callback, however when printing we disable the layout engine +for the final draw and it is useful to know the layout engine while the figure +is being created, in particular to deal with colorbars. + +Matplotlib supplies two layout engines, `.TightLayoutEngine` and +`.ConstrainedLayoutEngine`. Third parties can create their own layout engine +by subclassing `.LayoutEngine`. +""" + +from contextlib import nullcontext + +import matplotlib as mpl +import matplotlib._api as _api + +from matplotlib._constrained_layout import do_constrained_layout +from matplotlib._tight_layout import (get_subplotspec_list, + get_tight_layout_figure) +from matplotlib.backend_bases import _get_renderer + + +class LayoutEngine: + """ + Base class for Matplotlib layout engines. + + A layout engine can be passed to a figure at instantiation or at any time + with `~.figure.Figure.set_layout_engine`. Once attached to a figure, the + layout engine ``execute`` function is called at draw time by + `~.figure.Figure.draw`, providing a special draw-time hook. + + .. note :: + + However, note that layout engines affect the creation of colorbars, so + `~.figure.Figure.set_layout_engine` should be called before any + colorbars are created. + + Currently, there are two properties of `LayoutEngine` classes that are + consulted while manipulating the figure: + + - ``engine.colorbar_gridspec`` tells `.Figure.colorbar` whether to make the + axes using the gridspec method (see `.colorbar.make_axes_gridspec`) or + not (see `.colorbar.make_axes`); + - ``engine.adjust_compatible`` stops `.Figure.subplots_adjust` from being + run if it is not compatible with the layout engine. + + To implement a custom `LayoutEngine`: + + 1. override ``_adjust_compatible`` and ``_colorbar_gridspec`` + 2. override `LayoutEngine.set` to update *self._params* + 3. override `LayoutEngine.execute` with your implementation + + """ + # override these is sub-class + _adjust_compatible = None + _colorbar_gridspec = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._params = {} + + def set(self, **kwargs): + raise NotImplementedError + + @property + def colorbar_gridspec(self): + """ + Return a boolean if the layout engine creates colorbars using a + gridspec. + """ + if self._colorbar_gridspec is None: + raise NotImplementedError + return self._colorbar_gridspec + + @property + def adjust_compatible(self): + """ + Return a boolean if the layout engine is compatible with + `~.Figure.subplots_adjust`. + """ + if self._adjust_compatible is None: + raise NotImplementedError + return self._adjust_compatible + + def get(self): + """ + Return copy of the parameters for the layout engine. + """ + return dict(self._params) + + def execute(self, fig): + """ + Execute the layout on the figure given by *fig*. + """ + # subclasses must impliment this. + raise NotImplementedError + + +class TightLayoutEngine(LayoutEngine): + """ + Implements the ``tight_layout`` geometry management. See + :doc:`/tutorials/intermediate/tight_layout_guide` for details. + """ + _adjust_compatible = True + _colorbar_gridspec = True + + def __init__(self, *, pad=1.08, h_pad=None, w_pad=None, + rect=(0, 0, 1, 1), **kwargs): + """ + Initialize tight_layout engine. + + Parameters + ---------- + pad : float, 1.08 + Padding between the figure edge and the edges of subplots, as a + fraction of the font size. + h_pad, w_pad : float + Padding (height/width) between edges of adjacent subplots. + Defaults to *pad*. + rect : tuple[float, float, float, float], optional + (left, bottom, right, top) rectangle in normalized figure + coordinates that the subplots (including labels) + will fit into. Defaults to using the entire figure. + """ + super().__init__(**kwargs) + for td in ['pad', 'h_pad', 'w_pad', 'rect']: + # initialize these in case None is passed in above: + self._params[td] = None + self.set(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) + + def execute(self, fig): + """ + Execute tight_layout. + + This decides the subplot parameters given the padding that + will allow the axes labels to not be covered by other labels + and axes. + + Parameters + ---------- + fig : `.Figure` to perform layout on. + + See also: `.figure.Figure.tight_layout` and `.pyplot.tight_layout`. + """ + info = self._params + subplotspec_list = get_subplotspec_list(fig.axes) + if None in subplotspec_list: + _api.warn_external("This figure includes Axes that are not " + "compatible with tight_layout, so results " + "might be incorrect.") + renderer = _get_renderer(fig) + with getattr(renderer, "_draw_disabled", nullcontext)(): + kwargs = get_tight_layout_figure( + fig, fig.axes, subplotspec_list, renderer, + pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'], + rect=info['rect']) + if kwargs: + fig.subplots_adjust(**kwargs) + + def set(self, *, pad=None, w_pad=None, h_pad=None, rect=None): + for td in self.set.__kwdefaults__: + if locals()[td] is not None: + self._params[td] = locals()[td] + + +class ConstrainedLayoutEngine(LayoutEngine): + """ + Implements the ``constrained_layout`` geometry management. See + :doc:`/tutorials/intermediate/constrainedlayout_guide` for details. + """ + + _adjust_compatible = False + _colorbar_gridspec = False + + def __init__(self, *, h_pad=None, w_pad=None, + hspace=None, wspace=None, **kwargs): + """ + Initialize ``constrained_layout`` settings. + + Parameters + ---------- + h_pad, w_pad : float + Padding around the axes elements in figure-normalized units. + Default to :rc:`figure.constrained_layout.h_pad` and + :rc:`figure.constrained_layout.w_pad`. + hspace, wspace : float + Fraction of the figure to dedicate to space between the + axes. These are evenly spread between the gaps between the axes. + A value of 0.2 for a three-column layout would have a space + of 0.1 of the figure width between each column. + 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`. + """ + 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']) + # set anything that was passed in (None will be ignored): + self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace) + + def execute(self, fig): + """ + Perform constrained_layout and move and resize axes accordingly. + + Parameters + ---------- + fig : `.Figure` to perform layout on. + """ + width, height = fig.get_size_inches() + # pads are relative to the current state of the figure... + w_pad = self._params['w_pad'] / width + h_pad = self._params['h_pad'] / height + + return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad, + wspace=self._params['wspace'], + hspace=self._params['hspace']) + + def set(self, *, h_pad=None, w_pad=None, + hspace=None, wspace=None): + """ + Set the pads for constrained_layout. + + Parameters + ---------- + h_pad, w_pad : float + Padding around the axes elements in figure-normalized units. + Default to :rc:`figure.constrained_layout.h_pad` and + :rc:`figure.constrained_layout.w_pad`. + hspace, wspace : float + Fraction of the figure to dedicate to space between the + axes. These are evenly spread between the gaps between the axes. + A value of 0.2 for a three-column layout would have a space + of 0.1 of the figure width between each column. + 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`. + """ + for td in self.set.__kwdefaults__: + if locals()[td] is not None: + self._params[td] = locals()[td] diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 8fd3cc5a35a7..255102bc102a 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -1,3 +1,4 @@ +from matplotlib._api.deprecation import MatplotlibDeprecationWarning import numpy as np import pytest @@ -36,7 +37,7 @@ def example_pcolor(ax, fontsize=12): @image_comparison(['constrained_layout1.png']) def test_constrained_layout1(): """Test constrained_layout for a single subplot""" - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout="constrained") ax = fig.add_subplot() example_plot(ax, fontsize=24) @@ -44,7 +45,7 @@ def test_constrained_layout1(): @image_comparison(['constrained_layout2.png']) def test_constrained_layout2(): """Test constrained_layout for 2x2 subplots""" - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: example_plot(ax, fontsize=24) @@ -53,7 +54,7 @@ def test_constrained_layout2(): def test_constrained_layout3(): """Test constrained_layout for colorbars with subplots""" - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for nn, ax in enumerate(axs.flat): pcm = example_pcolor(ax, fontsize=24) if nn == 3: @@ -67,7 +68,7 @@ def test_constrained_layout3(): def test_constrained_layout4(): """Test constrained_layout for a single colorbar with subplots""" - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax, fontsize=24) fig.colorbar(pcm, ax=axs, pad=0.01, shrink=0.6) @@ -80,7 +81,7 @@ def test_constrained_layout5(): colorbar bottom """ - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax, fontsize=24) fig.colorbar(pcm, ax=axs, @@ -94,7 +95,7 @@ def test_constrained_layout6(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout="constrained") gs = fig.add_gridspec(1, 2, figure=fig) gsl = gs[0].subgridspec(2, 2) gsr = gs[1].subgridspec(1, 2) @@ -141,7 +142,7 @@ def test_constrained_layout7(): UserWarning, match=('There are no gridspecs with layoutgrids. ' 'Possibly did not call parent GridSpec with ' 'the "figure" keyword')): - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout="constrained") gs = gridspec.GridSpec(1, 2) gsl = gridspec.GridSpecFromSubplotSpec(2, 2, gs[0]) gsr = gridspec.GridSpecFromSubplotSpec(1, 2, gs[1]) @@ -155,7 +156,7 @@ def test_constrained_layout7(): def test_constrained_layout8(): """Test for gridspecs that are not completely full""" - fig = plt.figure(figsize=(10, 5), constrained_layout=True) + fig = plt.figure(figsize=(10, 5), layout="constrained") gs = gridspec.GridSpec(3, 5, figure=fig) axs = [] for j in [0, 1]: @@ -183,7 +184,7 @@ def test_constrained_layout8(): def test_constrained_layout9(): """Test for handling suptitle and for sharex and sharey""" - fig, axs = plt.subplots(2, 2, constrained_layout=True, + fig, axs = plt.subplots(2, 2, layout="constrained", sharex=False, sharey=False) for ax in axs.flat: pcm = example_pcolor(ax, fontsize=24) @@ -197,7 +198,7 @@ def test_constrained_layout9(): @image_comparison(['constrained_layout10.png']) def test_constrained_layout10(): """Test for handling legend outside axis""" - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: ax.plot(np.arange(12), label='This is a label') ax.legend(loc='center left', bbox_to_anchor=(0.8, 0.5)) @@ -207,7 +208,7 @@ def test_constrained_layout10(): def test_constrained_layout11(): """Test for multiple nested gridspecs""" - fig = plt.figure(constrained_layout=True, figsize=(13, 3)) + fig = plt.figure(layout="constrained", figsize=(13, 3)) gs0 = gridspec.GridSpec(1, 2, figure=fig) gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0]) gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1]) @@ -227,7 +228,7 @@ def test_constrained_layout11(): def test_constrained_layout11rat(): """Test for multiple nested gridspecs with width_ratios""" - fig = plt.figure(constrained_layout=True, figsize=(10, 3)) + fig = plt.figure(layout="constrained", figsize=(10, 3)) gs0 = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[6, 1]) gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0]) gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1], height_ratios=[2, 1]) @@ -246,7 +247,7 @@ def test_constrained_layout11rat(): @image_comparison(['constrained_layout12.png']) def test_constrained_layout12(): """Test that very unbalanced labeling still works.""" - fig = plt.figure(constrained_layout=True, figsize=(6, 8)) + fig = plt.figure(layout="constrained", figsize=(6, 8)) gs0 = gridspec.GridSpec(6, 2, figure=fig) @@ -268,23 +269,23 @@ def test_constrained_layout12(): @image_comparison(['constrained_layout13.png'], tol=2.e-2) def test_constrained_layout13(): """Test that padding works.""" - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax, fontsize=12) fig.colorbar(pcm, ax=ax, shrink=0.6, aspect=20., pad=0.02) - with pytest.raises(TypeError, match='unexpected keyword argument'): - fig.set_constrained_layout_pads(wpad=1, hpad=2) - fig.set_constrained_layout_pads(w_pad=24./72., h_pad=24./72.) + with pytest.raises(TypeError): + fig.get_layout_engine().set(wpad=1, hpad=2) + fig.get_layout_engine().set(w_pad=24./72., h_pad=24./72.) @image_comparison(['constrained_layout14.png']) def test_constrained_layout14(): """Test that padding works.""" - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax, fontsize=12) fig.colorbar(pcm, ax=ax, shrink=0.6, aspect=20., pad=0.02) - fig.set_constrained_layout_pads( + fig.get_layout_engine().set( w_pad=3./72., h_pad=3./72., hspace=0.2, wspace=0.2) @@ -301,7 +302,7 @@ def test_constrained_layout15(): @image_comparison(['constrained_layout16.png']) def test_constrained_layout16(): """Test ax.set_position.""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") example_plot(ax, fontsize=12) ax2 = fig.add_axes([0.2, 0.2, 0.4, 0.4]) @@ -309,7 +310,7 @@ def test_constrained_layout16(): @image_comparison(['constrained_layout17.png']) def test_constrained_layout17(): """Test uneven gridspecs""" - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout="constrained") gs = gridspec.GridSpec(3, 3, figure=fig) ax1 = fig.add_subplot(gs[0, 0]) @@ -325,7 +326,7 @@ def test_constrained_layout17(): def test_constrained_layout18(): """Test twinx""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") ax2 = ax.twinx() example_plot(ax) example_plot(ax2, fontsize=24) @@ -335,7 +336,7 @@ def test_constrained_layout18(): def test_constrained_layout19(): """Test twiny""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") ax2 = ax.twiny() example_plot(ax) example_plot(ax2, fontsize=24) @@ -358,7 +359,7 @@ def test_constrained_layout20(): def test_constrained_layout21(): """#11035: repeated calls to suptitle should not alter the layout""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") fig.suptitle("Suptitle0") fig.draw_without_rendering() @@ -373,7 +374,7 @@ def test_constrained_layout21(): def test_constrained_layout22(): """#11035: suptitle should not be include in CL if manually positioned""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") fig.draw_without_rendering() extents0 = np.copy(ax.get_position().extents) @@ -392,7 +393,7 @@ def test_constrained_layout23(): """ for i in range(2): - fig = plt.figure(constrained_layout=True, clear=True, num="123") + fig = plt.figure(layout="constrained", clear=True, num="123") gs = fig.add_gridspec(1, 2) sub = gs[0].subgridspec(2, 2) fig.suptitle("Suptitle{}".format(i)) @@ -408,7 +409,7 @@ def test_colorbar_location(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False - fig, axs = plt.subplots(4, 5, constrained_layout=True) + fig, axs = plt.subplots(4, 5, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax) ax.set_xlabel('') @@ -425,7 +426,7 @@ def test_hidden_axes(): # test that if we make an axes not visible that constrained_layout # still works. Note the axes still takes space in the layout # (as does a gridspec slot that is empty) - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") axs[0, 1].set_visible(False) fig.draw_without_rendering() extents1 = np.copy(axs[0, 0].get_position().extents) @@ -436,7 +437,7 @@ def test_hidden_axes(): def test_colorbar_align(): for location in ['right', 'left', 'top', 'bottom']: - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") cbs = [] for nn, ax in enumerate(axs.flat): ax.tick_params(direction='in') @@ -450,8 +451,8 @@ def test_colorbar_align(): cb.ax.yaxis.set_ticks([]) ax.set_xticklabels([]) ax.set_yticklabels([]) - fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.1, - wspace=0.1) + fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, + hspace=0.1, wspace=0.1) fig.draw_without_rendering() if location in ['left', 'right']: @@ -469,7 +470,7 @@ def test_colorbar_align(): @image_comparison(['test_colorbars_no_overlapV.png'], remove_text=False, style='mpl20') def test_colorbars_no_overlapV(): - fig = plt.figure(figsize=(2, 4), constrained_layout=True) + fig = plt.figure(figsize=(2, 4), layout="constrained") axs = fig.subplots(2, 1, sharex=True, sharey=True) for ax in axs: ax.yaxis.set_major_formatter(ticker.NullFormatter()) @@ -482,7 +483,7 @@ def test_colorbars_no_overlapV(): @image_comparison(['test_colorbars_no_overlapH.png'], remove_text=False, style='mpl20') def test_colorbars_no_overlapH(): - fig = plt.figure(figsize=(4, 2), constrained_layout=True) + fig = plt.figure(figsize=(4, 2), layout="constrained") fig.suptitle("foo") axs = fig.subplots(1, 2, sharex=True, sharey=True) for ax in axs: @@ -493,13 +494,13 @@ def test_colorbars_no_overlapH(): def test_manually_set_position(): - fig, axs = plt.subplots(1, 2, constrained_layout=True) + fig, axs = plt.subplots(1, 2, layout="constrained") axs[0].set_position([0.2, 0.2, 0.3, 0.3]) fig.draw_without_rendering() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.5, 0.5]]) - fig, axs = plt.subplots(1, 2, constrained_layout=True) + fig, axs = plt.subplots(1, 2, layout="constrained") axs[0].set_position([0.2, 0.2, 0.3, 0.3]) pc = axs[0].pcolormesh(np.random.rand(20, 20)) fig.colorbar(pc, ax=axs[0]) @@ -512,7 +513,7 @@ def test_manually_set_position(): remove_text=True, style='mpl20', savefig_kwarg={'bbox_inches': 'tight'}) def test_bboxtight(): - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") ax.set_aspect(1.) @@ -521,7 +522,7 @@ def test_bboxtight(): savefig_kwarg={'bbox_inches': mtransforms.Bbox([[0.5, 0], [2.5, 2]])}) def test_bbox(): - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") ax.set_aspect(1.) @@ -532,7 +533,7 @@ def test_align_labels(): negative numbers, drives the non-negative subplots' y labels off the edge of the plot """ - fig, (ax3, ax1, ax2) = plt.subplots(3, 1, constrained_layout=True, + fig, (ax3, ax1, ax2) = plt.subplots(3, 1, layout="constrained", figsize=(6.4, 8), gridspec_kw={"height_ratios": (1, 1, 0.7)}) @@ -560,7 +561,7 @@ def test_align_labels(): def test_suplabels(): - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") fig.draw_without_rendering() pos0 = ax.get_tightbbox(fig.canvas.get_renderer()) fig.supxlabel('Boo') @@ -570,7 +571,7 @@ def test_suplabels(): assert pos.y0 > pos0.y0 + 10.0 assert pos.x0 > pos0.x0 + 10.0 - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") fig.draw_without_rendering() pos0 = ax.get_tightbbox(fig.canvas.get_renderer()) # check that specifying x (y) doesn't ruin the layout @@ -587,3 +588,25 @@ def test_gridspec_addressing(): gs = fig.add_gridspec(3, 3) sp = fig.add_subplot(gs[0:, 1:]) fig.draw_without_rendering() + + +def test_discouraged_api(): + fig, ax = plt.subplots(constrained_layout=True) + fig.draw_without_rendering() + + with pytest.warns(MatplotlibDeprecationWarning, + match="was deprecated in Matplotlib 3.6"): + fig, ax = plt.subplots() + fig.set_constrained_layout(True) + fig.draw_without_rendering() + + with pytest.warns(MatplotlibDeprecationWarning, + match="was deprecated in Matplotlib 3.6"): + fig, ax = plt.subplots() + fig.set_constrained_layout({'w_pad': 0.02, 'h_pad': 0.02}) + fig.draw_without_rendering() + + +def test_kwargs(): + fig, ax = plt.subplots(constrained_layout={'h_pad': 0.02}) + fig.draw_without_rendering() diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 92c149cc002a..abc9f732fc50 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -16,6 +16,8 @@ from matplotlib.testing.decorators import image_comparison, check_figures_equal from matplotlib.axes import Axes from matplotlib.figure import Figure +from matplotlib.layout_engine import (ConstrainedLayoutEngine, + TightLayoutEngine) from matplotlib.ticker import AutoMinorLocator, FixedFormatter, ScalarFormatter import matplotlib.pyplot as plt import matplotlib.dates as mdates @@ -25,7 +27,7 @@ @image_comparison(['figure_align_labels'], extensions=['png', 'svg'], tol=0 if platform.machine() == 'x86_64' else 0.01) def test_align_labels(): - fig = plt.figure(tight_layout=True) + fig = plt.figure(layout='tight') gs = gridspec.GridSpec(3, 3) ax = fig.add_subplot(gs[0, :2]) @@ -575,29 +577,48 @@ def test_valid_layouts(): def test_invalid_layouts(): - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") with pytest.warns(UserWarning): # this should warn, fig.subplots_adjust(top=0.8) - assert not(fig.get_constrained_layout()) + assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine) # Using layout + (tight|constrained)_layout warns, but the former takes # precedence. - with pytest.warns(UserWarning, match="Figure parameters 'layout' and " - "'tight_layout' cannot"): + wst = "The Figure parameters 'layout' and 'tight_layout'" + with pytest.warns(UserWarning, match=wst): fig = Figure(layout='tight', tight_layout=False) - assert fig.get_tight_layout() - assert not fig.get_constrained_layout() - with pytest.warns(UserWarning, match="Figure parameters 'layout' and " - "'constrained_layout' cannot"): + assert isinstance(fig.get_layout_engine(), TightLayoutEngine) + wst = "The Figure parameters 'layout' and 'constrained_layout'" + with pytest.warns(UserWarning, match=wst): fig = Figure(layout='constrained', constrained_layout=False) - assert not fig.get_tight_layout() - assert fig.get_constrained_layout() + assert not isinstance(fig.get_layout_engine(), TightLayoutEngine) + assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine) with pytest.raises(ValueError, - match="'foobar' is not a valid value for layout"): + match="Invalid value for 'layout'"): Figure(layout='foobar') + # test that layouts can be swapped if no colorbar: + fig, ax = plt.subplots(layout="constrained") + fig.set_layout_engine("tight") + assert isinstance(fig.get_layout_engine(), TightLayoutEngine) + fig.set_layout_engine("constrained") + assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine) + + # test that layouts cannot be swapped if there is a colorbar: + fig, ax = plt.subplots(layout="constrained") + pc = ax.pcolormesh(np.random.randn(2, 2)) + fig.colorbar(pc) + with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): + fig.set_layout_engine("tight") + + fig, ax = plt.subplots(layout="tight") + pc = ax.pcolormesh(np.random.randn(2, 2)) + fig.colorbar(pc) + with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): + fig.set_layout_engine("constrained") + @check_figures_equal(extensions=["png", "pdf"]) def test_add_artist(fig_test, fig_ref): @@ -775,8 +796,8 @@ def test_all_nested(self, fig_test, fig_ref): x = [["A", "B"], ["C", "D"]] y = [["E", "F"], ["G", "H"]] - fig_ref.set_constrained_layout(True) - fig_test.set_constrained_layout(True) + fig_ref.set_layout_engine("constrained") + fig_test.set_layout_engine("constrained") grid_axes = fig_test.subplot_mosaic([[x, y]]) for ax in grid_axes.values(): @@ -796,8 +817,8 @@ def test_all_nested(self, fig_test, fig_ref): @check_figures_equal(extensions=["png"]) def test_nested(self, fig_test, fig_ref): - fig_ref.set_constrained_layout(True) - fig_test.set_constrained_layout(True) + fig_ref.set_layout_engine("constrained") + fig_test.set_layout_engine("constrained") x = [["A", "B"], ["C", "D"]] @@ -1005,7 +1026,7 @@ def test_reused_gridspec(): remove_text=False) def test_subfigure(): np.random.seed(19680801) - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout='constrained') sub = fig.subfigures(1, 2) axs = sub[0].subplots(2, 2) @@ -1025,7 +1046,7 @@ def test_subfigure(): def test_subfigure_tightbbox(): # test that we can get the tightbbox with a subfigure... - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout='constrained') sub = fig.subfigures(1, 2) np.testing.assert_allclose( @@ -1039,7 +1060,7 @@ def test_subfigure_tightbbox(): def test_subfigure_ss(): # test assigning the subfigure via subplotspec np.random.seed(19680801) - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout='constrained') gs = fig.add_gridspec(1, 2) sub = fig.add_subfigure(gs[0], facecolor='pink') @@ -1064,7 +1085,7 @@ def test_subfigure_double(): # test assigning the subfigure via subplotspec np.random.seed(19680801) - fig = plt.figure(constrained_layout=True, figsize=(10, 8)) + fig = plt.figure(layout='constrained', figsize=(10, 8)) fig.suptitle('fig') diff --git a/lib/matplotlib/tests/test_tightlayout.py b/lib/matplotlib/tests/test_tightlayout.py index 43ebd535be2b..b976d00546ec 100644 --- a/lib/matplotlib/tests/test_tightlayout.py +++ b/lib/matplotlib/tests/test_tightlayout.py @@ -1,4 +1,5 @@ import warnings +from matplotlib._api.deprecation import MatplotlibDeprecationWarning import numpy as np from numpy.testing import assert_array_equal @@ -134,9 +135,10 @@ def test_tight_layout7(): def test_tight_layout8(): """Test automatic use of tight_layout.""" fig = plt.figure() - fig.set_tight_layout({'pad': .1}) + fig.set_layout_engine(layout='tight', pad=0.1) ax = fig.add_subplot() example_plot(ax, fontsize=24) + fig.draw_without_rendering() @image_comparison(['tight_layout9']) @@ -366,3 +368,16 @@ def test_clipped_to_axes(): m.set_clip_path(rect.get_path(), rect.get_transform()) assert not h._fully_clipped_to_axes() assert not m._fully_clipped_to_axes() + + +def test_tight_pads(): + fig, ax = plt.subplots() + with pytest.warns(MatplotlibDeprecationWarning, + match='was deprecated in Matplotlib 3.6'): + fig.set_tight_layout({'pad': 0.15}) + fig.draw_without_rendering() + + +def test_tight_kwargs(): + fig, ax = plt.subplots(tight_layout={'pad': 0.15}) + fig.draw_without_rendering() diff --git a/tutorials/intermediate/constrainedlayout_guide.py b/tutorials/intermediate/constrainedlayout_guide.py index f01c28342b37..44def35d8d77 100644 --- a/tutorials/intermediate/constrainedlayout_guide.py +++ b/tutorials/intermediate/constrainedlayout_guide.py @@ -14,13 +14,13 @@ but uses a constraint solver to determine the size of axes that allows them to fit. -*constrained_layout* needs to be activated before any axes are added to -a figure. Two ways of doing so are +*constrained_layout* typically needs to be activated before any axes are +added to a figure. Two ways of doing so are * using the respective argument to :func:`~.pyplot.subplots` or :func:`~.pyplot.figure`, e.g.:: - plt.subplots(constrained_layout=True) + plt.subplots(layout="constrained") * activate it via :ref:`rcParams`, like:: @@ -63,32 +63,32 @@ def example_plot(ax, fontsize=12, hide_labels=False): ax.set_ylabel('y-label', fontsize=fontsize) ax.set_title('Title', fontsize=fontsize) -fig, ax = plt.subplots(constrained_layout=False) +fig, ax = plt.subplots(layout=None) example_plot(ax, fontsize=24) ############################################################################### # To prevent this, the location of axes needs to be adjusted. For # subplots, this can be done manually by adjusting the subplot parameters # using `.Figure.subplots_adjust`. However, specifying your figure with the -# # ``constrained_layout=True`` keyword argument will do the adjusting +# # ``layout="constrained"`` keyword argument will do the adjusting # # automatically. -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") example_plot(ax, fontsize=24) ############################################################################### # When you have multiple subplots, often you see labels of different # axes overlapping each other. -fig, axs = plt.subplots(2, 2, constrained_layout=False) +fig, axs = plt.subplots(2, 2, layout=None) for ax in axs.flat: example_plot(ax) ############################################################################### -# Specifying ``constrained_layout=True`` in the call to ``plt.subplots`` +# Specifying ``layout="constrained"`` in the call to ``plt.subplots`` # causes the layout to be properly constrained. -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: example_plot(ax) @@ -113,7 +113,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): norm = mcolors.Normalize(vmin=0., vmax=100.) # see note above: this makes all pcolormesh calls consistent: pc_kwargs = {'rasterized': True, 'cmap': 'viridis', 'norm': norm} -fig, ax = plt.subplots(figsize=(4, 4), constrained_layout=True) +fig, ax = plt.subplots(figsize=(4, 4), layout="constrained") im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=ax, shrink=0.6) @@ -122,7 +122,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # ``ax`` argument of ``colorbar``, constrained_layout will take space from # the specified axes. -fig, axs = plt.subplots(2, 2, figsize=(4, 4), constrained_layout=True) +fig, axs = plt.subplots(2, 2, figsize=(4, 4), layout="constrained") for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs, shrink=0.6) @@ -132,7 +132,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # will steal space appropriately, and leave a gap, but all subplots will # still be the same size. -fig, axs = plt.subplots(3, 3, figsize=(4, 4), constrained_layout=True) +fig, axs = plt.subplots(3, 3, figsize=(4, 4), layout="constrained") for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs[1:, ][:, 1], shrink=0.8) @@ -144,7 +144,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # # ``constrained_layout`` can also make room for `~.Figure.suptitle`. -fig, axs = plt.subplots(2, 2, figsize=(4, 4), constrained_layout=True) +fig, axs = plt.subplots(2, 2, figsize=(4, 4), layout="constrained") for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs, shrink=0.6) @@ -159,14 +159,14 @@ def example_plot(ax, fontsize=12, hide_labels=False): # However, constrained-layout does *not* handle legends being created via # :meth:`.Figure.legend` (yet). -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") ax.plot(np.arange(10), label='This is a plot') ax.legend(loc='center left', bbox_to_anchor=(0.8, 0.5)) ############################################# # However, this will steal space from a subplot layout: -fig, axs = plt.subplots(1, 2, figsize=(4, 2), constrained_layout=True) +fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained") axs[0].plot(np.arange(10)) axs[1].plot(np.arange(10), label='This is a plot') axs[1].legend(loc='center left', bbox_to_anchor=(0.8, 0.5)) @@ -182,7 +182,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # trigger a draw if we want constrained_layout to adjust the size # of the axes before printing. -fig, axs = plt.subplots(1, 2, figsize=(4, 2), constrained_layout=True) +fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained") axs[0].plot(np.arange(10)) axs[1].plot(np.arange(10), label='This is a plot') @@ -194,7 +194,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # we want the legend included in the bbox_inches='tight' calcs. leg.set_in_layout(True) # we don't want the layout to change at this point. -fig.set_constrained_layout(False) +fig.set_layout_engine(None) fig.savefig('../../doc/_static/constrained_layout_1b.png', bbox_inches='tight', dpi=100) @@ -206,7 +206,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # # A better way to get around this awkwardness is to simply # use the legend method provided by `.Figure.legend`: -fig, axs = plt.subplots(1, 2, figsize=(4, 2), constrained_layout=True) +fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained") axs[0].plot(np.arange(10)) lines = axs[1].plot(np.arange(10), label='This is a plot') labels = [l.get_label() for l in lines] @@ -228,13 +228,14 @@ def example_plot(ax, fontsize=12, hide_labels=False): # # Padding between axes is controlled in the horizontal by *w_pad* and # *wspace*, and vertical by *h_pad* and *hspace*. These can be edited -# via `~.Figure.set_constrained_layout_pads`. *w/h_pad* are +# via `~.layout_engine.ConstrainedLayoutEngine.set`. *w/h_pad* are # the minimum space around the axes in units of inches: -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: example_plot(ax, hide_labels=True) -fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0, wspace=0) +fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0, + wspace=0) ########################################## # Spacing between subplots is further set by *wspace* and *hspace*. These @@ -243,35 +244,35 @@ def example_plot(ax, fontsize=12, hide_labels=False): # used instead. Note in the below how the space at the edges doesn't change # from the above, but the space between subplots does. -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: example_plot(ax, hide_labels=True) -fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2, - wspace=0.2) +fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2, + wspace=0.2) ########################################## # If there are more than two columns, the *wspace* is shared between them, # so here the wspace is divided in 2, with a *wspace* of 0.1 between each # column: -fig, axs = plt.subplots(2, 3, constrained_layout=True) +fig, axs = plt.subplots(2, 3, layout="constrained") for ax in axs.flat: example_plot(ax, hide_labels=True) -fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2, - wspace=0.2) +fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2, + wspace=0.2) ########################################## # GridSpecs also have optional *hspace* and *wspace* keyword arguments, # that will be used instead of the pads set by ``constrained_layout``: -fig, axs = plt.subplots(2, 2, constrained_layout=True, +fig, axs = plt.subplots(2, 2, layout="constrained", gridspec_kw={'wspace': 0.3, 'hspace': 0.2}) for ax in axs.flat: example_plot(ax, hide_labels=True) # this has no effect because the space set in the gridspec trumps the # space set in constrained_layout. -fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.0, - wspace=0.0) +fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.0, + wspace=0.0) plt.show() ########################################## @@ -282,7 +283,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # is a fraction of the width of the parent(s). The spacing to the # next subplot is then given by *w/hspace*. -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") pads = [0, 0.05, 0.1, 0.2] for pad, ax in zip(pads, axs.flat): pc = ax.pcolormesh(arr, **pc_kwargs) @@ -290,8 +291,8 @@ def example_plot(ax, fontsize=12, hide_labels=False): ax.set_xticklabels([]) ax.set_yticklabels([]) ax.set_title(f'pad: {pad}') -fig.set_constrained_layout_pads(w_pad=2 / 72, h_pad=2 / 72, hspace=0.2, - wspace=0.2) +fig.get_layout_engine().set(w_pad=2 / 72, h_pad=2 / 72, hspace=0.2, + wspace=0.2) ########################################## # rcParams @@ -322,7 +323,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # :func:`~matplotlib.gridspec.GridSpec` and # :func:`~matplotlib.figure.Figure.add_subplot`. # -# Note that in what follows ``constrained_layout=True`` +# Note that in what follows ``layout="constrained"`` fig = plt.figure() @@ -436,7 +437,7 @@ def docomplicated(suptitle=None): # ``constrained_layout`` usually adjusts the axes positions on each draw # of the figure. If you want to get the spacing provided by # ``constrained_layout`` but not have it update, then do the initial -# draw and then call ``fig.set_constrained_layout(False)``. +# draw and then call ``fig.set_layout_engine(None)``. # This is potentially useful for animations where the tick labels may # change length. # @@ -579,7 +580,7 @@ def docomplicated(suptitle=None): from matplotlib._layoutgrid import plot_children -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") example_plot(ax, fontsize=24) plot_children(fig) @@ -593,7 +594,7 @@ def docomplicated(suptitle=None): # margin. The left and right margins are not shared, and hence are # allowed to be different. -fig, ax = plt.subplots(1, 2, constrained_layout=True) +fig, ax = plt.subplots(1, 2, layout="constrained") example_plot(ax[0], fontsize=32) example_plot(ax[1], fontsize=8) plot_children(fig, printit=False) @@ -605,7 +606,7 @@ def docomplicated(suptitle=None): # A colorbar is simply another item that expands the margin of the parent # layoutgrid cell: -fig, ax = plt.subplots(1, 2, constrained_layout=True) +fig, ax = plt.subplots(1, 2, layout="constrained") im = ax[0].pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=ax[0], shrink=0.6) im = ax[1].pcolormesh(arr, **pc_kwargs) @@ -618,7 +619,7 @@ def docomplicated(suptitle=None): # If a colorbar belongs to more than one cell of the grid, then # it makes a larger margin for each: -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs, shrink=0.6) @@ -639,7 +640,7 @@ def docomplicated(suptitle=None): # of the left-hand axes. This is consietent with how ``gridspec`` works # without constrained layout. -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") gs = gridspec.GridSpec(2, 2, figure=fig) ax = fig.add_subplot(gs[:, 0]) im = ax.pcolormesh(arr, **pc_kwargs) @@ -656,7 +657,7 @@ def docomplicated(suptitle=None): # so we take the maximum width of the margin widths that do have artists. # This makes all the axes have the same size: -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") gs = fig.add_gridspec(2, 4) ax00 = fig.add_subplot(gs[0, 0:2]) ax01 = fig.add_subplot(gs[0, 2:]) From de0060220534db26dae19b39406aa0e165b2403c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 13 Jan 2022 16:47:01 -0500 Subject: [PATCH 2/2] FIX: typo --- lib/matplotlib/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index a4b52e7c89bf..a18cd9fd0abd 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2594,7 +2594,7 @@ def get_constrained_layout_pads(self, relative=False): hspace = info['hspace'] if relative and (w_pad is not None or h_pad is not None): - renderer = _get_renderer(self).dpi + renderer = _get_renderer(self) dpi = renderer.dpi w_pad = w_pad * dpi / renderer.width h_pad = h_pad * dpi / renderer.height 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