layout_engine.py 11 KB

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