123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794 |
- """
- Adjust subplot layouts so that there are no overlapping axes or axes
- decorations. All axes decorations are dealt with (labels, ticks, titles,
- ticklabels) and some dependent artists are also dealt with (colorbar,
- suptitle).
- Layout is done via `~matplotlib.gridspec`, with one constraint per gridspec,
- so it is possible to have overlapping axes if the gridspecs overlap (i.e.
- using `~matplotlib.gridspec.GridSpecFromSubplotSpec`). Axes placed using
- ``figure.subplots()`` or ``figure.add_subplots()`` will participate in the
- layout. Axes manually placed via ``figure.add_axes()`` will not.
- See Tutorial: :ref:`constrainedlayout_guide`
- General idea:
- -------------
- First, a figure has a gridspec that divides the figure into nrows and ncols,
- with heights and widths set by ``height_ratios`` and ``width_ratios``,
- often just set to 1 for an equal grid.
- Subplotspecs that are derived from this gridspec can contain either a
- ``SubPanel``, a ``GridSpecFromSubplotSpec``, or an ``Axes``. The ``SubPanel``
- and ``GridSpecFromSubplotSpec`` are dealt with recursively and each contain an
- analogous layout.
- Each ``GridSpec`` has a ``_layoutgrid`` attached to it. The ``_layoutgrid``
- has the same logical layout as the ``GridSpec``. Each row of the grid spec
- has a top and bottom "margin" and each column has a left and right "margin".
- The "inner" height of each row is constrained to be the same (or as modified
- by ``height_ratio``), and the "inner" width of each column is
- constrained to be the same (as modified by ``width_ratio``), where "inner"
- is the width or height of each column/row minus the size of the margins.
- Then the size of the margins for each row and column are determined as the
- max width of the decorators on each axes that has decorators in that margin.
- For instance, a normal axes would have a left margin that includes the
- left ticklabels, and the ylabel if it exists. The right margin may include a
- colorbar, the bottom margin the xaxis decorations, and the top margin the
- title.
- With these constraints, the solver then finds appropriate bounds for the
- columns and rows. It's possible that the margins take up the whole figure,
- in which case the algorithm is not applied and a warning is raised.
- See the tutorial :ref:`constrainedlayout_guide`
- for more discussion of the algorithm with examples.
- """
- import logging
- import numpy as np
- from matplotlib import _api, artist as martist
- import matplotlib.transforms as mtransforms
- import matplotlib._layoutgrid as mlayoutgrid
- _log = logging.getLogger(__name__)
- ######################################################
- def do_constrained_layout(fig, h_pad, w_pad,
- hspace=None, wspace=None, rect=(0, 0, 1, 1),
- compress=False):
- """
- Do the constrained_layout. Called at draw time in
- ``figure.constrained_layout()``
- Parameters
- ----------
- fig : `~matplotlib.figure.Figure`
- `.Figure` instance to do the layout in.
- h_pad, w_pad : float
- Padding around the axes elements in figure-normalized units.
- hspace, wspace : float
- Fraction of the figure to dedicate to space between the
- axes. These are evenly spread between the gaps between the axes.
- A value of 0.2 for a three-column layout would have a space
- of 0.1 of the figure width between each column.
- If h/wspace < h/w_pad, then the pads are used instead.
- rect : tuple of 4 floats
- Rectangle in figure coordinates to perform constrained layout in
- [left, bottom, width, height], each from 0-1.
- compress : bool
- Whether to shift Axes so that white space in between them is
- removed. This is useful for simple grids of fixed-aspect Axes (e.g.
- a grid of images).
- Returns
- -------
- layoutgrid : private debugging structure
- """
- renderer = fig._get_renderer()
- # make layoutgrid tree...
- layoutgrids = make_layoutgrids(fig, None, rect=rect)
- if not layoutgrids['hasgrids']:
- _api.warn_external('There are no gridspecs with layoutgrids. '
- 'Possibly did not call parent GridSpec with the'
- ' "figure" keyword')
- return
- for _ in range(2):
- # do the algorithm twice. This has to be done because decorations
- # change size after the first re-position (i.e. x/yticklabels get
- # larger/smaller). This second reposition tends to be much milder,
- # so doing twice makes things work OK.
- # make margins for all the axes and subfigures in the
- # figure. Add margins for colorbars...
- make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad,
- w_pad=w_pad, hspace=hspace, wspace=wspace)
- make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad,
- w_pad=w_pad)
- # if a layout is such that a columns (or rows) margin has no
- # constraints, we need to make all such instances in the grid
- # match in margin size.
- match_submerged_margins(layoutgrids, fig)
- # update all the variables in the layout.
- layoutgrids[fig].update_variables()
- warn_collapsed = ('constrained_layout not applied because '
- 'axes sizes collapsed to zero. Try making '
- 'figure larger or axes decorations smaller.')
- if check_no_collapsed_axes(layoutgrids, fig):
- reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
- w_pad=w_pad, hspace=hspace, wspace=wspace)
- if compress:
- layoutgrids = compress_fixed_aspect(layoutgrids, fig)
- layoutgrids[fig].update_variables()
- if check_no_collapsed_axes(layoutgrids, fig):
- reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
- w_pad=w_pad, hspace=hspace, wspace=wspace)
- else:
- _api.warn_external(warn_collapsed)
- else:
- _api.warn_external(warn_collapsed)
- reset_margins(layoutgrids, fig)
- return layoutgrids
- def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)):
- """
- Make the layoutgrid tree.
- (Sub)Figures get a layoutgrid so we can have figure margins.
- Gridspecs that are attached to axes get a layoutgrid so axes
- can have margins.
- """
- if layoutgrids is None:
- layoutgrids = dict()
- layoutgrids['hasgrids'] = False
- if not hasattr(fig, '_parent'):
- # top figure; pass rect as parent to allow user-specified
- # margins
- layoutgrids[fig] = mlayoutgrid.LayoutGrid(parent=rect, name='figlb')
- else:
- # subfigure
- gs = fig._subplotspec.get_gridspec()
- # it is possible the gridspec containing this subfigure hasn't
- # been added to the tree yet:
- layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
- # add the layoutgrid for the subfigure:
- parentlb = layoutgrids[gs]
- layoutgrids[fig] = mlayoutgrid.LayoutGrid(
- parent=parentlb,
- name='panellb',
- parent_inner=True,
- nrows=1, ncols=1,
- parent_pos=(fig._subplotspec.rowspan,
- fig._subplotspec.colspan))
- # recursively do all subfigures in this figure...
- for sfig in fig.subfigs:
- layoutgrids = make_layoutgrids(sfig, layoutgrids)
- # for each axes at the local level add its gridspec:
- for ax in fig._localaxes:
- gs = ax.get_gridspec()
- if gs is not None:
- layoutgrids = make_layoutgrids_gs(layoutgrids, gs)
- return layoutgrids
- def make_layoutgrids_gs(layoutgrids, gs):
- """
- Make the layoutgrid for a gridspec (and anything nested in the gridspec)
- """
- if gs in layoutgrids or gs.figure is None:
- return layoutgrids
- # in order to do constrained_layout there has to be at least *one*
- # gridspec in the tree:
- layoutgrids['hasgrids'] = True
- if not hasattr(gs, '_subplot_spec'):
- # normal gridspec
- parent = layoutgrids[gs.figure]
- layoutgrids[gs] = mlayoutgrid.LayoutGrid(
- parent=parent,
- parent_inner=True,
- name='gridspec',
- ncols=gs._ncols, nrows=gs._nrows,
- width_ratios=gs.get_width_ratios(),
- height_ratios=gs.get_height_ratios())
- else:
- # this is a gridspecfromsubplotspec:
- subplot_spec = gs._subplot_spec
- parentgs = subplot_spec.get_gridspec()
- # if a nested gridspec it is possible the parent is not in there yet:
- if parentgs not in layoutgrids:
- layoutgrids = make_layoutgrids_gs(layoutgrids, parentgs)
- subspeclb = layoutgrids[parentgs]
- # gridspecfromsubplotspec need an outer container:
- # get a unique representation:
- rep = (gs, 'top')
- if rep not in layoutgrids:
- layoutgrids[rep] = mlayoutgrid.LayoutGrid(
- parent=subspeclb,
- name='top',
- nrows=1, ncols=1,
- parent_pos=(subplot_spec.rowspan, subplot_spec.colspan))
- layoutgrids[gs] = mlayoutgrid.LayoutGrid(
- parent=layoutgrids[rep],
- name='gridspec',
- nrows=gs._nrows, ncols=gs._ncols,
- width_ratios=gs.get_width_ratios(),
- height_ratios=gs.get_height_ratios())
- return layoutgrids
- def check_no_collapsed_axes(layoutgrids, fig):
- """
- Check that no axes have collapsed to zero size.
- """
- for sfig in fig.subfigs:
- ok = check_no_collapsed_axes(layoutgrids, sfig)
- if not ok:
- return False
- for ax in fig.axes:
- gs = ax.get_gridspec()
- if gs in layoutgrids: # also implies gs is not None.
- lg = layoutgrids[gs]
- for i in range(gs.nrows):
- for j in range(gs.ncols):
- bb = lg.get_inner_bbox(i, j)
- if bb.width <= 0 or bb.height <= 0:
- return False
- return True
- def compress_fixed_aspect(layoutgrids, fig):
- gs = None
- for ax in fig.axes:
- if ax.get_subplotspec() is None:
- continue
- ax.apply_aspect()
- sub = ax.get_subplotspec()
- _gs = sub.get_gridspec()
- if gs is None:
- gs = _gs
- extraw = np.zeros(gs.ncols)
- extrah = np.zeros(gs.nrows)
- elif _gs != gs:
- raise ValueError('Cannot do compressed layout if axes are not'
- 'all from the same gridspec')
- orig = ax.get_position(original=True)
- actual = ax.get_position(original=False)
- dw = orig.width - actual.width
- if dw > 0:
- extraw[sub.colspan] = np.maximum(extraw[sub.colspan], dw)
- dh = orig.height - actual.height
- if dh > 0:
- extrah[sub.rowspan] = np.maximum(extrah[sub.rowspan], dh)
- if gs is None:
- raise ValueError('Cannot do compressed layout if no axes '
- 'are part of a gridspec.')
- w = np.sum(extraw) / 2
- layoutgrids[fig].edit_margin_min('left', w)
- layoutgrids[fig].edit_margin_min('right', w)
- h = np.sum(extrah) / 2
- layoutgrids[fig].edit_margin_min('top', h)
- layoutgrids[fig].edit_margin_min('bottom', h)
- return layoutgrids
- def get_margin_from_padding(obj, *, w_pad=0, h_pad=0,
- hspace=0, wspace=0):
- ss = obj._subplotspec
- gs = ss.get_gridspec()
- if hasattr(gs, 'hspace'):
- _hspace = (gs.hspace if gs.hspace is not None else hspace)
- _wspace = (gs.wspace if gs.wspace is not None else wspace)
- else:
- _hspace = (gs._hspace if gs._hspace is not None else hspace)
- _wspace = (gs._wspace if gs._wspace is not None else wspace)
- _wspace = _wspace / 2
- _hspace = _hspace / 2
- nrows, ncols = gs.get_geometry()
- # there are two margins for each direction. The "cb"
- # margins are for pads and colorbars, the non-"cb" are
- # for the axes decorations (labels etc).
- margin = {'leftcb': w_pad, 'rightcb': w_pad,
- 'bottomcb': h_pad, 'topcb': h_pad,
- 'left': 0, 'right': 0,
- 'top': 0, 'bottom': 0}
- if _wspace / ncols > w_pad:
- if ss.colspan.start > 0:
- margin['leftcb'] = _wspace / ncols
- if ss.colspan.stop < ncols:
- margin['rightcb'] = _wspace / ncols
- if _hspace / nrows > h_pad:
- if ss.rowspan.stop < nrows:
- margin['bottomcb'] = _hspace / nrows
- if ss.rowspan.start > 0:
- margin['topcb'] = _hspace / nrows
- return margin
- def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0,
- hspace=0, wspace=0):
- """
- For each axes, make a margin between the *pos* layoutbox and the
- *axes* layoutbox be a minimum size that can accommodate the
- decorations on the axis.
- Then make room for colorbars.
- Parameters
- ----------
- layoutgrids : dict
- fig : `~matplotlib.figure.Figure`
- `.Figure` instance to do the layout in.
- renderer : `~matplotlib.backend_bases.RendererBase` subclass.
- The renderer to use.
- w_pad, h_pad : float, default: 0
- Width and height padding (in fraction of figure).
- hspace, wspace : float, default: 0
- Width and height padding as fraction of figure size divided by
- number of columns or rows.
- """
- for sfig in fig.subfigs: # recursively make child panel margins
- ss = sfig._subplotspec
- gs = ss.get_gridspec()
- make_layout_margins(layoutgrids, sfig, renderer,
- w_pad=w_pad, h_pad=h_pad,
- hspace=hspace, wspace=wspace)
- margins = get_margin_from_padding(sfig, w_pad=0, h_pad=0,
- hspace=hspace, wspace=wspace)
- layoutgrids[gs].edit_outer_margin_mins(margins, ss)
- for ax in fig._localaxes:
- if not ax.get_subplotspec() or not ax.get_in_layout():
- continue
- ss = ax.get_subplotspec()
- gs = ss.get_gridspec()
- if gs not in layoutgrids:
- return
- margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad,
- hspace=hspace, wspace=wspace)
- pos, bbox = get_pos_and_bbox(ax, renderer)
- # the margin is the distance between the bounding box of the axes
- # and its position (plus the padding from above)
- margin['left'] += pos.x0 - bbox.x0
- margin['right'] += bbox.x1 - pos.x1
- # remember that rows are ordered from top:
- margin['bottom'] += pos.y0 - bbox.y0
- margin['top'] += bbox.y1 - pos.y1
- # make margin for colorbars. These margins go in the
- # padding margin, versus the margin for axes decorators.
- for cbax in ax._colorbars:
- # note pad is a fraction of the parent width...
- pad = colorbar_get_pad(layoutgrids, cbax)
- # colorbars can be child of more than one subplot spec:
- cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax)
- loc = cbax._colorbar_info['location']
- cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
- if loc == 'right':
- if cbp_cspan.stop == ss.colspan.stop:
- # only increase if the colorbar is on the right edge
- margin['rightcb'] += cbbbox.width + pad
- elif loc == 'left':
- if cbp_cspan.start == ss.colspan.start:
- # only increase if the colorbar is on the left edge
- margin['leftcb'] += cbbbox.width + pad
- elif loc == 'top':
- if cbp_rspan.start == ss.rowspan.start:
- margin['topcb'] += cbbbox.height + pad
- else:
- if cbp_rspan.stop == ss.rowspan.stop:
- margin['bottomcb'] += cbbbox.height + pad
- # If the colorbars are wider than the parent box in the
- # cross direction
- if loc in ['top', 'bottom']:
- if (cbp_cspan.start == ss.colspan.start and
- cbbbox.x0 < bbox.x0):
- margin['left'] += bbox.x0 - cbbbox.x0
- if (cbp_cspan.stop == ss.colspan.stop and
- cbbbox.x1 > bbox.x1):
- margin['right'] += cbbbox.x1 - bbox.x1
- # or taller:
- if loc in ['left', 'right']:
- if (cbp_rspan.stop == ss.rowspan.stop and
- cbbbox.y0 < bbox.y0):
- margin['bottom'] += bbox.y0 - cbbbox.y0
- if (cbp_rspan.start == ss.rowspan.start and
- cbbbox.y1 > bbox.y1):
- margin['top'] += cbbbox.y1 - bbox.y1
- # pass the new margins down to the layout grid for the solution...
- layoutgrids[gs].edit_outer_margin_mins(margin, ss)
- # make margins for figure-level legends:
- for leg in fig.legends:
- inv_trans_fig = None
- if leg._outside_loc and leg._bbox_to_anchor is None:
- if inv_trans_fig is None:
- inv_trans_fig = fig.transFigure.inverted().transform_bbox
- bbox = inv_trans_fig(leg.get_tightbbox(renderer))
- w = bbox.width + 2 * w_pad
- h = bbox.height + 2 * h_pad
- legendloc = leg._outside_loc
- if legendloc == 'lower':
- layoutgrids[fig].edit_margin_min('bottom', h)
- elif legendloc == 'upper':
- layoutgrids[fig].edit_margin_min('top', h)
- if legendloc == 'right':
- layoutgrids[fig].edit_margin_min('right', w)
- elif legendloc == 'left':
- layoutgrids[fig].edit_margin_min('left', w)
- def make_margin_suptitles(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0):
- # Figure out how large the suptitle is and make the
- # top level figure margin larger.
- inv_trans_fig = fig.transFigure.inverted().transform_bbox
- # get the h_pad and w_pad as distances in the local subfigure coordinates:
- padbox = mtransforms.Bbox([[0, 0], [w_pad, h_pad]])
- padbox = (fig.transFigure -
- fig.transSubfigure).transform_bbox(padbox)
- h_pad_local = padbox.height
- w_pad_local = padbox.width
- for sfig in fig.subfigs:
- make_margin_suptitles(layoutgrids, sfig, renderer,
- w_pad=w_pad, h_pad=h_pad)
- if fig._suptitle is not None and fig._suptitle.get_in_layout():
- p = fig._suptitle.get_position()
- if getattr(fig._suptitle, '_autopos', False):
- fig._suptitle.set_position((p[0], 1 - h_pad_local))
- bbox = inv_trans_fig(fig._suptitle.get_tightbbox(renderer))
- layoutgrids[fig].edit_margin_min('top', bbox.height + 2 * h_pad)
- if fig._supxlabel is not None and fig._supxlabel.get_in_layout():
- p = fig._supxlabel.get_position()
- if getattr(fig._supxlabel, '_autopos', False):
- fig._supxlabel.set_position((p[0], h_pad_local))
- bbox = inv_trans_fig(fig._supxlabel.get_tightbbox(renderer))
- layoutgrids[fig].edit_margin_min('bottom',
- bbox.height + 2 * h_pad)
- if fig._supylabel is not None and fig._supylabel.get_in_layout():
- p = fig._supylabel.get_position()
- if getattr(fig._supylabel, '_autopos', False):
- fig._supylabel.set_position((w_pad_local, p[1]))
- bbox = inv_trans_fig(fig._supylabel.get_tightbbox(renderer))
- layoutgrids[fig].edit_margin_min('left', bbox.width + 2 * w_pad)
- def match_submerged_margins(layoutgrids, fig):
- """
- Make the margins that are submerged inside an Axes the same size.
- This allows axes that span two columns (or rows) that are offset
- from one another to have the same size.
- This gives the proper layout for something like::
- fig = plt.figure(constrained_layout=True)
- axs = fig.subplot_mosaic("AAAB\nCCDD")
- Without this routine, the axes D will be wider than C, because the
- margin width between the two columns in C has no width by default,
- whereas the margins between the two columns of D are set by the
- width of the margin between A and B. However, obviously the user would
- like C and D to be the same size, so we need to add constraints to these
- "submerged" margins.
- This routine makes all the interior margins the same, and the spacing
- between the three columns in A and the two column in C are all set to the
- margins between the two columns of D.
- See test_constrained_layout::test_constrained_layout12 for an example.
- """
- for sfig in fig.subfigs:
- match_submerged_margins(layoutgrids, sfig)
- axs = [a for a in fig.get_axes()
- if a.get_subplotspec() is not None and a.get_in_layout()]
- for ax1 in axs:
- ss1 = ax1.get_subplotspec()
- if ss1.get_gridspec() not in layoutgrids:
- axs.remove(ax1)
- continue
- lg1 = layoutgrids[ss1.get_gridspec()]
- # interior columns:
- if len(ss1.colspan) > 1:
- maxsubl = np.max(
- lg1.margin_vals['left'][ss1.colspan[1:]] +
- lg1.margin_vals['leftcb'][ss1.colspan[1:]]
- )
- maxsubr = np.max(
- lg1.margin_vals['right'][ss1.colspan[:-1]] +
- lg1.margin_vals['rightcb'][ss1.colspan[:-1]]
- )
- for ax2 in axs:
- ss2 = ax2.get_subplotspec()
- lg2 = layoutgrids[ss2.get_gridspec()]
- if lg2 is not None and len(ss2.colspan) > 1:
- maxsubl2 = np.max(
- lg2.margin_vals['left'][ss2.colspan[1:]] +
- lg2.margin_vals['leftcb'][ss2.colspan[1:]])
- if maxsubl2 > maxsubl:
- maxsubl = maxsubl2
- maxsubr2 = np.max(
- lg2.margin_vals['right'][ss2.colspan[:-1]] +
- lg2.margin_vals['rightcb'][ss2.colspan[:-1]])
- if maxsubr2 > maxsubr:
- maxsubr = maxsubr2
- for i in ss1.colspan[1:]:
- lg1.edit_margin_min('left', maxsubl, cell=i)
- for i in ss1.colspan[:-1]:
- lg1.edit_margin_min('right', maxsubr, cell=i)
- # interior rows:
- if len(ss1.rowspan) > 1:
- maxsubt = np.max(
- lg1.margin_vals['top'][ss1.rowspan[1:]] +
- lg1.margin_vals['topcb'][ss1.rowspan[1:]]
- )
- maxsubb = np.max(
- lg1.margin_vals['bottom'][ss1.rowspan[:-1]] +
- lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]]
- )
- for ax2 in axs:
- ss2 = ax2.get_subplotspec()
- lg2 = layoutgrids[ss2.get_gridspec()]
- if lg2 is not None:
- if len(ss2.rowspan) > 1:
- maxsubt = np.max([np.max(
- lg2.margin_vals['top'][ss2.rowspan[1:]] +
- lg2.margin_vals['topcb'][ss2.rowspan[1:]]
- ), maxsubt])
- maxsubb = np.max([np.max(
- lg2.margin_vals['bottom'][ss2.rowspan[:-1]] +
- lg2.margin_vals['bottomcb'][ss2.rowspan[:-1]]
- ), maxsubb])
- for i in ss1.rowspan[1:]:
- lg1.edit_margin_min('top', maxsubt, cell=i)
- for i in ss1.rowspan[:-1]:
- lg1.edit_margin_min('bottom', maxsubb, cell=i)
- def get_cb_parent_spans(cbax):
- """
- Figure out which subplotspecs this colorbar belongs to.
- Parameters
- ----------
- cbax : `~matplotlib.axes.Axes`
- Axes for the colorbar.
- """
- rowstart = np.inf
- rowstop = -np.inf
- colstart = np.inf
- colstop = -np.inf
- for parent in cbax._colorbar_info['parents']:
- ss = parent.get_subplotspec()
- rowstart = min(ss.rowspan.start, rowstart)
- rowstop = max(ss.rowspan.stop, rowstop)
- colstart = min(ss.colspan.start, colstart)
- colstop = max(ss.colspan.stop, colstop)
- rowspan = range(rowstart, rowstop)
- colspan = range(colstart, colstop)
- return rowspan, colspan
- def get_pos_and_bbox(ax, renderer):
- """
- Get the position and the bbox for the axes.
- Parameters
- ----------
- ax : `~matplotlib.axes.Axes`
- renderer : `~matplotlib.backend_bases.RendererBase` subclass.
- Returns
- -------
- pos : `~matplotlib.transforms.Bbox`
- Position in figure coordinates.
- bbox : `~matplotlib.transforms.Bbox`
- Tight bounding box in figure coordinates.
- """
- fig = ax.figure
- pos = ax.get_position(original=True)
- # pos is in panel co-ords, but we need in figure for the layout
- pos = pos.transformed(fig.transSubfigure - fig.transFigure)
- tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer)
- if tightbbox is None:
- bbox = pos
- else:
- bbox = tightbbox.transformed(fig.transFigure.inverted())
- return pos, bbox
- def reposition_axes(layoutgrids, fig, renderer, *,
- w_pad=0, h_pad=0, hspace=0, wspace=0):
- """
- Reposition all the axes based on the new inner bounding box.
- """
- trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
- for sfig in fig.subfigs:
- bbox = layoutgrids[sfig].get_outer_bbox()
- sfig._redo_transform_rel_fig(
- bbox=bbox.transformed(trans_fig_to_subfig))
- reposition_axes(layoutgrids, sfig, renderer,
- w_pad=w_pad, h_pad=h_pad,
- wspace=wspace, hspace=hspace)
- for ax in fig._localaxes:
- if ax.get_subplotspec() is None or not ax.get_in_layout():
- continue
- # grid bbox is in Figure coordinates, but we specify in panel
- # coordinates...
- ss = ax.get_subplotspec()
- gs = ss.get_gridspec()
- if gs not in layoutgrids:
- return
- bbox = layoutgrids[gs].get_inner_bbox(rows=ss.rowspan,
- cols=ss.colspan)
- # transform from figure to panel for set_position:
- newbbox = trans_fig_to_subfig.transform_bbox(bbox)
- ax._set_position(newbbox)
- # move the colorbars:
- # we need to keep track of oldw and oldh if there is more than
- # one colorbar:
- offset = {'left': 0, 'right': 0, 'bottom': 0, 'top': 0}
- for nn, cbax in enumerate(ax._colorbars[::-1]):
- if ax == cbax._colorbar_info['parents'][0]:
- reposition_colorbar(layoutgrids, cbax, renderer,
- offset=offset)
- def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None):
- """
- Place the colorbar in its new place.
- Parameters
- ----------
- layoutgrids : dict
- cbax : `~matplotlib.axes.Axes`
- Axes for the colorbar.
- renderer : `~matplotlib.backend_bases.RendererBase` subclass.
- The renderer to use.
- offset : array-like
- Offset the colorbar needs to be pushed to in order to
- account for multiple colorbars.
- """
- parents = cbax._colorbar_info['parents']
- gs = parents[0].get_gridspec()
- fig = cbax.figure
- trans_fig_to_subfig = fig.transFigure - fig.transSubfigure
- cb_rspans, cb_cspans = get_cb_parent_spans(cbax)
- bboxparent = layoutgrids[gs].get_bbox_for_cb(rows=cb_rspans,
- cols=cb_cspans)
- pb = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
- location = cbax._colorbar_info['location']
- anchor = cbax._colorbar_info['anchor']
- fraction = cbax._colorbar_info['fraction']
- aspect = cbax._colorbar_info['aspect']
- shrink = cbax._colorbar_info['shrink']
- cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
- # Colorbar gets put at extreme edge of outer bbox of the subplotspec
- # It needs to be moved in by: 1) a pad 2) its "margin" 3) by
- # any colorbars already added at this location:
- cbpad = colorbar_get_pad(layoutgrids, cbax)
- if location in ('left', 'right'):
- # fraction and shrink are fractions of parent
- pbcb = pb.shrunk(fraction, shrink).anchored(anchor, pb)
- # The colorbar is at the left side of the parent. Need
- # to translate to right (or left)
- if location == 'right':
- lmargin = cbpos.x0 - cbbbox.x0
- dx = bboxparent.x1 - pbcb.x0 + offset['right']
- dx += cbpad + lmargin
- offset['right'] += cbbbox.width + cbpad
- pbcb = pbcb.translated(dx, 0)
- else:
- lmargin = cbpos.x0 - cbbbox.x0
- dx = bboxparent.x0 - pbcb.x0 # edge of parent
- dx += -cbbbox.width - cbpad + lmargin - offset['left']
- offset['left'] += cbbbox.width + cbpad
- pbcb = pbcb.translated(dx, 0)
- else: # horizontal axes:
- pbcb = pb.shrunk(shrink, fraction).anchored(anchor, pb)
- if location == 'top':
- bmargin = cbpos.y0 - cbbbox.y0
- dy = bboxparent.y1 - pbcb.y0 + offset['top']
- dy += cbpad + bmargin
- offset['top'] += cbbbox.height + cbpad
- pbcb = pbcb.translated(0, dy)
- else:
- bmargin = cbpos.y0 - cbbbox.y0
- dy = bboxparent.y0 - pbcb.y0
- dy += -cbbbox.height - cbpad + bmargin - offset['bottom']
- offset['bottom'] += cbbbox.height + cbpad
- pbcb = pbcb.translated(0, dy)
- pbcb = trans_fig_to_subfig.transform_bbox(pbcb)
- cbax.set_transform(fig.transSubfigure)
- cbax._set_position(pbcb)
- cbax.set_anchor(anchor)
- if location in ['bottom', 'top']:
- aspect = 1 / aspect
- cbax.set_box_aspect(aspect)
- cbax.set_aspect('auto')
- return offset
- def reset_margins(layoutgrids, fig):
- """
- Reset the margins in the layoutboxes of *fig*.
- Margins are usually set as a minimum, so if the figure gets smaller
- the minimum needs to be zero in order for it to grow again.
- """
- for sfig in fig.subfigs:
- reset_margins(layoutgrids, sfig)
- for ax in fig.axes:
- if ax.get_in_layout():
- gs = ax.get_gridspec()
- if gs in layoutgrids: # also implies gs is not None.
- layoutgrids[gs].reset_margins()
- layoutgrids[fig].reset_margins()
- def colorbar_get_pad(layoutgrids, cax):
- parents = cax._colorbar_info['parents']
- gs = parents[0].get_gridspec()
- cb_rspans, cb_cspans = get_cb_parent_spans(cax)
- bboxouter = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans)
- if cax._colorbar_info['location'] in ['right', 'left']:
- size = bboxouter.width
- else:
- size = bboxouter.height
- return cax._colorbar_info['pad'] * size
|