Spaces:
Running
Running
| """ | |
| 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) | |
| if ((suptitle := fig._suptitle) is not None and | |
| suptitle.get_in_layout() and suptitle._autopos): | |
| x, _ = suptitle.get_position() | |
| suptitle.set_position( | |
| (x, layoutgrids[fig].get_inner_bbox().y1 + h_pad)) | |
| suptitle.set_verticalalignment('bottom') | |
| 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. | |
| """ | |
| axsdone = [] | |
| for sfig in fig.subfigs: | |
| axsdone += 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() and | |
| a not in axsdone)] | |
| 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) | |
| return axs | |
| 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.get_figure(root=False) | |
| 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.get_figure(root=False) | |
| 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 | |