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