1"""
2Adjust subplot layouts so that there are no overlapping Axes or Axes
3decorations. All Axes decorations are dealt with (labels, ticks, titles,
4ticklabels) and some dependent artists are also dealt with (colorbar,
5suptitle).
6
7Layout is done via `~matplotlib.gridspec`, with one constraint per gridspec,
8so it is possible to have overlapping Axes if the gridspecs overlap (i.e.
9using `~matplotlib.gridspec.GridSpecFromSubplotSpec`). Axes placed using
10``figure.subplots()`` or ``figure.add_subplots()`` will participate in the
11layout. Axes manually placed via ``figure.add_axes()`` will not.
12
13See Tutorial: :ref:`constrainedlayout_guide`
14
15General idea:
16-------------
17
18First, a figure has a gridspec that divides the figure into nrows and ncols,
19with heights and widths set by ``height_ratios`` and ``width_ratios``,
20often just set to 1 for an equal grid.
21
22Subplotspecs that are derived from this gridspec can contain either a
23``SubPanel``, a ``GridSpecFromSubplotSpec``, or an ``Axes``. The ``SubPanel``
24and ``GridSpecFromSubplotSpec`` are dealt with recursively and each contain an
25analogous layout.
26
27Each ``GridSpec`` has a ``_layoutgrid`` attached to it. The ``_layoutgrid``
28has the same logical layout as the ``GridSpec``. Each row of the grid spec
29has a top and bottom "margin" and each column has a left and right "margin".
30The "inner" height of each row is constrained to be the same (or as modified
31by ``height_ratio``), and the "inner" width of each column is
32constrained to be the same (as modified by ``width_ratio``), where "inner"
33is the width or height of each column/row minus the size of the margins.
34
35Then the size of the margins for each row and column are determined as the
36max width of the decorators on each Axes that has decorators in that margin.
37For instance, a normal Axes would have a left margin that includes the
38left ticklabels, and the ylabel if it exists. The right margin may include a
39colorbar, the bottom margin the xaxis decorations, and the top margin the
40title.
41
42With these constraints, the solver then finds appropriate bounds for the
43columns and rows. It's possible that the margins take up the whole figure,
44in which case the algorithm is not applied and a warning is raised.
45
46See the tutorial :ref:`constrainedlayout_guide`
47for more discussion of the algorithm with examples.
48"""
49
50import logging
51
52import numpy as np
53
54from matplotlib import _api, artist as martist
55import matplotlib.transforms as mtransforms
56import matplotlib._layoutgrid as mlayoutgrid
57
58
59_log = logging.getLogger(__name__)
60
61
62######################################################
63def do_constrained_layout(fig, h_pad, w_pad,
64 hspace=None, wspace=None, rect=(0, 0, 1, 1),
65 compress=False):
66 """
67 Do the constrained_layout. Called at draw time in
68 ``figure.constrained_layout()``
69
70 Parameters
71 ----------
72 fig : `~matplotlib.figure.Figure`
73 `.Figure` instance to do the layout in.
74
75 h_pad, w_pad : float
76 Padding around the Axes elements in figure-normalized units.
77
78 hspace, wspace : float
79 Fraction of the figure to dedicate to space between the
80 Axes. These are evenly spread between the gaps between the Axes.
81 A value of 0.2 for a three-column layout would have a space
82 of 0.1 of the figure width between each column.
83 If h/wspace < h/w_pad, then the pads are used instead.
84
85 rect : tuple of 4 floats
86 Rectangle in figure coordinates to perform constrained layout in
87 [left, bottom, width, height], each from 0-1.
88
89 compress : bool
90 Whether to shift Axes so that white space in between them is
91 removed. This is useful for simple grids of fixed-aspect Axes (e.g.
92 a grid of images).
93
94 Returns
95 -------
96 layoutgrid : private debugging structure
97 """
98
99 renderer = fig._get_renderer()
100 # make layoutgrid tree...
101 layoutgrids = make_layoutgrids(fig, None, rect=rect)
102 if not layoutgrids['hasgrids']:
103 _api.warn_external('There are no gridspecs with layoutgrids. '
104 'Possibly did not call parent GridSpec with the'
105 ' "figure" keyword')
106 return
107
108 for _ in range(2):
109 # do the algorithm twice. This has to be done because decorations
110 # change size after the first re-position (i.e. x/yticklabels get
111 # larger/smaller). This second reposition tends to be much milder,
112 # so doing twice makes things work OK.
113
114 # make margins for all the Axes and subfigures in the
115 # figure. Add margins for colorbars...
116 make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
117 w_pad=w_pad, hspace=hspace, wspace=wspace)
118 make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
119 w_pad=w_pad)
120
121 # if a layout is such that a columns (or rows) margin has no
122 # constraints, we need to make all such instances in the grid
123 # match in margin size.
124 match_submerged_margins(layoutgrids, fig)
125
126 # update all the variables in the layout.
127 layoutgrids[fig].update_variables()
128
129 warn_collapsed = ('constrained_layout not applied because '
130 'axes sizes collapsed to zero. Try making '
131 'figure larger or Axes decorations smaller.')
132 if check_no_collapsed_axes(layoutgrids, fig):
133 reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
134 w_pad=w_pad, hspace=hspace, wspace=wspace)
135 if compress:
136 layoutgrids = compress_fixed_aspect(layoutgrids, fig)
137 layoutgrids[fig].update_variables()
138 if check_no_collapsed_axes(layoutgrids, fig):
139 reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
140 w_pad=w_pad, hspace=hspace, wspace=wspace)
141 else:
142 _api.warn_external(warn_collapsed)
143 else:
144 _api.warn_external(warn_collapsed)
145 reset_margins(layoutgrids, fig)
146 return layoutgrids
147
148
149def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)):
150 """
151 Make the layoutgrid tree.
152
153 (Sub)Figures get a layoutgrid so we can have figure margins.
154
155 Gridspecs that are attached to Axes get a layoutgrid so Axes
156 can have margins.
157 """
158
159 if layoutgrids is None:
160 layoutgrids = dict()
161 layoutgrids['hasgrids'] = False
162 if not hasattr(fig, '_parent'):
163 # top figure; pass rect as parent to allow user-specified
164 # margins
165 layoutgrids[fig] = mlayoutgrid.LayoutGrid(parent=rect, name='figlb')
166 else:
167 # subfigure
168 gs = fig._subplotspec.get_gridspec()
169 # it is possible the gridspec containing this subfigure hasn't
170 # been added to the tree yet:
171 layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
172 # add the layoutgrid for the subfigure:
173 parentlb = layoutgrids[gs]
174 layoutgrids[fig] = mlayoutgrid.LayoutGrid(
175 parent=parentlb,
176 name='panellb',
177 parent_inner=True,
178 nrows=1, ncols=1,
179 parent_pos=(fig._subplotspec.rowspan,
180 fig._subplotspec.colspan))
181 # recursively do all subfigures in this figure...
182 for sfig in fig.subfigs:
183 layoutgrids = make_layoutgrids(sfig, layoutgrids)
184
185 # for each Axes at the local level add its gridspec:
186 for ax in fig._localaxes:
187 gs = ax.get_gridspec()
188 if gs is not None:
189 layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
190
191 return layoutgrids
192
193
194def make_layoutgrids_gs(layoutgrids, gs):
195 """
196 Make the layoutgrid for a gridspec (and anything nested in the gridspec)
197 """
198
199 if gs in layoutgrids or gs.figure is None:
200 return layoutgrids
201 # in order to do constrained_layout there has to be at least *one*
202 # gridspec in the tree:
203 layoutgrids['hasgrids'] = True
204 if not hasattr(gs, '_subplot_spec'):
205 # normal gridspec
206 parent = layoutgrids[gs.figure]
207 layoutgrids[gs] = mlayoutgrid.LayoutGrid(
208 parent=parent,
209 parent_inner=True,
210 name='gridspec',
211 ncols=gs._ncols, nrows=gs._nrows,
212 width_ratios=gs.get_width_ratios(),
213 height_ratios=gs.get_height_ratios())
214 else:
215 # this is a gridspecfromsubplotspec:
216 subplot_spec = gs._subplot_spec
217 parentgs = subplot_spec.get_gridspec()
218 # if a nested gridspec it is possible the parent is not in there yet:
219 if parentgs not in layoutgrids:
220 layoutgrids = make_layoutgrids_gs(layoutgrids, parentgs)
221 subspeclb = layoutgrids[parentgs]
222 # gridspecfromsubplotspec need an outer container:
223 # get a unique representation:
224 rep = (gs, 'top')
225 if rep not in layoutgrids:
226 layoutgrids[rep] = mlayoutgrid.LayoutGrid(
227 parent=subspeclb,
228 name='top',
229 nrows=1, ncols=1,
230 parent_pos=(subplot_spec.rowspan, subplot_spec.colspan))
231 layoutgrids[gs] = mlayoutgrid.LayoutGrid(
232 parent=layoutgrids[rep],
233 name='gridspec',
234 nrows=gs._nrows, ncols=gs._ncols,
235 width_ratios=gs.get_width_ratios(),
236 height_ratios=gs.get_height_ratios())
237 return layoutgrids
238
239
240def check_no_collapsed_axes(layoutgrids, fig):
241 """
242 Check that no Axes have collapsed to zero size.
243 """
244 for sfig in fig.subfigs:
245 ok = check_no_collapsed_axes(layoutgrids, sfig)
246 if not ok:
247 return False
248 for ax in fig.axes:
249 gs = ax.get_gridspec()
250 if gs in layoutgrids: # also implies gs is not None.
251 lg = layoutgrids[gs]
252 for i in range(gs.nrows):
253 for j in range(gs.ncols):
254 bb = lg.get_inner_bbox(i, j)
255 if bb.width <= 0 or bb.height <= 0:
256 return False
257 return True
258
259
260def compress_fixed_aspect(layoutgrids, fig):
261 gs = None
262 for ax in fig.axes:
263 if ax.get_subplotspec() is None:
264 continue
265 ax.apply_aspect()
266 sub = ax.get_subplotspec()
267 _gs = sub.get_gridspec()
268 if gs is None:
269 gs = _gs
270 extraw = np.zeros(gs.ncols)
271 extrah = np.zeros(gs.nrows)
272 elif _gs != gs:
273 raise ValueError('Cannot do compressed layout if Axes are not'
274 'all from the same gridspec')
275 orig = ax.get_position(original=True)
276 actual = ax.get_position(original=False)
277 dw = orig.width - actual.width
278 if dw > 0:
279 extraw[sub.colspan] = np.maximum(extraw[sub.colspan], dw)
280 dh = orig.height - actual.height
281 if dh > 0:
282 extrah[sub.rowspan] = np.maximum(extrah[sub.rowspan], dh)
283
284 if gs is None:
285 raise ValueError('Cannot do compressed layout if no Axes '
286 'are part of a gridspec.')
287 w = np.sum(extraw) / 2
288 layoutgrids[fig].edit_margin_min('left', w)
289 layoutgrids[fig].edit_margin_min('right', w)
290
291 h = np.sum(extrah) / 2
292 layoutgrids[fig].edit_margin_min('top', h)
293 layoutgrids[fig].edit_margin_min('bottom', h)
294 return layoutgrids
295
296
297def get_margin_from_padding(obj, *, w_pad=0, h_pad=0,
298 hspace=0, wspace=0):
299
300 ss = obj._subplotspec
301 gs = ss.get_gridspec()
302
303 if hasattr(gs, 'hspace'):
304 _hspace = (gs.hspace if gs.hspace is not None else hspace)
305 _wspace = (gs.wspace if gs.wspace is not None else wspace)
306 else:
307 _hspace = (gs._hspace if gs._hspace is not None else hspace)
308 _wspace = (gs._wspace if gs._wspace is not None else wspace)
309
310 _wspace = _wspace / 2
311 _hspace = _hspace / 2
312
313 nrows, ncols = gs.get_geometry()
314 # there are two margins for each direction. The "cb"
315 # margins are for pads and colorbars, the non-"cb" are
316 # for the Axes decorations (labels etc).
317 margin = {'leftcb': w_pad, 'rightcb': w_pad,
318 'bottomcb': h_pad, 'topcb': h_pad,
319 'left': 0, 'right': 0,
320 'top': 0, 'bottom': 0}
321 if _wspace / ncols > w_pad:
322 if ss.colspan.start > 0:
323 margin['leftcb'] = _wspace / ncols
324 if ss.colspan.stop < ncols:
325 margin['rightcb'] = _wspace / ncols
326 if _hspace / nrows > h_pad:
327 if ss.rowspan.stop < nrows:
328 margin['bottomcb'] = _hspace / nrows
329 if ss.rowspan.start > 0:
330 margin['topcb'] = _hspace / nrows
331
332 return margin
333
334
335def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0,
336 hspace=0, wspace=0):
337 """
338 For each Axes, make a margin between the *pos* layoutbox and the
339 *axes* layoutbox be a minimum size that can accommodate the
340 decorations on the axis.
341
342 Then make room for colorbars.
343
344 Parameters
345 ----------
346 layoutgrids : dict
347 fig : `~matplotlib.figure.Figure`
348 `.Figure` instance to do the layout in.
349 renderer : `~matplotlib.backend_bases.RendererBase` subclass.
350 The renderer to use.
351 w_pad, h_pad : float, default: 0
352 Width and height padding (in fraction of figure).
353 hspace, wspace : float, default: 0
354 Width and height padding as fraction of figure size divided by
355 number of columns or rows.
356 """
357 for sfig in fig.subfigs: # recursively make child panel margins
358 ss = sfig._subplotspec
359 gs = ss.get_gridspec()
360
361 make_layout_margins(layoutgrids, sfig, renderer,
362 w_pad=w_pad, h_pad=h_pad,
363 hspace=hspace, wspace=wspace)
364
365 margins = get_margin_from_padding(sfig, w_pad=0, h_pad=0,
366 hspace=hspace, wspace=wspace)
367 layoutgrids[gs].edit_outer_margin_mins(margins, ss)
368
369 for ax in fig._localaxes:
370 if not ax.get_subplotspec() or not ax.get_in_layout():
371 continue
372
373 ss = ax.get_subplotspec()
374 gs = ss.get_gridspec()
375
376 if gs not in layoutgrids:
377 return
378
379 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
380 hspace=hspace, wspace=wspace)
381 pos, bbox = get_pos_and_bbox(ax, renderer)
382 # the margin is the distance between the bounding box of the Axes
383 # and its position (plus the padding from above)
384 margin['left'] += pos.x0 - bbox.x0
385 margin['right'] += bbox.x1 - pos.x1
386 # remember that rows are ordered from top:
387 margin['bottom'] += pos.y0 - bbox.y0
388 margin['top'] += bbox.y1 - pos.y1
389
390 # make margin for colorbars. These margins go in the
391 # padding margin, versus the margin for Axes decorators.
392 for cbax in ax._colorbars:
393 # note pad is a fraction of the parent width...
394 pad = colorbar_get_pad(layoutgrids, cbax)
395 # colorbars can be child of more than one subplot spec:
396 cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
397 loc = cbax._colorbar_info['location']
398 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
399 if loc == 'right':
400 if cbp_cspan.stop == ss.colspan.stop:
401 # only increase if the colorbar is on the right edge
402 margin['rightcb'] += cbbbox.width + pad
403 elif loc == 'left':
404 if cbp_cspan.start == ss.colspan.start:
405 # only increase if the colorbar is on the left edge
406 margin['leftcb'] += cbbbox.width + pad
407 elif loc == 'top':
408 if cbp_rspan.start == ss.rowspan.start:
409 margin['topcb'] += cbbbox.height + pad
410 else:
411 if cbp_rspan.stop == ss.rowspan.stop:
412 margin['bottomcb'] += cbbbox.height + pad
413 # If the colorbars are wider than the parent box in the
414 # cross direction
415 if loc in ['top', 'bottom']:
416 if (cbp_cspan.start == ss.colspan.start and
417 cbbbox.x0 < bbox.x0):
418 margin['left'] += bbox.x0 - cbbbox.x0
419 if (cbp_cspan.stop == ss.colspan.stop and
420 cbbbox.x1 > bbox.x1):
421 margin['right'] += cbbbox.x1 - bbox.x1
422 # or taller:
423 if loc in ['left', 'right']:
424 if (cbp_rspan.stop == ss.rowspan.stop and
425 cbbbox.y0 < bbox.y0):
426 margin['bottom'] += bbox.y0 - cbbbox.y0
427 if (cbp_rspan.start == ss.rowspan.start and
428 cbbbox.y1 > bbox.y1):
429 margin['top'] += cbbbox.y1 - bbox.y1
430 # pass the new margins down to the layout grid for the solution...
431 layoutgrids[gs].edit_outer_margin_mins(margin, ss)
432
433 # make margins for figure-level legends:
434 for leg in fig.legends:
435 inv_trans_fig = None
436 if leg._outside_loc and leg._bbox_to_anchor is None:
437 if inv_trans_fig is None:
438 inv_trans_fig = fig.transFigure.inverted().transform_bbox
439 bbox = inv_trans_fig(leg.get_tightbbox(renderer))
440 w = bbox.width + 2 * w_pad
441 h = bbox.height + 2 * h_pad
442 legendloc = leg._outside_loc
443 if legendloc == 'lower':
444 layoutgrids[fig].edit_margin_min('bottom', h)
445 elif legendloc == 'upper':
446 layoutgrids[fig].edit_margin_min('top', h)
447 if legendloc == 'right':
448 layoutgrids[fig].edit_margin_min('right', w)
449 elif legendloc == 'left':
450 layoutgrids[fig].edit_margin_min('left', w)
451
452
453def make_margin_suptitles(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0):
454 # Figure out how large the suptitle is and make the
455 # top level figure margin larger.
456
457 inv_trans_fig = fig.transFigure.inverted().transform_bbox
458 # get the h_pad and w_pad as distances in the local subfigure coordinates:
459 padbox = mtransforms.Bbox([[0, 0], [w_pad, h_pad]])
460 padbox = (fig.transFigure -
461 fig.transSubfigure).transform_bbox(padbox)
462 h_pad_local = padbox.height
463 w_pad_local = padbox.width
464
465 for sfig in fig.subfigs:
466 make_margin_suptitles(layoutgrids, sfig, renderer,
467 w_pad=w_pad, h_pad=h_pad)
468
469 if fig._suptitle is not None and fig._suptitle.get_in_layout():
470 p = fig._suptitle.get_position()
471 if getattr(fig._suptitle, '_autopos', False):
472 fig._suptitle.set_position((p[0], 1 - h_pad_local))
473 bbox = inv_trans_fig(fig._suptitle.get_tightbbox(renderer))
474 layoutgrids[fig].edit_margin_min('top', bbox.height + 2 * h_pad)
475
476 if fig._supxlabel is not None and fig._supxlabel.get_in_layout():
477 p = fig._supxlabel.get_position()
478 if getattr(fig._supxlabel, '_autopos', False):
479 fig._supxlabel.set_position((p[0], h_pad_local))
480 bbox = inv_trans_fig(fig._supxlabel.get_tightbbox(renderer))
481 layoutgrids[fig].edit_margin_min('bottom',
482 bbox.height + 2 * h_pad)
483
484 if fig._supylabel is not None and fig._supylabel.get_in_layout():
485 p = fig._supylabel.get_position()
486 if getattr(fig._supylabel, '_autopos', False):
487 fig._supylabel.set_position((w_pad_local, p[1]))
488 bbox = inv_trans_fig(fig._supylabel.get_tightbbox(renderer))
489 layoutgrids[fig].edit_margin_min('left', bbox.width + 2 * w_pad)
490
491
492def match_submerged_margins(layoutgrids, fig):
493 """
494 Make the margins that are submerged inside an Axes the same size.
495
496 This allows Axes that span two columns (or rows) that are offset
497 from one another to have the same size.
498
499 This gives the proper layout for something like::
500 fig = plt.figure(constrained_layout=True)
501 axs = fig.subplot_mosaic("AAAB\nCCDD")
502
503 Without this routine, the Axes D will be wider than C, because the
504 margin width between the two columns in C has no width by default,
505 whereas the margins between the two columns of D are set by the
506 width of the margin between A and B. However, obviously the user would
507 like C and D to be the same size, so we need to add constraints to these
508 "submerged" margins.
509
510 This routine makes all the interior margins the same, and the spacing
511 between the three columns in A and the two column in C are all set to the
512 margins between the two columns of D.
513
514 See test_constrained_layout::test_constrained_layout12 for an example.
515 """
516
517 for sfig in fig.subfigs:
518 match_submerged_margins(layoutgrids, sfig)
519
520 axs = [a for a in fig.get_axes()
521 if a.get_subplotspec() is not None and a.get_in_layout()]
522
523 for ax1 in axs:
524 ss1 = ax1.get_subplotspec()
525 if ss1.get_gridspec() not in layoutgrids:
526 axs.remove(ax1)
527 continue
528 lg1 = layoutgrids[ss1.get_gridspec()]
529
530 # interior columns:
531 if len(ss1.colspan) > 1:
532 maxsubl = np.max(
533 lg1.margin_vals['left'][ss1.colspan[1:]] +
534 lg1.margin_vals['leftcb'][ss1.colspan[1:]]
535 )
536 maxsubr = np.max(
537 lg1.margin_vals['right'][ss1.colspan[:-1]] +
538 lg1.margin_vals['rightcb'][ss1.colspan[:-1]]
539 )
540 for ax2 in axs:
541 ss2 = ax2.get_subplotspec()
542 lg2 = layoutgrids[ss2.get_gridspec()]
543 if lg2 is not None and len(ss2.colspan) > 1:
544 maxsubl2 = np.max(
545 lg2.margin_vals['left'][ss2.colspan[1:]] +
546 lg2.margin_vals['leftcb'][ss2.colspan[1:]])
547 if maxsubl2 > maxsubl:
548 maxsubl = maxsubl2
549 maxsubr2 = np.max(
550 lg2.margin_vals['right'][ss2.colspan[:-1]] +
551 lg2.margin_vals['rightcb'][ss2.colspan[:-1]])
552 if maxsubr2 > maxsubr:
553 maxsubr = maxsubr2
554 for i in ss1.colspan[1:]:
555 lg1.edit_margin_min('left', maxsubl, cell=i)
556 for i in ss1.colspan[:-1]:
557 lg1.edit_margin_min('right', maxsubr, cell=i)
558
559 # interior rows:
560 if len(ss1.rowspan) > 1:
561 maxsubt = np.max(
562 lg1.margin_vals['top'][ss1.rowspan[1:]] +
563 lg1.margin_vals['topcb'][ss1.rowspan[1:]]
564 )
565 maxsubb = np.max(
566 lg1.margin_vals['bottom'][ss1.rowspan[:-1]] +
567 lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]]
568 )
569
570 for ax2 in axs:
571 ss2 = ax2.get_subplotspec()
572 lg2 = layoutgrids[ss2.get_gridspec()]
573 if lg2 is not None:
574 if len(ss2.rowspan) > 1:
575 maxsubt = np.max([np.max(
576 lg2.margin_vals['top'][ss2.rowspan[1:]] +
577 lg2.margin_vals['topcb'][ss2.rowspan[1:]]
578 ), maxsubt])
579 maxsubb = np.max([np.max(
580 lg2.margin_vals['bottom'][ss2.rowspan[:-1]] +
581 lg2.margin_vals['bottomcb'][ss2.rowspan[:-1]]
582 ), maxsubb])
583 for i in ss1.rowspan[1:]:
584 lg1.edit_margin_min('top', maxsubt, cell=i)
585 for i in ss1.rowspan[:-1]:
586 lg1.edit_margin_min('bottom', maxsubb, cell=i)
587
588
589def get_cb_parent_spans(cbax):
590 """
591 Figure out which subplotspecs this colorbar belongs to.
592
593 Parameters
594 ----------
595 cbax : `~matplotlib.axes.Axes`
596 Axes for the colorbar.
597 """
598 rowstart = np.inf
599 rowstop = -np.inf
600 colstart = np.inf
601 colstop = -np.inf
602 for parent in cbax._colorbar_info['parents']:
603 ss = parent.get_subplotspec()
604 rowstart = min(ss.rowspan.start, rowstart)
605 rowstop = max(ss.rowspan.stop, rowstop)
606 colstart = min(ss.colspan.start, colstart)
607 colstop = max(ss.colspan.stop, colstop)
608
609 rowspan = range(rowstart, rowstop)
610 colspan = range(colstart, colstop)
611 return rowspan, colspan
612
613
614def get_pos_and_bbox(ax, renderer):
615 """
616 Get the position and the bbox for the Axes.
617
618 Parameters
619 ----------
620 ax : `~matplotlib.axes.Axes`
621 renderer : `~matplotlib.backend_bases.RendererBase` subclass.
622
623 Returns
624 -------
625 pos : `~matplotlib.transforms.Bbox`
626 Position in figure coordinates.
627 bbox : `~matplotlib.transforms.Bbox`
628 Tight bounding box in figure coordinates.
629 """
630 fig = ax.figure
631 pos = ax.get_position(original=True)
632 # pos is in panel co-ords, but we need in figure for the layout
633 pos = pos.transformed(fig.transSubfigure - fig.transFigure)
634 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
635 if tightbbox is None:
636 bbox = pos
637 else:
638 bbox = tightbbox.transformed(fig.transFigure.inverted())
639 return pos, bbox
640
641
642def reposition_axes(layoutgrids, fig, renderer, *,
643 w_pad=0, h_pad=0, hspace=0, wspace=0):
644 """
645 Reposition all the Axes based on the new inner bounding box.
646 """
647 trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
648 for sfig in fig.subfigs:
649 bbox = layoutgrids[sfig].get_outer_bbox()
650 sfig._redo_transform_rel_fig(
651 bbox=bbox.transformed(trans_fig_to_subfig))
652 reposition_axes(layoutgrids, sfig, renderer,
653 w_pad=w_pad, h_pad=h_pad,
654 wspace=wspace, hspace=hspace)
655
656 for ax in fig._localaxes:
657 if ax.get_subplotspec() is None or not ax.get_in_layout():
658 continue
659
660 # grid bbox is in Figure coordinates, but we specify in panel
661 # coordinates...
662 ss = ax.get_subplotspec()
663 gs = ss.get_gridspec()
664 if gs not in layoutgrids:
665 return
666
667 bbox = layoutgrids[gs].get_inner_bbox(rows=ss.rowspan,
668 cols=ss.colspan)
669
670 # transform from figure to panel for set_position:
671 newbbox = trans_fig_to_subfig.transform_bbox(bbox)
672 ax._set_position(newbbox)
673
674 # move the colorbars:
675 # we need to keep track of oldw and oldh if there is more than
676 # one colorbar:
677 offset = {'left': 0, 'right': 0, 'bottom': 0, 'top': 0}
678 for nn, cbax in enumerate(ax._colorbars[::-1]):
679 if ax == cbax._colorbar_info['parents'][0]:
680 reposition_colorbar(layoutgrids, cbax, renderer,
681 offset=offset)
682
683
684def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None):
685 """
686 Place the colorbar in its new place.
687
688 Parameters
689 ----------
690 layoutgrids : dict
691 cbax : `~matplotlib.axes.Axes`
692 Axes for the colorbar.
693 renderer : `~matplotlib.backend_bases.RendererBase` subclass.
694 The renderer to use.
695 offset : array-like
696 Offset the colorbar needs to be pushed to in order to
697 account for multiple colorbars.
698 """
699
700 parents = cbax._colorbar_info['parents']
701 gs = parents[0].get_gridspec()
702 fig = cbax.figure
703 trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
704
705 cb_rspans, cb_cspans = get_cb_parent_spans(cbax)
706 bboxparent = layoutgrids[gs].get_bbox_for_cb(rows=cb_rspans,
707 cols=cb_cspans)
708 pb = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
709
710 location = cbax._colorbar_info['location']
711 anchor = cbax._colorbar_info['anchor']
712 fraction = cbax._colorbar_info['fraction']
713 aspect = cbax._colorbar_info['aspect']
714 shrink = cbax._colorbar_info['shrink']
715
716 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
717
718 # Colorbar gets put at extreme edge of outer bbox of the subplotspec
719 # It needs to be moved in by: 1) a pad 2) its "margin" 3) by
720 # any colorbars already added at this location:
721 cbpad = colorbar_get_pad(layoutgrids, cbax)
722 if location in ('left', 'right'):
723 # fraction and shrink are fractions of parent
724 pbcb = pb.shrunk(fraction, shrink).anchored(anchor, pb)
725 # The colorbar is at the left side of the parent. Need
726 # to translate to right (or left)
727 if location == 'right':
728 lmargin = cbpos.x0 - cbbbox.x0
729 dx = bboxparent.x1 - pbcb.x0 + offset['right']
730 dx += cbpad + lmargin
731 offset['right'] += cbbbox.width + cbpad
732 pbcb = pbcb.translated(dx, 0)
733 else:
734 lmargin = cbpos.x0 - cbbbox.x0
735 dx = bboxparent.x0 - pbcb.x0 # edge of parent
736 dx += -cbbbox.width - cbpad + lmargin - offset['left']
737 offset['left'] += cbbbox.width + cbpad
738 pbcb = pbcb.translated(dx, 0)
739 else: # horizontal axes:
740 pbcb = pb.shrunk(shrink, fraction).anchored(anchor, pb)
741 if location == 'top':
742 bmargin = cbpos.y0 - cbbbox.y0
743 dy = bboxparent.y1 - pbcb.y0 + offset['top']
744 dy += cbpad + bmargin
745 offset['top'] += cbbbox.height + cbpad
746 pbcb = pbcb.translated(0, dy)
747 else:
748 bmargin = cbpos.y0 - cbbbox.y0
749 dy = bboxparent.y0 - pbcb.y0
750 dy += -cbbbox.height - cbpad + bmargin - offset['bottom']
751 offset['bottom'] += cbbbox.height + cbpad
752 pbcb = pbcb.translated(0, dy)
753
754 pbcb = trans_fig_to_subfig.transform_bbox(pbcb)
755 cbax.set_transform(fig.transSubfigure)
756 cbax._set_position(pbcb)
757 cbax.set_anchor(anchor)
758 if location in ['bottom', 'top']:
759 aspect = 1 / aspect
760 cbax.set_box_aspect(aspect)
761 cbax.set_aspect('auto')
762 return offset
763
764
765def reset_margins(layoutgrids, fig):
766 """
767 Reset the margins in the layoutboxes of *fig*.
768
769 Margins are usually set as a minimum, so if the figure gets smaller
770 the minimum needs to be zero in order for it to grow again.
771 """
772 for sfig in fig.subfigs:
773 reset_margins(layoutgrids, sfig)
774 for ax in fig.axes:
775 if ax.get_in_layout():
776 gs = ax.get_gridspec()
777 if gs in layoutgrids: # also implies gs is not None.
778 layoutgrids[gs].reset_margins()
779 layoutgrids[fig].reset_margins()
780
781
782def colorbar_get_pad(layoutgrids, cax):
783 parents = cax._colorbar_info['parents']
784 gs = parents[0].get_gridspec()
785
786 cb_rspans, cb_cspans = get_cb_parent_spans(cax)
787 bboxouter = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
788
789 if cax._colorbar_info['location'] in ['right', 'left']:
790 size = bboxouter.width
791 else:
792 size = bboxouter.height
793
794 return cax._colorbar_info['pad'] * size