layout_engine.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. """
  2. Classes to layout elements in a `.Figure`.
  3. Figures have a ``layout_engine`` property that holds a subclass of
  4. `~.LayoutEngine` defined here (or *None* for no layout). At draw time
  5. ``figure.get_layout_engine().execute()`` is called, the goal of which is
  6. usually to rearrange Axes on the figure to produce a pleasing layout. This is
  7. like a ``draw`` callback but with two differences. First, when printing we
  8. disable the layout engine for the final draw. Second, it is useful to know the
  9. layout engine while the figure is being created. In particular, colorbars are
  10. made differently with different layout engines (for historical reasons).
  11. Matplotlib has two built-in layout engines:
  12. - `.TightLayoutEngine` was the first layout engine added to Matplotlib.
  13. See also :ref:`tight_layout_guide`.
  14. - `.ConstrainedLayoutEngine` is more modern and generally gives better results.
  15. See also :ref:`constrainedlayout_guide`.
  16. Third parties can create their own layout engine by subclassing `.LayoutEngine`.
  17. """
  18. from contextlib import nullcontext
  19. import matplotlib as mpl
  20. from matplotlib._constrained_layout import do_constrained_layout
  21. from matplotlib._tight_layout import (get_subplotspec_list,
  22. get_tight_layout_figure)
  23. class LayoutEngine:
  24. """
  25. Base class for Matplotlib layout engines.
  26. A layout engine can be passed to a figure at instantiation or at any time
  27. with `~.figure.Figure.set_layout_engine`. Once attached to a figure, the
  28. layout engine ``execute`` function is called at draw time by
  29. `~.figure.Figure.draw`, providing a special draw-time hook.
  30. .. note::
  31. However, note that layout engines affect the creation of colorbars, so
  32. `~.figure.Figure.set_layout_engine` should be called before any
  33. colorbars are created.
  34. Currently, there are two properties of `LayoutEngine` classes that are
  35. consulted while manipulating the figure:
  36. - ``engine.colorbar_gridspec`` tells `.Figure.colorbar` whether to make the
  37. axes using the gridspec method (see `.colorbar.make_axes_gridspec`) or
  38. not (see `.colorbar.make_axes`);
  39. - ``engine.adjust_compatible`` stops `.Figure.subplots_adjust` from being
  40. run if it is not compatible with the layout engine.
  41. To implement a custom `LayoutEngine`:
  42. 1. override ``_adjust_compatible`` and ``_colorbar_gridspec``
  43. 2. override `LayoutEngine.set` to update *self._params*
  44. 3. override `LayoutEngine.execute` with your implementation
  45. """
  46. # override these in subclass
  47. _adjust_compatible = None
  48. _colorbar_gridspec = None
  49. def __init__(self, **kwargs):
  50. super().__init__(**kwargs)
  51. self._params = {}
  52. def set(self, **kwargs):
  53. """
  54. Set the parameters for the layout engine.
  55. """
  56. raise NotImplementedError
  57. @property
  58. def colorbar_gridspec(self):
  59. """
  60. Return a boolean if the layout engine creates colorbars using a
  61. gridspec.
  62. """
  63. if self._colorbar_gridspec is None:
  64. raise NotImplementedError
  65. return self._colorbar_gridspec
  66. @property
  67. def adjust_compatible(self):
  68. """
  69. Return a boolean if the layout engine is compatible with
  70. `~.Figure.subplots_adjust`.
  71. """
  72. if self._adjust_compatible is None:
  73. raise NotImplementedError
  74. return self._adjust_compatible
  75. def get(self):
  76. """
  77. Return copy of the parameters for the layout engine.
  78. """
  79. return dict(self._params)
  80. def execute(self, fig):
  81. """
  82. Execute the layout on the figure given by *fig*.
  83. """
  84. # subclasses must implement this.
  85. raise NotImplementedError
  86. class PlaceHolderLayoutEngine(LayoutEngine):
  87. """
  88. This layout engine does not adjust the figure layout at all.
  89. The purpose of this `.LayoutEngine` is to act as a placeholder when the user removes
  90. a layout engine to ensure an incompatible `.LayoutEngine` cannot be set later.
  91. Parameters
  92. ----------
  93. adjust_compatible, colorbar_gridspec : bool
  94. Allow the PlaceHolderLayoutEngine to mirror the behavior of whatever
  95. layout engine it is replacing.
  96. """
  97. def __init__(self, adjust_compatible, colorbar_gridspec, **kwargs):
  98. self._adjust_compatible = adjust_compatible
  99. self._colorbar_gridspec = colorbar_gridspec
  100. super().__init__(**kwargs)
  101. def execute(self, fig):
  102. """
  103. Do nothing.
  104. """
  105. return
  106. class TightLayoutEngine(LayoutEngine):
  107. """
  108. Implements the ``tight_layout`` geometry management. See
  109. :ref:`tight_layout_guide` for details.
  110. """
  111. _adjust_compatible = True
  112. _colorbar_gridspec = True
  113. def __init__(self, *, pad=1.08, h_pad=None, w_pad=None,
  114. rect=(0, 0, 1, 1), **kwargs):
  115. """
  116. Initialize tight_layout engine.
  117. Parameters
  118. ----------
  119. pad : float, default: 1.08
  120. Padding between the figure edge and the edges of subplots, as a
  121. fraction of the font size.
  122. h_pad, w_pad : float
  123. Padding (height/width) between edges of adjacent subplots.
  124. Defaults to *pad*.
  125. rect : tuple (left, bottom, right, top), default: (0, 0, 1, 1).
  126. rectangle in normalized figure coordinates that the subplots
  127. (including labels) will fit into.
  128. """
  129. super().__init__(**kwargs)
  130. for td in ['pad', 'h_pad', 'w_pad', 'rect']:
  131. # initialize these in case None is passed in above:
  132. self._params[td] = None
  133. self.set(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
  134. def execute(self, fig):
  135. """
  136. Execute tight_layout.
  137. This decides the subplot parameters given the padding that
  138. will allow the Axes labels to not be covered by other labels
  139. and Axes.
  140. Parameters
  141. ----------
  142. fig : `.Figure` to perform layout on.
  143. See Also
  144. --------
  145. .figure.Figure.tight_layout
  146. .pyplot.tight_layout
  147. """
  148. info = self._params
  149. renderer = fig._get_renderer()
  150. with getattr(renderer, "_draw_disabled", nullcontext)():
  151. kwargs = get_tight_layout_figure(
  152. fig, fig.axes, get_subplotspec_list(fig.axes), renderer,
  153. pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'],
  154. rect=info['rect'])
  155. if kwargs:
  156. fig.subplots_adjust(**kwargs)
  157. def set(self, *, pad=None, w_pad=None, h_pad=None, rect=None):
  158. """
  159. Set the pads for tight_layout.
  160. Parameters
  161. ----------
  162. pad : float
  163. Padding between the figure edge and the edges of subplots, as a
  164. fraction of the font size.
  165. w_pad, h_pad : float
  166. Padding (width/height) between edges of adjacent subplots.
  167. Defaults to *pad*.
  168. rect : tuple (left, bottom, right, top)
  169. rectangle in normalized figure coordinates that the subplots
  170. (including labels) will fit into.
  171. """
  172. for td in self.set.__kwdefaults__:
  173. if locals()[td] is not None:
  174. self._params[td] = locals()[td]
  175. class ConstrainedLayoutEngine(LayoutEngine):
  176. """
  177. Implements the ``constrained_layout`` geometry management. See
  178. :ref:`constrainedlayout_guide` for details.
  179. """
  180. _adjust_compatible = False
  181. _colorbar_gridspec = False
  182. def __init__(self, *, h_pad=None, w_pad=None,
  183. hspace=None, wspace=None, rect=(0, 0, 1, 1),
  184. compress=False, **kwargs):
  185. """
  186. Initialize ``constrained_layout`` settings.
  187. Parameters
  188. ----------
  189. h_pad, w_pad : float
  190. Padding around the Axes elements in inches.
  191. Default to :rc:`figure.constrained_layout.h_pad` and
  192. :rc:`figure.constrained_layout.w_pad`.
  193. hspace, wspace : float
  194. Fraction of the figure to dedicate to space between the
  195. axes. These are evenly spread between the gaps between the Axes.
  196. A value of 0.2 for a three-column layout would have a space
  197. of 0.1 of the figure width between each column.
  198. If h/wspace < h/w_pad, then the pads are used instead.
  199. Default to :rc:`figure.constrained_layout.hspace` and
  200. :rc:`figure.constrained_layout.wspace`.
  201. rect : tuple of 4 floats
  202. Rectangle in figure coordinates to perform constrained layout in
  203. (left, bottom, width, height), each from 0-1.
  204. compress : bool
  205. Whether to shift Axes so that white space in between them is
  206. removed. This is useful for simple grids of fixed-aspect Axes (e.g.
  207. a grid of images). See :ref:`compressed_layout`.
  208. """
  209. super().__init__(**kwargs)
  210. # set the defaults:
  211. self.set(w_pad=mpl.rcParams['figure.constrained_layout.w_pad'],
  212. h_pad=mpl.rcParams['figure.constrained_layout.h_pad'],
  213. wspace=mpl.rcParams['figure.constrained_layout.wspace'],
  214. hspace=mpl.rcParams['figure.constrained_layout.hspace'],
  215. rect=(0, 0, 1, 1))
  216. # set anything that was passed in (None will be ignored):
  217. self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace,
  218. rect=rect)
  219. self._compress = compress
  220. def execute(self, fig):
  221. """
  222. Perform constrained_layout and move and resize Axes accordingly.
  223. Parameters
  224. ----------
  225. fig : `.Figure` to perform layout on.
  226. """
  227. width, height = fig.get_size_inches()
  228. # pads are relative to the current state of the figure...
  229. w_pad = self._params['w_pad'] / width
  230. h_pad = self._params['h_pad'] / height
  231. return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
  232. wspace=self._params['wspace'],
  233. hspace=self._params['hspace'],
  234. rect=self._params['rect'],
  235. compress=self._compress)
  236. def set(self, *, h_pad=None, w_pad=None,
  237. hspace=None, wspace=None, rect=None):
  238. """
  239. Set the pads for constrained_layout.
  240. Parameters
  241. ----------
  242. h_pad, w_pad : float
  243. Padding around the Axes elements in inches.
  244. Default to :rc:`figure.constrained_layout.h_pad` and
  245. :rc:`figure.constrained_layout.w_pad`.
  246. hspace, wspace : float
  247. Fraction of the figure to dedicate to space between the
  248. axes. These are evenly spread between the gaps between the Axes.
  249. A value of 0.2 for a three-column layout would have a space
  250. of 0.1 of the figure width between each column.
  251. If h/wspace < h/w_pad, then the pads are used instead.
  252. Default to :rc:`figure.constrained_layout.hspace` and
  253. :rc:`figure.constrained_layout.wspace`.
  254. rect : tuple of 4 floats
  255. Rectangle in figure coordinates to perform constrained layout in
  256. (left, bottom, width, height), each from 0-1.
  257. """
  258. for td in self.set.__kwdefaults__:
  259. if locals()[td] is not None:
  260. self._params[td] = locals()[td]