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..a18cd9fd0abd 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,10 +2585,13 @@ 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) @@ -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:])