1"""
2Routines to adjust subplot params so that subplots are
3nicely fit in the figure. In doing so, only axis labels, tick labels, Axes
4titles and offsetboxes that are anchored to Axes are currently considered.
5
6Internally, this module assumes that the margins (left margin, etc.) which are
7differences between ``Axes.get_tightbbox`` and ``Axes.bbox`` are independent of
8Axes position. This may fail if ``Axes.adjustable`` is ``datalim`` as well as
9such cases as when left or right margin are affected by xlabel.
10"""
11
12import numpy as np
13
14import matplotlib as mpl
15from matplotlib import _api, artist as martist
16from matplotlib.font_manager import FontProperties
17from matplotlib.transforms import Bbox
18
19
20def _auto_adjust_subplotpars(
21 fig, renderer, shape, span_pairs, subplot_list,
22 ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None):
23 """
24 Return a dict of subplot parameters to adjust spacing between subplots
25 or ``None`` if resulting Axes would have zero height or width.
26
27 Note that this function ignores geometry information of subplot itself, but
28 uses what is given by the *shape* and *subplot_list* parameters. Also, the
29 results could be incorrect if some subplots have ``adjustable=datalim``.
30
31 Parameters
32 ----------
33 shape : tuple[int, int]
34 Number of rows and columns of the grid.
35 span_pairs : list[tuple[slice, slice]]
36 List of rowspans and colspans occupied by each subplot.
37 subplot_list : list of subplots
38 List of subplots that will be used to calculate optimal subplot_params.
39 pad : float
40 Padding between the figure edge and the edges of subplots, as a
41 fraction of the font size.
42 h_pad, w_pad : float
43 Padding (height/width) between edges of adjacent subplots, as a
44 fraction of the font size. Defaults to *pad*.
45 rect : tuple
46 (left, bottom, right, top), default: None.
47 """
48 rows, cols = shape
49
50 font_size_inch = (FontProperties(
51 size=mpl.rcParams["font.size"]).get_size_in_points() / 72)
52 pad_inch = pad * font_size_inch
53 vpad_inch = h_pad * font_size_inch if h_pad is not None else pad_inch
54 hpad_inch = w_pad * font_size_inch if w_pad is not None else pad_inch
55
56 if len(span_pairs) != len(subplot_list) or len(subplot_list) == 0:
57 raise ValueError
58
59 if rect is None:
60 margin_left = margin_bottom = margin_right = margin_top = None
61 else:
62 margin_left, margin_bottom, _right, _top = rect
63 margin_right = 1 - _right if _right else None
64 margin_top = 1 - _top if _top else None
65
66 vspaces = np.zeros((rows + 1, cols))
67 hspaces = np.zeros((rows, cols + 1))
68
69 if ax_bbox_list is None:
70 ax_bbox_list = [
71 Bbox.union([ax.get_position(original=True) for ax in subplots])
72 for subplots in subplot_list]
73
74 for subplots, ax_bbox, (rowspan, colspan) in zip(
75 subplot_list, ax_bbox_list, span_pairs):
76 if all(not ax.get_visible() for ax in subplots):
77 continue
78
79 bb = []
80 for ax in subplots:
81 if ax.get_visible():
82 bb += [martist._get_tightbbox_for_layout_only(ax, renderer)]
83
84 tight_bbox_raw = Bbox.union(bb)
85 tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw)
86
87 hspaces[rowspan, colspan.start] += ax_bbox.xmin - tight_bbox.xmin # l
88 hspaces[rowspan, colspan.stop] += tight_bbox.xmax - ax_bbox.xmax # r
89 vspaces[rowspan.start, colspan] += tight_bbox.ymax - ax_bbox.ymax # t
90 vspaces[rowspan.stop, colspan] += ax_bbox.ymin - tight_bbox.ymin # b
91
92 fig_width_inch, fig_height_inch = fig.get_size_inches()
93
94 # margins can be negative for Axes with aspect applied, so use max(, 0) to
95 # make them nonnegative.
96 if not margin_left:
97 margin_left = max(hspaces[:, 0].max(), 0) + pad_inch/fig_width_inch
98 suplabel = fig._supylabel
99 if suplabel and suplabel.get_in_layout():
100 rel_width = fig.transFigure.inverted().transform_bbox(
101 suplabel.get_window_extent(renderer)).width
102 margin_left += rel_width + pad_inch/fig_width_inch
103 if not margin_right:
104 margin_right = max(hspaces[:, -1].max(), 0) + pad_inch/fig_width_inch
105 if not margin_top:
106 margin_top = max(vspaces[0, :].max(), 0) + pad_inch/fig_height_inch
107 if fig._suptitle and fig._suptitle.get_in_layout():
108 rel_height = fig.transFigure.inverted().transform_bbox(
109 fig._suptitle.get_window_extent(renderer)).height
110 margin_top += rel_height + pad_inch/fig_height_inch
111 if not margin_bottom:
112 margin_bottom = max(vspaces[-1, :].max(), 0) + pad_inch/fig_height_inch
113 suplabel = fig._supxlabel
114 if suplabel and suplabel.get_in_layout():
115 rel_height = fig.transFigure.inverted().transform_bbox(
116 suplabel.get_window_extent(renderer)).height
117 margin_bottom += rel_height + pad_inch/fig_height_inch
118
119 if margin_left + margin_right >= 1:
120 _api.warn_external('Tight layout not applied. The left and right '
121 'margins cannot be made large enough to '
122 'accommodate all Axes decorations.')
123 return None
124 if margin_bottom + margin_top >= 1:
125 _api.warn_external('Tight layout not applied. The bottom and top '
126 'margins cannot be made large enough to '
127 'accommodate all Axes decorations.')
128 return None
129
130 kwargs = dict(left=margin_left,
131 right=1 - margin_right,
132 bottom=margin_bottom,
133 top=1 - margin_top)
134
135 if cols > 1:
136 hspace = hspaces[:, 1:-1].max() + hpad_inch / fig_width_inch
137 # axes widths:
138 h_axes = (1 - margin_right - margin_left - hspace * (cols - 1)) / cols
139 if h_axes < 0:
140 _api.warn_external('Tight layout not applied. tight_layout '
141 'cannot make Axes width small enough to '
142 'accommodate all Axes decorations')
143 return None
144 else:
145 kwargs["wspace"] = hspace / h_axes
146 if rows > 1:
147 vspace = vspaces[1:-1, :].max() + vpad_inch / fig_height_inch
148 v_axes = (1 - margin_top - margin_bottom - vspace * (rows - 1)) / rows
149 if v_axes < 0:
150 _api.warn_external('Tight layout not applied. tight_layout '
151 'cannot make Axes height small enough to '
152 'accommodate all Axes decorations.')
153 return None
154 else:
155 kwargs["hspace"] = vspace / v_axes
156
157 return kwargs
158
159
160def get_subplotspec_list(axes_list, grid_spec=None):
161 """
162 Return a list of subplotspec from the given list of Axes.
163
164 For an instance of Axes that does not support subplotspec, None is inserted
165 in the list.
166
167 If grid_spec is given, None is inserted for those not from the given
168 grid_spec.
169 """
170 subplotspec_list = []
171 for ax in axes_list:
172 axes_or_locator = ax.get_axes_locator()
173 if axes_or_locator is None:
174 axes_or_locator = ax
175
176 if hasattr(axes_or_locator, "get_subplotspec"):
177 subplotspec = axes_or_locator.get_subplotspec()
178 if subplotspec is not None:
179 subplotspec = subplotspec.get_topmost_subplotspec()
180 gs = subplotspec.get_gridspec()
181 if grid_spec is not None:
182 if gs != grid_spec:
183 subplotspec = None
184 elif gs.locally_modified_subplot_params():
185 subplotspec = None
186 else:
187 subplotspec = None
188
189 subplotspec_list.append(subplotspec)
190
191 return subplotspec_list
192
193
194def get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer,
195 pad=1.08, h_pad=None, w_pad=None, rect=None):
196 """
197 Return subplot parameters for tight-layouted-figure with specified padding.
198
199 Parameters
200 ----------
201 fig : Figure
202 axes_list : list of Axes
203 subplotspec_list : list of `.SubplotSpec`
204 The subplotspecs of each Axes.
205 renderer : renderer
206 pad : float
207 Padding between the figure edge and the edges of subplots, as a
208 fraction of the font size.
209 h_pad, w_pad : float
210 Padding (height/width) between edges of adjacent subplots. Defaults to
211 *pad*.
212 rect : tuple (left, bottom, right, top), default: None.
213 rectangle in normalized figure coordinates
214 that the whole subplots area (including labels) will fit into.
215 Defaults to using the entire figure.
216
217 Returns
218 -------
219 subplotspec or None
220 subplotspec kwargs to be passed to `.Figure.subplots_adjust` or
221 None if tight_layout could not be accomplished.
222 """
223
224 # Multiple Axes can share same subplotspec (e.g., if using axes_grid1);
225 # we need to group them together.
226 ss_to_subplots = {ss: [] for ss in subplotspec_list}
227 for ax, ss in zip(axes_list, subplotspec_list):
228 ss_to_subplots[ss].append(ax)
229 if ss_to_subplots.pop(None, None):
230 _api.warn_external(
231 "This figure includes Axes that are not compatible with "
232 "tight_layout, so results might be incorrect.")
233 if not ss_to_subplots:
234 return {}
235 subplot_list = list(ss_to_subplots.values())
236 ax_bbox_list = [ss.get_position(fig) for ss in ss_to_subplots]
237
238 max_nrows = max(ss.get_gridspec().nrows for ss in ss_to_subplots)
239 max_ncols = max(ss.get_gridspec().ncols for ss in ss_to_subplots)
240
241 span_pairs = []
242 for ss in ss_to_subplots:
243 # The intent here is to support Axes from different gridspecs where
244 # one's nrows (or ncols) is a multiple of the other (e.g. 2 and 4),
245 # but this doesn't actually work because the computed wspace, in
246 # relative-axes-height, corresponds to different physical spacings for
247 # the 2-row grid and the 4-row grid. Still, this code is left, mostly
248 # for backcompat.
249 rows, cols = ss.get_gridspec().get_geometry()
250 div_row, mod_row = divmod(max_nrows, rows)
251 div_col, mod_col = divmod(max_ncols, cols)
252 if mod_row != 0:
253 _api.warn_external('tight_layout not applied: number of rows '
254 'in subplot specifications must be '
255 'multiples of one another.')
256 return {}
257 if mod_col != 0:
258 _api.warn_external('tight_layout not applied: number of '
259 'columns in subplot specifications must be '
260 'multiples of one another.')
261 return {}
262 span_pairs.append((
263 slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row),
264 slice(ss.colspan.start * div_col, ss.colspan.stop * div_col)))
265
266 kwargs = _auto_adjust_subplotpars(fig, renderer,
267 shape=(max_nrows, max_ncols),
268 span_pairs=span_pairs,
269 subplot_list=subplot_list,
270 ax_bbox_list=ax_bbox_list,
271 pad=pad, h_pad=h_pad, w_pad=w_pad)
272
273 # kwargs can be none if tight_layout fails...
274 if rect is not None and kwargs is not None:
275 # if rect is given, the whole subplots area (including
276 # labels) will fit into the rect instead of the
277 # figure. Note that the rect argument of
278 # *auto_adjust_subplotpars* specify the area that will be
279 # covered by the total area of axes.bbox. Thus we call
280 # auto_adjust_subplotpars twice, where the second run
281 # with adjusted rect parameters.
282
283 left, bottom, right, top = rect
284 if left is not None:
285 left += kwargs["left"]
286 if bottom is not None:
287 bottom += kwargs["bottom"]
288 if right is not None:
289 right -= (1 - kwargs["right"])
290 if top is not None:
291 top -= (1 - kwargs["top"])
292
293 kwargs = _auto_adjust_subplotpars(fig, renderer,
294 shape=(max_nrows, max_ncols),
295 span_pairs=span_pairs,
296 subplot_list=subplot_list,
297 ax_bbox_list=ax_bbox_list,
298 pad=pad, h_pad=h_pad, w_pad=w_pad,
299 rect=(left, bottom, right, top))
300
301 return kwargs