Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/_constrained_layout.py: 6%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

373 statements  

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