_constrained_layout.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. """
  2. This module provides the routine to adjust subplot layouts so that there are
  3. no overlapping axes or axes decorations. All axes decorations are dealt with
  4. (labels, ticks, titles, ticklabels) and some dependent artists are also dealt
  5. with (colorbar, suptitle, legend).
  6. Layout is done via :meth:`~matplotlib.gridspec`, with one constraint per
  7. gridspec, so it is possible to have overlapping axes if the gridspecs
  8. overlap (i.e. using :meth:`~matplotlib.gridspec.GridSpecFromSubplotSpec`).
  9. Axes placed using ``figure.subplots()`` or ``figure.add_subplots()`` will
  10. participate in the layout. Axes manually placed via ``figure.add_axes()``
  11. will not.
  12. See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide`
  13. """
  14. # Development Notes:
  15. # What gets a layoutbox:
  16. # - figure
  17. # - gridspec
  18. # - subplotspec
  19. # EITHER:
  20. # - axes + pos for the axes (i.e. the total area taken by axis and
  21. # the actual "position" argument that needs to be sent to
  22. # ax.set_position.)
  23. # - The axes layout box will also encompass the legend, and that is
  24. # how legends get included (axes legends, not figure legends)
  25. # - colorbars are siblings of the axes if they are single-axes
  26. # colorbars
  27. # OR:
  28. # - a gridspec can be inside a subplotspec.
  29. # - subplotspec
  30. # EITHER:
  31. # - axes...
  32. # OR:
  33. # - gridspec... with arbitrary nesting...
  34. # - colorbars are siblings of the subplotspecs if they are multi-axes
  35. # colorbars.
  36. # - suptitle:
  37. # - right now suptitles are just stacked atop everything else in figure.
  38. # Could imagine suptitles being gridspec suptitles, but not implemented
  39. #
  40. # Todo: AnchoredOffsetbox connected to gridspecs or axes. This would
  41. # be more general way to add extra-axes annotations.
  42. import logging
  43. import numpy as np
  44. import matplotlib.cbook as cbook
  45. import matplotlib._layoutbox as layoutbox
  46. _log = logging.getLogger(__name__)
  47. def _in_same_column(colnum0min, colnum0max, colnumCmin, colnumCmax):
  48. return (colnumCmin <= colnum0min <= colnumCmax
  49. or colnumCmin <= colnum0max <= colnumCmax)
  50. def _in_same_row(rownum0min, rownum0max, rownumCmin, rownumCmax):
  51. return (rownumCmin <= rownum0min <= rownumCmax
  52. or rownumCmin <= rownum0max <= rownumCmax)
  53. def _axes_all_finite_sized(fig):
  54. """Return whether all axes in the figure have a finite width and height."""
  55. for ax in fig.axes:
  56. if ax._layoutbox is not None:
  57. newpos = ax._poslayoutbox.get_rect()
  58. if newpos[2] <= 0 or newpos[3] <= 0:
  59. return False
  60. return True
  61. ######################################################
  62. def do_constrained_layout(fig, renderer, h_pad, w_pad,
  63. hspace=None, wspace=None):
  64. """
  65. Do the constrained_layout. Called at draw time in
  66. ``figure.constrained_layout()``
  67. Parameters
  68. ----------
  69. fig : Figure
  70. is the ``figure`` instance to do the layout in.
  71. renderer : Renderer
  72. the renderer to use.
  73. h_pad, w_pad : float
  74. are in figure-normalized units, and are a padding around the axes
  75. elements.
  76. hspace, wspace : float
  77. are in fractions of the subplot sizes.
  78. """
  79. # Steps:
  80. #
  81. # 1. get a list of unique gridspecs in this figure. Each gridspec will be
  82. # constrained separately.
  83. # 2. Check for gaps in the gridspecs. i.e. if not every axes slot in the
  84. # gridspec has been filled. If empty, add a ghost axis that is made so
  85. # that it cannot be seen (though visible=True). This is needed to make
  86. # a blank spot in the layout.
  87. # 3. Compare the tight_bbox of each axes to its `position`, and assume that
  88. # the difference is the space needed by the elements around the edge of
  89. # the axes (decorations) like the title, ticklabels, x-labels, etc. This
  90. # can include legends who overspill the axes boundaries.
  91. # 4. Constrain gridspec elements to line up:
  92. # a) if colnum0 != colnumC, the two subplotspecs are stacked next to
  93. # each other, with the appropriate order.
  94. # b) if colnum0 == colnumC, line up the left or right side of the
  95. # _poslayoutbox (depending if it is the min or max num that is equal).
  96. # c) do the same for rows...
  97. # 5. The above doesn't constrain relative sizes of the _poslayoutboxes
  98. # at all, and indeed zero-size is a solution that the solver often finds
  99. # more convenient than expanding the sizes. Right now the solution is to
  100. # compare subplotspec sizes (i.e. drowsC and drows0) and constrain the
  101. # larger _poslayoutbox to be larger than the ratio of the sizes. i.e. if
  102. # drows0 > drowsC, then ax._poslayoutbox > axc._poslayoutbox*drowsC/drows0.
  103. # This works fine *if* the decorations are similar between the axes.
  104. # If the larger subplotspec has much larger axes decorations, then the
  105. # constraint above is incorrect.
  106. #
  107. # We need the greater than in the above, in general, rather than an equals
  108. # sign. Consider the case of the left column having 2 rows, and the right
  109. # column having 1 row. We want the top and bottom of the _poslayoutboxes
  110. # to line up. So that means if there are decorations on the left column
  111. # axes they will be smaller than half as large as the right hand axis.
  112. #
  113. # This can break down if the decoration size for the right hand axis (the
  114. # margins) is very large. There must be a math way to check for this case.
  115. invTransFig = fig.transFigure.inverted().transform_bbox
  116. # list of unique gridspecs that contain child axes:
  117. gss = set()
  118. for ax in fig.axes:
  119. if hasattr(ax, 'get_subplotspec'):
  120. gs = ax.get_subplotspec().get_gridspec()
  121. if gs._layoutbox is not None:
  122. gss.add(gs)
  123. if len(gss) == 0:
  124. cbook._warn_external('There are no gridspecs with layoutboxes. '
  125. 'Possibly did not call parent GridSpec with the'
  126. ' figure= keyword')
  127. if fig._layoutbox.constrained_layout_called < 1:
  128. for gs in gss:
  129. # fill in any empty gridspec slots w/ ghost axes...
  130. _make_ghost_gridspec_slots(fig, gs)
  131. for nnn in range(2):
  132. # do the algorithm twice. This has to be done because decorators
  133. # change size after the first re-position (i.e. x/yticklabels get
  134. # larger/smaller). This second reposition tends to be much milder,
  135. # so doing twice makes things work OK.
  136. for ax in fig.axes:
  137. _log.debug(ax._layoutbox)
  138. if ax._layoutbox is not None:
  139. # make margins for each layout box based on the size of
  140. # the decorators.
  141. _make_layout_margins(ax, renderer, h_pad, w_pad)
  142. # do layout for suptitle.
  143. suptitle = fig._suptitle
  144. do_suptitle = (suptitle is not None and
  145. suptitle._layoutbox is not None and
  146. suptitle.get_in_layout())
  147. if do_suptitle:
  148. bbox = invTransFig(
  149. suptitle.get_window_extent(renderer=renderer))
  150. height = bbox.y1 - bbox.y0
  151. if np.isfinite(height):
  152. # reserve at top of figure include an h_pad above and below
  153. suptitle._layoutbox.edit_height(height + h_pad * 2)
  154. # OK, the above lines up ax._poslayoutbox with ax._layoutbox
  155. # now we need to
  156. # 1) arrange the subplotspecs. We do it at this level because
  157. # the subplotspecs are meant to contain other dependent axes
  158. # like colorbars or legends.
  159. # 2) line up the right and left side of the ax._poslayoutbox
  160. # that have the same subplotspec maxes.
  161. if fig._layoutbox.constrained_layout_called < 1:
  162. # arrange the subplotspecs... This is all done relative to each
  163. # other. Some subplotspecs contain axes, and others contain
  164. # gridspecs the ones that contain gridspecs are a set proportion
  165. # of their parent gridspec. The ones that contain axes are
  166. # not so constrained.
  167. figlb = fig._layoutbox
  168. for child in figlb.children:
  169. if child._is_gridspec_layoutbox():
  170. # This routine makes all the subplot spec containers
  171. # have the correct arrangement. It just stacks the
  172. # subplot layoutboxes in the correct order...
  173. _arrange_subplotspecs(child, hspace=hspace, wspace=wspace)
  174. for gs in gss:
  175. _align_spines(fig, gs)
  176. fig._layoutbox.constrained_layout_called += 1
  177. fig._layoutbox.update_variables()
  178. # check if any axes collapsed to zero. If not, don't change positions:
  179. if _axes_all_finite_sized(fig):
  180. # Now set the position of the axes...
  181. for ax in fig.axes:
  182. if ax._layoutbox is not None:
  183. newpos = ax._poslayoutbox.get_rect()
  184. # Now set the new position.
  185. # ax.set_position will zero out the layout for
  186. # this axis, allowing users to hard-code the position,
  187. # so this does the same w/o zeroing layout.
  188. ax._set_position(newpos, which='original')
  189. if do_suptitle:
  190. newpos = suptitle._layoutbox.get_rect()
  191. suptitle.set_y(1.0 - h_pad)
  192. else:
  193. if suptitle is not None and suptitle._layoutbox is not None:
  194. suptitle._layoutbox.edit_height(0)
  195. else:
  196. cbook._warn_external('constrained_layout not applied. At least '
  197. 'one axes collapsed to zero width or height.')
  198. def _make_ghost_gridspec_slots(fig, gs):
  199. """
  200. Check for unoccupied gridspec slots and make ghost axes for these
  201. slots... Do for each gs separately. This is a pretty big kludge
  202. but shouldn't have too much ill effect. The worst is that
  203. someone querying the figure will wonder why there are more
  204. axes than they thought.
  205. """
  206. nrows, ncols = gs.get_geometry()
  207. hassubplotspec = np.zeros(nrows * ncols, dtype=bool)
  208. axs = []
  209. for ax in fig.axes:
  210. if (hasattr(ax, 'get_subplotspec')
  211. and ax._layoutbox is not None
  212. and ax.get_subplotspec().get_gridspec() == gs):
  213. axs += [ax]
  214. for ax in axs:
  215. ss0 = ax.get_subplotspec()
  216. hassubplotspec[ss0.num1:(ss0.num2 + 1)] = True
  217. for nn, hss in enumerate(hassubplotspec):
  218. if not hss:
  219. # this gridspec slot doesn't have an axis so we
  220. # make a "ghost".
  221. ax = fig.add_subplot(gs[nn])
  222. ax.set_visible(False)
  223. def _make_layout_margins(ax, renderer, h_pad, w_pad):
  224. """
  225. For each axes, make a margin between the *pos* layoutbox and the
  226. *axes* layoutbox be a minimum size that can accommodate the
  227. decorations on the axis.
  228. """
  229. fig = ax.figure
  230. invTransFig = fig.transFigure.inverted().transform_bbox
  231. pos = ax.get_position(original=True)
  232. tightbbox = ax.get_tightbbox(renderer=renderer)
  233. if tightbbox is None:
  234. bbox = pos
  235. else:
  236. bbox = invTransFig(tightbbox)
  237. # this can go wrong:
  238. if not (np.isfinite(bbox.width) and np.isfinite(bbox.height)):
  239. # just abort, this is likely a bad set of co-ordinates that
  240. # is transitory...
  241. return
  242. # use stored h_pad if it exists
  243. h_padt = ax._poslayoutbox.h_pad
  244. if h_padt is None:
  245. h_padt = h_pad
  246. w_padt = ax._poslayoutbox.w_pad
  247. if w_padt is None:
  248. w_padt = w_pad
  249. ax._poslayoutbox.edit_left_margin_min(-bbox.x0 +
  250. pos.x0 + w_padt)
  251. ax._poslayoutbox.edit_right_margin_min(bbox.x1 -
  252. pos.x1 + w_padt)
  253. ax._poslayoutbox.edit_bottom_margin_min(
  254. -bbox.y0 + pos.y0 + h_padt)
  255. ax._poslayoutbox.edit_top_margin_min(bbox.y1-pos.y1+h_padt)
  256. _log.debug('left %f', (-bbox.x0 + pos.x0 + w_pad))
  257. _log.debug('right %f', (bbox.x1 - pos.x1 + w_pad))
  258. _log.debug('bottom %f', (-bbox.y0 + pos.y0 + h_padt))
  259. _log.debug('bbox.y0 %f', bbox.y0)
  260. _log.debug('pos.y0 %f', pos.y0)
  261. # Sometimes its possible for the solver to collapse
  262. # rather than expand axes, so they all have zero height
  263. # or width. This stops that... It *should* have been
  264. # taken into account w/ pref_width...
  265. if fig._layoutbox.constrained_layout_called < 1:
  266. ax._poslayoutbox.constrain_height_min(20, strength='weak')
  267. ax._poslayoutbox.constrain_width_min(20, strength='weak')
  268. ax._layoutbox.constrain_height_min(20, strength='weak')
  269. ax._layoutbox.constrain_width_min(20, strength='weak')
  270. ax._poslayoutbox.constrain_top_margin(0, strength='weak')
  271. ax._poslayoutbox.constrain_bottom_margin(0,
  272. strength='weak')
  273. ax._poslayoutbox.constrain_right_margin(0, strength='weak')
  274. ax._poslayoutbox.constrain_left_margin(0, strength='weak')
  275. def _align_spines(fig, gs):
  276. """
  277. - Align right/left and bottom/top spines of appropriate subplots.
  278. - Compare size of subplotspec including height and width ratios
  279. and make sure that the axes spines are at least as large
  280. as they should be.
  281. """
  282. # for each gridspec...
  283. nrows, ncols = gs.get_geometry()
  284. width_ratios = gs.get_width_ratios()
  285. height_ratios = gs.get_height_ratios()
  286. if width_ratios is None:
  287. width_ratios = np.ones(ncols)
  288. if height_ratios is None:
  289. height_ratios = np.ones(nrows)
  290. # get axes in this gridspec....
  291. axs = []
  292. for ax in fig.axes:
  293. if (hasattr(ax, 'get_subplotspec')
  294. and ax._layoutbox is not None):
  295. if ax.get_subplotspec().get_gridspec() == gs:
  296. axs += [ax]
  297. rownummin = np.zeros(len(axs), dtype=np.int8)
  298. rownummax = np.zeros(len(axs), dtype=np.int8)
  299. colnummin = np.zeros(len(axs), dtype=np.int8)
  300. colnummax = np.zeros(len(axs), dtype=np.int8)
  301. width = np.zeros(len(axs))
  302. height = np.zeros(len(axs))
  303. for n, ax in enumerate(axs):
  304. ss0 = ax.get_subplotspec()
  305. rownummin[n], colnummin[n] = divmod(ss0.num1, ncols)
  306. rownummax[n], colnummax[n] = divmod(ss0.num2, ncols)
  307. width[n] = np.sum(
  308. width_ratios[colnummin[n]:(colnummax[n] + 1)])
  309. height[n] = np.sum(
  310. height_ratios[rownummin[n]:(rownummax[n] + 1)])
  311. for nn, ax in enumerate(axs[:-1]):
  312. # now compare ax to all the axs:
  313. #
  314. # If the subplotspecs have the same colnumXmax, then line
  315. # up their right sides. If they have the same min, then
  316. # line up their left sides (and vertical equivalents).
  317. rownum0min, colnum0min = rownummin[nn], colnummin[nn]
  318. rownum0max, colnum0max = rownummax[nn], colnummax[nn]
  319. width0, height0 = width[nn], height[nn]
  320. alignleft = False
  321. alignright = False
  322. alignbot = False
  323. aligntop = False
  324. alignheight = False
  325. alignwidth = False
  326. for mm in range(nn+1, len(axs)):
  327. axc = axs[mm]
  328. rownumCmin, colnumCmin = rownummin[mm], colnummin[mm]
  329. rownumCmax, colnumCmax = rownummax[mm], colnummax[mm]
  330. widthC, heightC = width[mm], height[mm]
  331. # Horizontally align axes spines if they have the
  332. # same min or max:
  333. if not alignleft and colnum0min == colnumCmin:
  334. # we want the _poslayoutboxes to line up on left
  335. # side of the axes spines...
  336. layoutbox.align([ax._poslayoutbox,
  337. axc._poslayoutbox],
  338. 'left')
  339. alignleft = True
  340. if not alignright and colnum0max == colnumCmax:
  341. # line up right sides of _poslayoutbox
  342. layoutbox.align([ax._poslayoutbox,
  343. axc._poslayoutbox],
  344. 'right')
  345. alignright = True
  346. # Vertically align axes spines if they have the
  347. # same min or max:
  348. if not aligntop and rownum0min == rownumCmin:
  349. # line up top of _poslayoutbox
  350. _log.debug('rownum0min == rownumCmin')
  351. layoutbox.align([ax._poslayoutbox, axc._poslayoutbox],
  352. 'top')
  353. aligntop = True
  354. if not alignbot and rownum0max == rownumCmax:
  355. # line up bottom of _poslayoutbox
  356. _log.debug('rownum0max == rownumCmax')
  357. layoutbox.align([ax._poslayoutbox, axc._poslayoutbox],
  358. 'bottom')
  359. alignbot = True
  360. ###########
  361. # Now we make the widths and heights of position boxes
  362. # similar. (i.e the spine locations)
  363. # This allows vertically stacked subplots to have
  364. # different sizes if they occupy different amounts
  365. # of the gridspec: i.e.
  366. # gs = gridspec.GridSpec(3, 1)
  367. # ax1 = gs[0,:]
  368. # ax2 = gs[1:,:]
  369. # then drows0 = 1, and drowsC = 2, and ax2
  370. # should be at least twice as large as ax1.
  371. # But it can be more than twice as large because
  372. # it needs less room for the labeling.
  373. #
  374. # For height, this only needs to be done if the
  375. # subplots share a column. For width if they
  376. # share a row.
  377. drowsC = (rownumCmax - rownumCmin + 1)
  378. drows0 = (rownum0max - rownum0min + 1)
  379. dcolsC = (colnumCmax - colnumCmin + 1)
  380. dcols0 = (colnum0max - colnum0min + 1)
  381. if not alignheight and drows0 == drowsC:
  382. ax._poslayoutbox.constrain_height(
  383. axc._poslayoutbox.height * height0 / heightC)
  384. alignheight = True
  385. elif _in_same_column(colnum0min, colnum0max,
  386. colnumCmin, colnumCmax):
  387. if height0 > heightC:
  388. ax._poslayoutbox.constrain_height_min(
  389. axc._poslayoutbox.height * height0 / heightC)
  390. # these constraints stop the smaller axes from
  391. # being allowed to go to zero height...
  392. axc._poslayoutbox.constrain_height_min(
  393. ax._poslayoutbox.height * heightC /
  394. (height0*1.8))
  395. elif height0 < heightC:
  396. axc._poslayoutbox.constrain_height_min(
  397. ax._poslayoutbox.height * heightC / height0)
  398. ax._poslayoutbox.constrain_height_min(
  399. ax._poslayoutbox.height * height0 /
  400. (heightC*1.8))
  401. # widths...
  402. if not alignwidth and dcols0 == dcolsC:
  403. ax._poslayoutbox.constrain_width(
  404. axc._poslayoutbox.width * width0 / widthC)
  405. alignwidth = True
  406. elif _in_same_row(rownum0min, rownum0max,
  407. rownumCmin, rownumCmax):
  408. if width0 > widthC:
  409. ax._poslayoutbox.constrain_width_min(
  410. axc._poslayoutbox.width * width0 / widthC)
  411. axc._poslayoutbox.constrain_width_min(
  412. ax._poslayoutbox.width * widthC /
  413. (width0*1.8))
  414. elif width0 < widthC:
  415. axc._poslayoutbox.constrain_width_min(
  416. ax._poslayoutbox.width * widthC / width0)
  417. ax._poslayoutbox.constrain_width_min(
  418. axc._poslayoutbox.width * width0 /
  419. (widthC*1.8))
  420. def _arrange_subplotspecs(gs, hspace=0, wspace=0):
  421. """Recursively arrange the subplotspec children of the given gridspec."""
  422. sschildren = []
  423. for child in gs.children:
  424. if child._is_subplotspec_layoutbox():
  425. for child2 in child.children:
  426. # check for gridspec children...
  427. if child2._is_gridspec_layoutbox():
  428. _arrange_subplotspecs(child2, hspace=hspace, wspace=wspace)
  429. sschildren += [child]
  430. # now arrange the subplots...
  431. for child0 in sschildren:
  432. ss0 = child0.artist
  433. nrows, ncols = ss0.get_gridspec().get_geometry()
  434. rowNum0min, colNum0min = divmod(ss0.num1, ncols)
  435. rowNum0max, colNum0max = divmod(ss0.num2, ncols)
  436. sschildren = sschildren[1:]
  437. for childc in sschildren:
  438. ssc = childc.artist
  439. rowNumCmin, colNumCmin = divmod(ssc.num1, ncols)
  440. rowNumCmax, colNumCmax = divmod(ssc.num2, ncols)
  441. # OK, this tells us the relative layout of ax
  442. # with axc
  443. thepad = wspace / ncols
  444. if colNum0max < colNumCmin:
  445. layoutbox.hstack([ss0._layoutbox, ssc._layoutbox],
  446. padding=thepad)
  447. if colNumCmax < colNum0min:
  448. layoutbox.hstack([ssc._layoutbox, ss0._layoutbox],
  449. padding=thepad)
  450. ####
  451. # vertical alignment
  452. thepad = hspace / nrows
  453. if rowNum0max < rowNumCmin:
  454. layoutbox.vstack([ss0._layoutbox,
  455. ssc._layoutbox],
  456. padding=thepad)
  457. if rowNumCmax < rowNum0min:
  458. layoutbox.vstack([ssc._layoutbox,
  459. ss0._layoutbox],
  460. padding=thepad)
  461. def layoutcolorbarsingle(ax, cax, shrink, aspect, location, pad=0.05):
  462. """
  463. Do the layout for a colorbar, to not overly pollute colorbar.py
  464. *pad* is in fraction of the original axis size.
  465. """
  466. axlb = ax._layoutbox
  467. axpos = ax._poslayoutbox
  468. axsslb = ax.get_subplotspec()._layoutbox
  469. lb = layoutbox.LayoutBox(
  470. parent=axsslb,
  471. name=axsslb.name + '.cbar',
  472. artist=cax)
  473. if location in ('left', 'right'):
  474. lbpos = layoutbox.LayoutBox(
  475. parent=lb,
  476. name=lb.name + '.pos',
  477. tightwidth=False,
  478. pos=True,
  479. subplot=False,
  480. artist=cax)
  481. if location == 'right':
  482. # arrange to right of parent axis
  483. layoutbox.hstack([axlb, lb], padding=pad * axlb.width,
  484. strength='strong')
  485. else:
  486. layoutbox.hstack([lb, axlb], padding=pad * axlb.width)
  487. # constrain the height and center...
  488. layoutbox.match_heights([axpos, lbpos], [1, shrink])
  489. layoutbox.align([axpos, lbpos], 'v_center')
  490. # set the width of the pos box
  491. lbpos.constrain_width(shrink * axpos.height * (1/aspect),
  492. strength='strong')
  493. elif location in ('bottom', 'top'):
  494. lbpos = layoutbox.LayoutBox(
  495. parent=lb,
  496. name=lb.name + '.pos',
  497. tightheight=True,
  498. pos=True,
  499. subplot=False,
  500. artist=cax)
  501. if location == 'bottom':
  502. layoutbox.vstack([axlb, lb], padding=pad * axlb.height)
  503. else:
  504. layoutbox.vstack([lb, axlb], padding=pad * axlb.height)
  505. # constrain the height and center...
  506. layoutbox.match_widths([axpos, lbpos],
  507. [1, shrink], strength='strong')
  508. layoutbox.align([axpos, lbpos], 'h_center')
  509. # set the height of the pos box
  510. lbpos.constrain_height(axpos.width * aspect * shrink,
  511. strength='medium')
  512. return lb, lbpos
  513. def _getmaxminrowcolumn(axs):
  514. # helper to get the min/max rows and columns of a list of axes.
  515. maxrow = -100000
  516. minrow = 1000000
  517. maxax = None
  518. minax = None
  519. maxcol = -100000
  520. mincol = 1000000
  521. maxax_col = None
  522. minax_col = None
  523. for ax in axs:
  524. subspec = ax.get_subplotspec()
  525. nrows, ncols, row_start, row_stop, col_start, col_stop = \
  526. subspec.get_rows_columns()
  527. if row_stop > maxrow:
  528. maxrow = row_stop
  529. maxax = ax
  530. if row_start < minrow:
  531. minrow = row_start
  532. minax = ax
  533. if col_stop > maxcol:
  534. maxcol = col_stop
  535. maxax_col = ax
  536. if col_start < mincol:
  537. mincol = col_start
  538. minax_col = ax
  539. return (minrow, maxrow, minax, maxax, mincol, maxcol, minax_col, maxax_col)
  540. def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05):
  541. """
  542. Do the layout for a colorbar, to not overly pollute colorbar.py
  543. *pad* is in fraction of the original axis size.
  544. """
  545. gs = parents[0].get_subplotspec().get_gridspec()
  546. # parent layout box....
  547. gslb = gs._layoutbox
  548. lb = layoutbox.LayoutBox(parent=gslb.parent,
  549. name=gslb.parent.name + '.cbar',
  550. artist=cax)
  551. # figure out the row and column extent of the parents.
  552. (minrow, maxrow, minax_row, maxax_row,
  553. mincol, maxcol, minax_col, maxax_col) = _getmaxminrowcolumn(parents)
  554. if location in ('left', 'right'):
  555. lbpos = layoutbox.LayoutBox(
  556. parent=lb,
  557. name=lb.name + '.pos',
  558. tightwidth=False,
  559. pos=True,
  560. subplot=False,
  561. artist=cax)
  562. for ax in parents:
  563. if location == 'right':
  564. order = [ax._layoutbox, lb]
  565. else:
  566. order = [lb, ax._layoutbox]
  567. layoutbox.hstack(order, padding=pad * gslb.width,
  568. strength='strong')
  569. # constrain the height and center...
  570. # This isn't quite right. We'd like the colorbar
  571. # pos to line up w/ the axes poss, not the size of the
  572. # gs.
  573. # Horizontal Layout: need to check all the axes in this gridspec
  574. for ch in gslb.children:
  575. subspec = ch.artist
  576. nrows, ncols, row_start, row_stop, col_start, col_stop = \
  577. subspec.get_rows_columns()
  578. if location == 'right':
  579. if col_stop <= maxcol:
  580. order = [subspec._layoutbox, lb]
  581. # arrange to right of the parents
  582. if col_start > maxcol:
  583. order = [lb, subspec._layoutbox]
  584. elif location == 'left':
  585. if col_start >= mincol:
  586. order = [lb, subspec._layoutbox]
  587. if col_stop < mincol:
  588. order = [subspec._layoutbox, lb]
  589. layoutbox.hstack(order, padding=pad * gslb.width,
  590. strength='strong')
  591. # Vertical layout:
  592. maxposlb = minax_row._poslayoutbox
  593. minposlb = maxax_row._poslayoutbox
  594. # now we want the height of the colorbar pos to be
  595. # set by the top and bottom of the min/max axes...
  596. # bottom top
  597. # b t
  598. # h = (top-bottom)*shrink
  599. # b = bottom + (top-bottom - h) / 2.
  600. lbpos.constrain_height(
  601. (maxposlb.top - minposlb.bottom) *
  602. shrink, strength='strong')
  603. lbpos.constrain_bottom(
  604. (maxposlb.top - minposlb.bottom) *
  605. (1 - shrink)/2 + minposlb.bottom,
  606. strength='strong')
  607. # set the width of the pos box
  608. lbpos.constrain_width(lbpos.height * (shrink / aspect),
  609. strength='strong')
  610. elif location in ('bottom', 'top'):
  611. lbpos = layoutbox.LayoutBox(
  612. parent=lb,
  613. name=lb.name + '.pos',
  614. tightheight=True,
  615. pos=True,
  616. subplot=False,
  617. artist=cax)
  618. for ax in parents:
  619. if location == 'bottom':
  620. order = [ax._layoutbox, lb]
  621. else:
  622. order = [lb, ax._layoutbox]
  623. layoutbox.vstack(order, padding=pad * gslb.width,
  624. strength='strong')
  625. # Vertical Layout: need to check all the axes in this gridspec
  626. for ch in gslb.children:
  627. subspec = ch.artist
  628. nrows, ncols, row_start, row_stop, col_start, col_stop = \
  629. subspec.get_rows_columns()
  630. if location == 'bottom':
  631. if row_stop <= minrow:
  632. order = [subspec._layoutbox, lb]
  633. if row_start > maxrow:
  634. order = [lb, subspec._layoutbox]
  635. elif location == 'top':
  636. if row_stop < minrow:
  637. order = [subspec._layoutbox, lb]
  638. if row_start >= maxrow:
  639. order = [lb, subspec._layoutbox]
  640. layoutbox.vstack(order, padding=pad * gslb.width,
  641. strength='strong')
  642. # Do horizontal layout...
  643. maxposlb = maxax_col._poslayoutbox
  644. minposlb = minax_col._poslayoutbox
  645. lbpos.constrain_width((maxposlb.right - minposlb.left) *
  646. shrink)
  647. lbpos.constrain_left(
  648. (maxposlb.right - minposlb.left) *
  649. (1-shrink)/2 + minposlb.left)
  650. # set the height of the pos box
  651. lbpos.constrain_height(lbpos.width * shrink * aspect,
  652. strength='medium')
  653. return lb, lbpos