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

357 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

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: :doc:`/tutorials/intermediate/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 doc:`/tutorials/intermediate/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 : Figure 

73 ``Figure`` instance to do the layout in. 

74 

75 renderer : Renderer 

76 Renderer to use. 

77 

78 h_pad, w_pad : float 

79 Padding around the axes elements in figure-normalized units. 

80 

81 hspace, wspace : float 

82 Fraction of the figure to dedicate to space between the 

83 axes. These are evenly spread between the gaps between the axes. 

84 A value of 0.2 for a three-column layout would have a space 

85 of 0.1 of the figure width between each column. 

86 If h/wspace < h/w_pad, then the pads are used instead. 

87 

88 rect : tuple of 4 floats 

89 Rectangle in figure coordinates to perform constrained layout in 

90 [left, bottom, width, height], each from 0-1. 

91 

92 compress : bool 

93 Whether to shift Axes so that white space in between them is 

94 removed. This is useful for simple grids of fixed-aspect Axes (e.g. 

95 a grid of images). 

96 

97 Returns 

98 ------- 

99 layoutgrid : private debugging structure 

100 """ 

101 

102 renderer = fig._get_renderer() 

103 # make layoutgrid tree... 

104 layoutgrids = make_layoutgrids(fig, None, rect=rect) 

105 if not layoutgrids['hasgrids']: 

106 _api.warn_external('There are no gridspecs with layoutgrids. ' 

107 'Possibly did not call parent GridSpec with the' 

108 ' "figure" keyword') 

109 return 

110 

111 for _ in range(2): 

112 # do the algorithm twice. This has to be done because decorations 

113 # change size after the first re-position (i.e. x/yticklabels get 

114 # larger/smaller). This second reposition tends to be much milder, 

115 # so doing twice makes things work OK. 

116 

117 # make margins for all the axes and subfigures in the 

118 # figure. Add margins for colorbars... 

119 make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad, 

120 w_pad=w_pad, hspace=hspace, wspace=wspace) 

121 make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad, 

122 w_pad=w_pad) 

123 

124 # if a layout is such that a columns (or rows) margin has no 

125 # constraints, we need to make all such instances in the grid 

126 # match in margin size. 

127 match_submerged_margins(layoutgrids, fig) 

128 

129 # update all the variables in the layout. 

130 layoutgrids[fig].update_variables() 

131 

132 warn_collapsed = ('constrained_layout not applied because ' 

133 'axes sizes collapsed to zero. Try making ' 

134 'figure larger or axes decorations smaller.') 

135 if check_no_collapsed_axes(layoutgrids, fig): 

136 reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad, 

137 w_pad=w_pad, hspace=hspace, wspace=wspace) 

138 if compress: 

139 layoutgrids = compress_fixed_aspect(layoutgrids, fig) 

140 layoutgrids[fig].update_variables() 

141 if check_no_collapsed_axes(layoutgrids, fig): 

142 reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad, 

143 w_pad=w_pad, hspace=hspace, wspace=wspace) 

144 else: 

145 _api.warn_external(warn_collapsed) 

146 else: 

147 _api.warn_external(warn_collapsed) 

148 reset_margins(layoutgrids, fig) 

149 return layoutgrids 

150 

151 

152def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)): 

153 """ 

154 Make the layoutgrid tree. 

155 

156 (Sub)Figures get a layoutgrid so we can have figure margins. 

157 

158 Gridspecs that are attached to axes get a layoutgrid so axes 

159 can have margins. 

160 """ 

161 

162 if layoutgrids is None: 

163 layoutgrids = dict() 

164 layoutgrids['hasgrids'] = False 

165 if not hasattr(fig, '_parent'): 

166 # top figure; pass rect as parent to allow user-specified 

167 # margins 

168 layoutgrids[fig] = mlayoutgrid.LayoutGrid(parent=rect, name='figlb') 

169 else: 

170 # subfigure 

171 gs = fig._subplotspec.get_gridspec() 

172 # it is possible the gridspec containing this subfigure hasn't 

173 # been added to the tree yet: 

174 layoutgrids = make_layoutgrids_gs(layoutgrids, gs) 

175 # add the layoutgrid for the subfigure: 

176 parentlb = layoutgrids[gs] 

177 layoutgrids[fig] = mlayoutgrid.LayoutGrid( 

178 parent=parentlb, 

179 name='panellb', 

180 parent_inner=True, 

181 nrows=1, ncols=1, 

182 parent_pos=(fig._subplotspec.rowspan, 

183 fig._subplotspec.colspan)) 

184 # recursively do all subfigures in this figure... 

185 for sfig in fig.subfigs: 

186 layoutgrids = make_layoutgrids(sfig, layoutgrids) 

187 

188 # for each axes at the local level add its gridspec: 

189 for ax in fig._localaxes: 

190 if hasattr(ax, 'get_subplotspec'): 

191 gs = ax.get_subplotspec().get_gridspec() 

192 layoutgrids = make_layoutgrids_gs(layoutgrids, gs) 

193 

194 return layoutgrids 

195 

196 

197def make_layoutgrids_gs(layoutgrids, gs): 

198 """ 

199 Make the layoutgrid for a gridspec (and anything nested in the gridspec) 

200 """ 

201 

202 if gs in layoutgrids or gs.figure is None: 

203 return layoutgrids 

204 # in order to do constrained_layout there has to be at least *one* 

205 # gridspec in the tree: 

206 layoutgrids['hasgrids'] = True 

207 if not hasattr(gs, '_subplot_spec'): 

208 # normal gridspec 

209 parent = layoutgrids[gs.figure] 

210 layoutgrids[gs] = mlayoutgrid.LayoutGrid( 

211 parent=parent, 

212 parent_inner=True, 

213 name='gridspec', 

214 ncols=gs._ncols, nrows=gs._nrows, 

215 width_ratios=gs.get_width_ratios(), 

216 height_ratios=gs.get_height_ratios()) 

217 else: 

218 # this is a gridspecfromsubplotspec: 

219 subplot_spec = gs._subplot_spec 

220 parentgs = subplot_spec.get_gridspec() 

221 # if a nested gridspec it is possible the parent is not in there yet: 

222 if parentgs not in layoutgrids: 

223 layoutgrids = make_layoutgrids_gs(layoutgrids, parentgs) 

224 subspeclb = layoutgrids[parentgs] 

225 # gridspecfromsubplotspec need an outer container: 

226 # get a unique representation: 

227 rep = (gs, 'top') 

228 if rep not in layoutgrids: 

229 layoutgrids[rep] = mlayoutgrid.LayoutGrid( 

230 parent=subspeclb, 

231 name='top', 

232 nrows=1, ncols=1, 

233 parent_pos=(subplot_spec.rowspan, subplot_spec.colspan)) 

234 layoutgrids[gs] = mlayoutgrid.LayoutGrid( 

235 parent=layoutgrids[rep], 

236 name='gridspec', 

237 nrows=gs._nrows, ncols=gs._ncols, 

238 width_ratios=gs.get_width_ratios(), 

239 height_ratios=gs.get_height_ratios()) 

240 return layoutgrids 

241 

242 

243def check_no_collapsed_axes(layoutgrids, fig): 

244 """ 

245 Check that no axes have collapsed to zero size. 

246 """ 

247 for sfig in fig.subfigs: 

248 ok = check_no_collapsed_axes(layoutgrids, sfig) 

249 if not ok: 

250 return False 

251 

252 for ax in fig.axes: 

253 if hasattr(ax, 'get_subplotspec'): 

254 gs = ax.get_subplotspec().get_gridspec() 

255 if gs in layoutgrids: 

256 lg = layoutgrids[gs] 

257 for i in range(gs.nrows): 

258 for j in range(gs.ncols): 

259 bb = lg.get_inner_bbox(i, j) 

260 if bb.width <= 0 or bb.height <= 0: 

261 return False 

262 return True 

263 

264 

265def compress_fixed_aspect(layoutgrids, fig): 

266 gs = None 

267 for ax in fig.axes: 

268 if not hasattr(ax, 'get_subplotspec'): 

269 continue 

270 ax.apply_aspect() 

271 sub = ax.get_subplotspec() 

272 _gs = sub.get_gridspec() 

273 if gs is None: 

274 gs = _gs 

275 extraw = np.zeros(gs.ncols) 

276 extrah = np.zeros(gs.nrows) 

277 elif _gs != gs: 

278 raise ValueError('Cannot do compressed layout if axes are not' 

279 'all from the same gridspec') 

280 orig = ax.get_position(original=True) 

281 actual = ax.get_position(original=False) 

282 dw = orig.width - actual.width 

283 if dw > 0: 

284 extraw[sub.colspan] = np.maximum(extraw[sub.colspan], dw) 

285 dh = orig.height - actual.height 

286 if dh > 0: 

287 extrah[sub.rowspan] = np.maximum(extrah[sub.rowspan], dh) 

288 

289 if gs is None: 

290 raise ValueError('Cannot do compressed layout if no axes ' 

291 'are part of a gridspec.') 

292 w = np.sum(extraw) / 2 

293 layoutgrids[fig].edit_margin_min('left', w) 

294 layoutgrids[fig].edit_margin_min('right', w) 

295 

296 h = np.sum(extrah) / 2 

297 layoutgrids[fig].edit_margin_min('top', h) 

298 layoutgrids[fig].edit_margin_min('bottom', h) 

299 return layoutgrids 

300 

301 

302def get_margin_from_padding(obj, *, w_pad=0, h_pad=0, 

303 hspace=0, wspace=0): 

304 

305 ss = obj._subplotspec 

306 gs = ss.get_gridspec() 

307 

308 if hasattr(gs, 'hspace'): 

309 _hspace = (gs.hspace if gs.hspace is not None else hspace) 

310 _wspace = (gs.wspace if gs.wspace is not None else wspace) 

311 else: 

312 _hspace = (gs._hspace if gs._hspace is not None else hspace) 

313 _wspace = (gs._wspace if gs._wspace is not None else wspace) 

314 

315 _wspace = _wspace / 2 

316 _hspace = _hspace / 2 

317 

318 nrows, ncols = gs.get_geometry() 

319 # there are two margins for each direction. The "cb" 

320 # margins are for pads and colorbars, the non-"cb" are 

321 # for the axes decorations (labels etc). 

322 margin = {'leftcb': w_pad, 'rightcb': w_pad, 

323 'bottomcb': h_pad, 'topcb': h_pad, 

324 'left': 0, 'right': 0, 

325 'top': 0, 'bottom': 0} 

326 if _wspace / ncols > w_pad: 

327 if ss.colspan.start > 0: 

328 margin['leftcb'] = _wspace / ncols 

329 if ss.colspan.stop < ncols: 

330 margin['rightcb'] = _wspace / ncols 

331 if _hspace / nrows > h_pad: 

332 if ss.rowspan.stop < nrows: 

333 margin['bottomcb'] = _hspace / nrows 

334 if ss.rowspan.start > 0: 

335 margin['topcb'] = _hspace / nrows 

336 

337 return margin 

338 

339 

340def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0, 

341 hspace=0, wspace=0): 

342 """ 

343 For each axes, make a margin between the *pos* layoutbox and the 

344 *axes* layoutbox be a minimum size that can accommodate the 

345 decorations on the axis. 

346 

347 Then make room for colorbars. 

348 """ 

349 for sfig in fig.subfigs: # recursively make child panel margins 

350 ss = sfig._subplotspec 

351 make_layout_margins(layoutgrids, sfig, renderer, 

352 w_pad=w_pad, h_pad=h_pad, 

353 hspace=hspace, wspace=wspace) 

354 

355 margins = get_margin_from_padding(sfig, w_pad=0, h_pad=0, 

356 hspace=hspace, wspace=wspace) 

357 layoutgrids[sfig].parent.edit_outer_margin_mins(margins, ss) 

358 

359 for ax in fig._localaxes: 

360 if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout(): 

361 continue 

362 

363 ss = ax.get_subplotspec() 

364 gs = ss.get_gridspec() 

365 

366 if gs not in layoutgrids: 

367 return 

368 

369 margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad, 

370 hspace=hspace, wspace=wspace) 

371 pos, bbox = get_pos_and_bbox(ax, renderer) 

372 # the margin is the distance between the bounding box of the axes 

373 # and its position (plus the padding from above) 

374 margin['left'] += pos.x0 - bbox.x0 

375 margin['right'] += bbox.x1 - pos.x1 

376 # remember that rows are ordered from top: 

377 margin['bottom'] += pos.y0 - bbox.y0 

378 margin['top'] += bbox.y1 - pos.y1 

379 

380 # make margin for colorbars. These margins go in the 

381 # padding margin, versus the margin for axes decorators. 

382 for cbax in ax._colorbars: 

383 # note pad is a fraction of the parent width... 

384 pad = colorbar_get_pad(layoutgrids, cbax) 

385 # colorbars can be child of more than one subplot spec: 

386 cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax) 

387 loc = cbax._colorbar_info['location'] 

388 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) 

389 if loc == 'right': 

390 if cbp_cspan.stop == ss.colspan.stop: 

391 # only increase if the colorbar is on the right edge 

392 margin['rightcb'] += cbbbox.width + pad 

393 elif loc == 'left': 

394 if cbp_cspan.start == ss.colspan.start: 

395 # only increase if the colorbar is on the left edge 

396 margin['leftcb'] += cbbbox.width + pad 

397 elif loc == 'top': 

398 if cbp_rspan.start == ss.rowspan.start: 

399 margin['topcb'] += cbbbox.height + pad 

400 else: 

401 if cbp_rspan.stop == ss.rowspan.stop: 

402 margin['bottomcb'] += cbbbox.height + pad 

403 # If the colorbars are wider than the parent box in the 

404 # cross direction 

405 if loc in ['top', 'bottom']: 

406 if (cbp_cspan.start == ss.colspan.start and 

407 cbbbox.x0 < bbox.x0): 

408 margin['left'] += bbox.x0 - cbbbox.x0 

409 if (cbp_cspan.stop == ss.colspan.stop and 

410 cbbbox.x1 > bbox.x1): 

411 margin['right'] += cbbbox.x1 - bbox.x1 

412 # or taller: 

413 if loc in ['left', 'right']: 

414 if (cbp_rspan.stop == ss.rowspan.stop and 

415 cbbbox.y0 < bbox.y0): 

416 margin['bottom'] += bbox.y0 - cbbbox.y0 

417 if (cbp_rspan.start == ss.rowspan.start and 

418 cbbbox.y1 > bbox.y1): 

419 margin['top'] += cbbbox.y1 - bbox.y1 

420 # pass the new margins down to the layout grid for the solution... 

421 layoutgrids[gs].edit_outer_margin_mins(margin, ss) 

422 

423 

424def make_margin_suptitles(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0): 

425 # Figure out how large the suptitle is and make the 

426 # top level figure margin larger. 

427 

428 inv_trans_fig = fig.transFigure.inverted().transform_bbox 

429 # get the h_pad and w_pad as distances in the local subfigure coordinates: 

430 padbox = mtransforms.Bbox([[0, 0], [w_pad, h_pad]]) 

431 padbox = (fig.transFigure - 

432 fig.transSubfigure).transform_bbox(padbox) 

433 h_pad_local = padbox.height 

434 w_pad_local = padbox.width 

435 

436 for sfig in fig.subfigs: 

437 make_margin_suptitles(layoutgrids, sfig, renderer, 

438 w_pad=w_pad, h_pad=h_pad) 

439 

440 if fig._suptitle is not None and fig._suptitle.get_in_layout(): 

441 p = fig._suptitle.get_position() 

442 if getattr(fig._suptitle, '_autopos', False): 

443 fig._suptitle.set_position((p[0], 1 - h_pad_local)) 

444 bbox = inv_trans_fig(fig._suptitle.get_tightbbox(renderer)) 

445 layoutgrids[fig].edit_margin_min('top', bbox.height + 2 * h_pad) 

446 

447 if fig._supxlabel is not None and fig._supxlabel.get_in_layout(): 

448 p = fig._supxlabel.get_position() 

449 if getattr(fig._supxlabel, '_autopos', False): 

450 fig._supxlabel.set_position((p[0], h_pad_local)) 

451 bbox = inv_trans_fig(fig._supxlabel.get_tightbbox(renderer)) 

452 layoutgrids[fig].edit_margin_min('bottom', 

453 bbox.height + 2 * h_pad) 

454 

455 if fig._supylabel is not None and fig._supylabel.get_in_layout(): 

456 p = fig._supylabel.get_position() 

457 if getattr(fig._supylabel, '_autopos', False): 

458 fig._supylabel.set_position((w_pad_local, p[1])) 

459 bbox = inv_trans_fig(fig._supylabel.get_tightbbox(renderer)) 

460 layoutgrids[fig].edit_margin_min('left', bbox.width + 2 * w_pad) 

461 

462 

463def match_submerged_margins(layoutgrids, fig): 

464 """ 

465 Make the margins that are submerged inside an Axes the same size. 

466 

467 This allows axes that span two columns (or rows) that are offset 

468 from one another to have the same size. 

469 

470 This gives the proper layout for something like:: 

471 fig = plt.figure(constrained_layout=True) 

472 axs = fig.subplot_mosaic("AAAB\nCCDD") 

473 

474 Without this routine, the axes D will be wider than C, because the 

475 margin width between the two columns in C has no width by default, 

476 whereas the margins between the two columns of D are set by the 

477 width of the margin between A and B. However, obviously the user would 

478 like C and D to be the same size, so we need to add constraints to these 

479 "submerged" margins. 

480 

481 This routine makes all the interior margins the same, and the spacing 

482 between the three columns in A and the two column in C are all set to the 

483 margins between the two columns of D. 

484 

485 See test_constrained_layout::test_constrained_layout12 for an example. 

486 """ 

487 

488 for sfig in fig.subfigs: 

489 match_submerged_margins(layoutgrids, sfig) 

490 

491 axs = [a for a in fig.get_axes() if (hasattr(a, 'get_subplotspec') 

492 and a.get_in_layout())] 

493 

494 for ax1 in axs: 

495 ss1 = ax1.get_subplotspec() 

496 if ss1.get_gridspec() not in layoutgrids: 

497 axs.remove(ax1) 

498 continue 

499 lg1 = layoutgrids[ss1.get_gridspec()] 

500 

501 # interior columns: 

502 if len(ss1.colspan) > 1: 

503 maxsubl = np.max( 

504 lg1.margin_vals['left'][ss1.colspan[1:]] + 

505 lg1.margin_vals['leftcb'][ss1.colspan[1:]] 

506 ) 

507 maxsubr = np.max( 

508 lg1.margin_vals['right'][ss1.colspan[:-1]] + 

509 lg1.margin_vals['rightcb'][ss1.colspan[:-1]] 

510 ) 

511 for ax2 in axs: 

512 ss2 = ax2.get_subplotspec() 

513 lg2 = layoutgrids[ss2.get_gridspec()] 

514 if lg2 is not None and len(ss2.colspan) > 1: 

515 maxsubl2 = np.max( 

516 lg2.margin_vals['left'][ss2.colspan[1:]] + 

517 lg2.margin_vals['leftcb'][ss2.colspan[1:]]) 

518 if maxsubl2 > maxsubl: 

519 maxsubl = maxsubl2 

520 maxsubr2 = np.max( 

521 lg2.margin_vals['right'][ss2.colspan[:-1]] + 

522 lg2.margin_vals['rightcb'][ss2.colspan[:-1]]) 

523 if maxsubr2 > maxsubr: 

524 maxsubr = maxsubr2 

525 for i in ss1.colspan[1:]: 

526 lg1.edit_margin_min('left', maxsubl, cell=i) 

527 for i in ss1.colspan[:-1]: 

528 lg1.edit_margin_min('right', maxsubr, cell=i) 

529 

530 # interior rows: 

531 if len(ss1.rowspan) > 1: 

532 maxsubt = np.max( 

533 lg1.margin_vals['top'][ss1.rowspan[1:]] + 

534 lg1.margin_vals['topcb'][ss1.rowspan[1:]] 

535 ) 

536 maxsubb = np.max( 

537 lg1.margin_vals['bottom'][ss1.rowspan[:-1]] + 

538 lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]] 

539 ) 

540 

541 for ax2 in axs: 

542 ss2 = ax2.get_subplotspec() 

543 lg2 = layoutgrids[ss2.get_gridspec()] 

544 if lg2 is not None: 

545 if len(ss2.rowspan) > 1: 

546 maxsubt = np.max([np.max( 

547 lg2.margin_vals['top'][ss2.rowspan[1:]] + 

548 lg2.margin_vals['topcb'][ss2.rowspan[1:]] 

549 ), maxsubt]) 

550 maxsubb = np.max([np.max( 

551 lg2.margin_vals['bottom'][ss2.rowspan[:-1]] + 

552 lg2.margin_vals['bottomcb'][ss2.rowspan[:-1]] 

553 ), maxsubb]) 

554 for i in ss1.rowspan[1:]: 

555 lg1.edit_margin_min('top', maxsubt, cell=i) 

556 for i in ss1.rowspan[:-1]: 

557 lg1.edit_margin_min('bottom', maxsubb, cell=i) 

558 

559 

560def get_cb_parent_spans(cbax): 

561 """ 

562 Figure out which subplotspecs this colorbar belongs to: 

563 """ 

564 rowstart = np.inf 

565 rowstop = -np.inf 

566 colstart = np.inf 

567 colstop = -np.inf 

568 for parent in cbax._colorbar_info['parents']: 

569 ss = parent.get_subplotspec() 

570 rowstart = min(ss.rowspan.start, rowstart) 

571 rowstop = max(ss.rowspan.stop, rowstop) 

572 colstart = min(ss.colspan.start, colstart) 

573 colstop = max(ss.colspan.stop, colstop) 

574 

575 rowspan = range(rowstart, rowstop) 

576 colspan = range(colstart, colstop) 

577 return rowspan, colspan 

578 

579 

580def get_pos_and_bbox(ax, renderer): 

581 """ 

582 Get the position and the bbox for the axes. 

583 

584 Parameters 

585 ---------- 

586 ax 

587 renderer 

588 

589 Returns 

590 ------- 

591 pos : Bbox 

592 Position in figure coordinates. 

593 bbox : Bbox 

594 Tight bounding box in figure coordinates. 

595 """ 

596 fig = ax.figure 

597 pos = ax.get_position(original=True) 

598 # pos is in panel co-ords, but we need in figure for the layout 

599 pos = pos.transformed(fig.transSubfigure - fig.transFigure) 

600 tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer) 

601 if tightbbox is None: 

602 bbox = pos 

603 else: 

604 bbox = tightbbox.transformed(fig.transFigure.inverted()) 

605 return pos, bbox 

606 

607 

608def reposition_axes(layoutgrids, fig, renderer, *, 

609 w_pad=0, h_pad=0, hspace=0, wspace=0): 

610 """ 

611 Reposition all the axes based on the new inner bounding box. 

612 """ 

613 trans_fig_to_subfig = fig.transFigure - fig.transSubfigure 

614 for sfig in fig.subfigs: 

615 bbox = layoutgrids[sfig].get_outer_bbox() 

616 sfig._redo_transform_rel_fig( 

617 bbox=bbox.transformed(trans_fig_to_subfig)) 

618 reposition_axes(layoutgrids, sfig, renderer, 

619 w_pad=w_pad, h_pad=h_pad, 

620 wspace=wspace, hspace=hspace) 

621 

622 for ax in fig._localaxes: 

623 if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout(): 

624 continue 

625 

626 # grid bbox is in Figure coordinates, but we specify in panel 

627 # coordinates... 

628 ss = ax.get_subplotspec() 

629 gs = ss.get_gridspec() 

630 if gs not in layoutgrids: 

631 return 

632 

633 bbox = layoutgrids[gs].get_inner_bbox(rows=ss.rowspan, 

634 cols=ss.colspan) 

635 

636 # transform from figure to panel for set_position: 

637 newbbox = trans_fig_to_subfig.transform_bbox(bbox) 

638 ax._set_position(newbbox) 

639 

640 # move the colorbars: 

641 # we need to keep track of oldw and oldh if there is more than 

642 # one colorbar: 

643 offset = {'left': 0, 'right': 0, 'bottom': 0, 'top': 0} 

644 for nn, cbax in enumerate(ax._colorbars[::-1]): 

645 if ax == cbax._colorbar_info['parents'][0]: 

646 reposition_colorbar(layoutgrids, cbax, renderer, 

647 offset=offset) 

648 

649 

650def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None): 

651 """ 

652 Place the colorbar in its new place. 

653 

654 Parameters 

655 ---------- 

656 cbax : Axes 

657 Axes for the colorbar 

658 

659 renderer : 

660 w_pad, h_pad : float 

661 width and height padding (in fraction of figure) 

662 hspace, wspace : float 

663 width and height padding as fraction of figure size divided by 

664 number of columns or rows 

665 margin : array-like 

666 offset the colorbar needs to be pushed to in order to 

667 account for multiple colorbars 

668 """ 

669 

670 parents = cbax._colorbar_info['parents'] 

671 gs = parents[0].get_gridspec() 

672 fig = cbax.figure 

673 trans_fig_to_subfig = fig.transFigure - fig.transSubfigure 

674 

675 cb_rspans, cb_cspans = get_cb_parent_spans(cbax) 

676 bboxparent = layoutgrids[gs].get_bbox_for_cb(rows=cb_rspans, 

677 cols=cb_cspans) 

678 pb = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans) 

679 

680 location = cbax._colorbar_info['location'] 

681 anchor = cbax._colorbar_info['anchor'] 

682 fraction = cbax._colorbar_info['fraction'] 

683 aspect = cbax._colorbar_info['aspect'] 

684 shrink = cbax._colorbar_info['shrink'] 

685 

686 cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) 

687 

688 # Colorbar gets put at extreme edge of outer bbox of the subplotspec 

689 # It needs to be moved in by: 1) a pad 2) its "margin" 3) by 

690 # any colorbars already added at this location: 

691 cbpad = colorbar_get_pad(layoutgrids, cbax) 

692 if location in ('left', 'right'): 

693 # fraction and shrink are fractions of parent 

694 pbcb = pb.shrunk(fraction, shrink).anchored(anchor, pb) 

695 # The colorbar is at the left side of the parent. Need 

696 # to translate to right (or left) 

697 if location == 'right': 

698 lmargin = cbpos.x0 - cbbbox.x0 

699 dx = bboxparent.x1 - pbcb.x0 + offset['right'] 

700 dx += cbpad + lmargin 

701 offset['right'] += cbbbox.width + cbpad 

702 pbcb = pbcb.translated(dx, 0) 

703 else: 

704 lmargin = cbpos.x0 - cbbbox.x0 

705 dx = bboxparent.x0 - pbcb.x0 # edge of parent 

706 dx += -cbbbox.width - cbpad + lmargin - offset['left'] 

707 offset['left'] += cbbbox.width + cbpad 

708 pbcb = pbcb.translated(dx, 0) 

709 else: # horizontal axes: 

710 pbcb = pb.shrunk(shrink, fraction).anchored(anchor, pb) 

711 if location == 'top': 

712 bmargin = cbpos.y0 - cbbbox.y0 

713 dy = bboxparent.y1 - pbcb.y0 + offset['top'] 

714 dy += cbpad + bmargin 

715 offset['top'] += cbbbox.height + cbpad 

716 pbcb = pbcb.translated(0, dy) 

717 else: 

718 bmargin = cbpos.y0 - cbbbox.y0 

719 dy = bboxparent.y0 - pbcb.y0 

720 dy += -cbbbox.height - cbpad + bmargin - offset['bottom'] 

721 offset['bottom'] += cbbbox.height + cbpad 

722 pbcb = pbcb.translated(0, dy) 

723 

724 pbcb = trans_fig_to_subfig.transform_bbox(pbcb) 

725 cbax.set_transform(fig.transSubfigure) 

726 cbax._set_position(pbcb) 

727 cbax.set_anchor(anchor) 

728 if location in ['bottom', 'top']: 

729 aspect = 1 / aspect 

730 cbax.set_box_aspect(aspect) 

731 cbax.set_aspect('auto') 

732 return offset 

733 

734 

735def reset_margins(layoutgrids, fig): 

736 """ 

737 Reset the margins in the layoutboxes of fig. 

738 

739 Margins are usually set as a minimum, so if the figure gets smaller 

740 the minimum needs to be zero in order for it to grow again. 

741 """ 

742 for sfig in fig.subfigs: 

743 reset_margins(layoutgrids, sfig) 

744 for ax in fig.axes: 

745 if hasattr(ax, 'get_subplotspec') and ax.get_in_layout(): 

746 ss = ax.get_subplotspec() 

747 gs = ss.get_gridspec() 

748 if gs in layoutgrids: 

749 layoutgrids[gs].reset_margins() 

750 layoutgrids[fig].reset_margins() 

751 

752 

753def colorbar_get_pad(layoutgrids, cax): 

754 parents = cax._colorbar_info['parents'] 

755 gs = parents[0].get_gridspec() 

756 

757 cb_rspans, cb_cspans = get_cb_parent_spans(cax) 

758 bboxouter = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans) 

759 

760 if cax._colorbar_info['location'] in ['right', 'left']: 

761 size = bboxouter.width 

762 else: 

763 size = bboxouter.height 

764 

765 return cax._colorbar_info['pad'] * size