123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- """
- 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 but with two differences. First, when printing we
- disable the layout engine for the final draw. Second, it is useful to know the
- layout engine while the figure is being created. In particular, colorbars are
- made differently with different layout engines (for historical reasons).
- 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
- from matplotlib._constrained_layout import do_constrained_layout
- from matplotlib._tight_layout import (get_subplotspec_list,
- get_tight_layout_figure)
- 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 in subclass
- _adjust_compatible = None
- _colorbar_gridspec = None
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self._params = {}
- def set(self, **kwargs):
- """
- Set the parameters for the layout engine.
- """
- 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 implement this.
- raise NotImplementedError
- class PlaceHolderLayoutEngine(LayoutEngine):
- """
- This layout engine does not adjust the figure layout at all.
- The purpose of this `.LayoutEngine` is to act as a placeholder when the user removes
- a layout engine to ensure an incompatible `.LayoutEngine` cannot be set later.
- Parameters
- ----------
- adjust_compatible, colorbar_gridspec : bool
- Allow the PlaceHolderLayoutEngine to mirror the behavior of whatever
- layout engine it is replacing.
- """
- def __init__(self, adjust_compatible, colorbar_gridspec, **kwargs):
- self._adjust_compatible = adjust_compatible
- self._colorbar_gridspec = colorbar_gridspec
- super().__init__(**kwargs)
- def execute(self, fig):
- """
- Do nothing.
- """
- return
- class TightLayoutEngine(LayoutEngine):
- """
- Implements the ``tight_layout`` geometry management. See
- :ref:`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, default: 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 (left, bottom, right, top), default: (0, 0, 1, 1).
- rectangle in normalized figure coordinates that the subplots
- (including labels) will fit into.
- """
- 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
- .pyplot.tight_layout
- """
- info = self._params
- renderer = fig._get_renderer()
- with getattr(renderer, "_draw_disabled", nullcontext)():
- kwargs = get_tight_layout_figure(
- fig, fig.axes, get_subplotspec_list(fig.axes), 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):
- """
- Set the pads for tight_layout.
- Parameters
- ----------
- pad : float
- Padding between the figure edge and the edges of subplots, as a
- fraction of the font size.
- w_pad, h_pad : float
- Padding (width/height) between edges of adjacent subplots.
- Defaults to *pad*.
- rect : tuple (left, bottom, right, top)
- rectangle in normalized figure coordinates that the subplots
- (including labels) will fit into.
- """
- 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
- :ref:`constrainedlayout_guide` for details.
- """
- _adjust_compatible = False
- _colorbar_gridspec = False
- def __init__(self, *, h_pad=None, w_pad=None,
- hspace=None, wspace=None, rect=(0, 0, 1, 1),
- compress=False, **kwargs):
- """
- Initialize ``constrained_layout`` settings.
- Parameters
- ----------
- h_pad, w_pad : float
- Padding around the axes elements in inches.
- 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`.
- rect : tuple of 4 floats
- Rectangle in figure coordinates to perform constrained layout in
- (left, bottom, width, height), each from 0-1.
- compress : bool
- Whether to shift Axes so that white space in between them is
- removed. This is useful for simple grids of fixed-aspect Axes (e.g.
- a grid of images). See :ref:`compressed_layout`.
- """
- 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'],
- rect=(0, 0, 1, 1))
- # set anything that was passed in (None will be ignored):
- self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace,
- rect=rect)
- self._compress = compress
- 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'],
- rect=self._params['rect'],
- compress=self._compress)
- def set(self, *, h_pad=None, w_pad=None,
- hspace=None, wspace=None, rect=None):
- """
- Set the pads for constrained_layout.
- Parameters
- ----------
- h_pad, w_pad : float
- Padding around the axes elements in inches.
- 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`.
- rect : tuple of 4 floats
- Rectangle in figure coordinates to perform constrained layout in
- (left, bottom, width, height), each from 0-1.
- """
- for td in self.set.__kwdefaults__:
- if locals()[td] is not None:
- self._params[td] = locals()[td]
|