From 8db28aa052552a7145f936eec9ea07b154c0b4e7 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Fri, 22 May 2020 11:48:50 -0700 Subject: [PATCH] Rewrite of constrained layout. Apply suggestions from code review Co-authored-by: Elliott Sales de Andrade --- .../next_api_changes/behavior/17494-JMK.rst | 25 + lib/matplotlib/_constrained_layout.py | 1070 ++++++++--------- lib/matplotlib/_layoutbox.py | 679 ----------- lib/matplotlib/_layoutgrid.py | 560 +++++++++ lib/matplotlib/axes/_base.py | 11 +- lib/matplotlib/axes/_secondary_axes.py | 2 - lib/matplotlib/axes/_subplots.py | 22 - lib/matplotlib/colorbar.py | 58 +- lib/matplotlib/figure.py | 66 +- lib/matplotlib/gridspec.py | 67 +- .../test_axes/secondary_xy.png | Bin 42023 -> 48775 bytes .../constrained_layout1.png | Bin 23743 -> 23698 bytes .../constrained_layout10.png | Bin 35847 -> 36013 bytes .../constrained_layout11.png | Bin 42649 -> 42537 bytes .../constrained_layout11rat.png | Bin 38505 -> 38796 bytes .../constrained_layout12.png | Bin 44660 -> 40909 bytes .../constrained_layout13.png | Bin 35515 -> 35619 bytes .../constrained_layout14.png | Bin 40330 -> 40908 bytes .../constrained_layout15.png | Bin 25452 -> 27748 bytes .../constrained_layout16.png | Bin 23120 -> 23123 bytes .../constrained_layout17.png | Bin 32858 -> 34175 bytes .../constrained_layout2.png | Bin 34397 -> 34454 bytes .../constrained_layout3.png | Bin 52171 -> 50973 bytes .../constrained_layout4.pdf | Bin 16636 -> 0 bytes .../constrained_layout4.png | Bin 41172 -> 40954 bytes .../constrained_layout5.png | Bin 35858 -> 36077 bytes .../constrained_layout6.png | Bin 38503 -> 39012 bytes .../constrained_layout8.png | Bin 35732 -> 35884 bytes .../constrained_layout9.png | Bin 34984 -> 35074 bytes .../test_colorbar_location.png | Bin 13713 -> 14984 bytes .../test_colorbars_no_overlapH.png | Bin 0 -> 4519 bytes .../test_colorbars_no_overlapV.png | Bin 0 -> 7189 bytes .../test_text/large_subscript_title.png | Bin 11673 -> 11552 bytes .../tests/test_constrainedlayout.py | 86 +- lib/mpl_toolkits/axes_grid1/colorbar.py | 6 +- .../intermediate/constrainedlayout_guide.py | 369 ++---- 36 files changed, 1365 insertions(+), 1656 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/17494-JMK.rst delete mode 100644 lib/matplotlib/_layoutbox.py create mode 100644 lib/matplotlib/_layoutgrid.py delete mode 100644 lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.pdf create mode 100644 lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbars_no_overlapH.png create mode 100644 lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbars_no_overlapV.png diff --git a/doc/api/next_api_changes/behavior/17494-JMK.rst b/doc/api/next_api_changes/behavior/17494-JMK.rst new file mode 100644 index 000000000000..2833ff3dcdbf --- /dev/null +++ b/doc/api/next_api_changes/behavior/17494-JMK.rst @@ -0,0 +1,25 @@ +Constrained layout rewrite +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The layout manager ``constrained_layout`` was re-written with different +outer constraints that should be more robust to complicated subplot layouts. +User-facing changes are: + +- some poorly constrained layouts will have different width/height plots than + before. +- colorbars now respect the ``anchor`` keyword argument of + `matplotlib.colorbar.make_axes` +- colorbars are wider. +- colorbars in different rows or columns line up more robustly. +- *hspace* and *wspace* options to `.Figure.set_constrained_layout_pads` + were twice as wide as the docs said they should be. So these now follow + the docs. + +This feature will remain "experimental" until the new changes have been +used enough by users, so we anticipate version 3.5 or 3.6. On the other hand, +``constrained_layout`` is extensively tested and used in examples in the +library, so using it should be safe, but layouts may not be exactly the same +as more development takes place. + +Details of using ``constrained_layout``, and its algorithm are available at +:doc:`/tutorials/intermediate/constrainedlayout_guide` diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index dc00b6c50e05..fedb69c3d91c 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -1,8 +1,8 @@ """ 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, -legend). +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. @@ -13,58 +13,49 @@ See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide` """ -# Development Notes: - -# What gets a layoutbox: -# - figure -# - gridspec -# - subplotspec -# EITHER: -# - axes + pos for the axes (i.e. the total area taken by axis and -# the actual "position" argument that needs to be sent to -# ax.set_position.) -# - The axes layout box will also encompass the legend, and that is -# how legends get included (axes legends, not figure legends) -# - colorbars are siblings of the axes if they are single-axes -# colorbars -# OR: -# - a gridspec can be inside a subplotspec. -# - subplotspec -# EITHER: -# - axes... -# OR: -# - gridspec... with arbitrary nesting... -# - colorbars are siblings of the subplotspecs if they are multi-axes -# colorbars. -# - suptitle: -# - right now suptitles are just stacked atop everything else in figure. -# Could imagine suptitles being gridspec suptitles, but not implemented -# -# Todo: AnchoredOffsetbox connected to gridspecs or axes. This would -# be more general way to add extra-axes annotations. - import logging import numpy as np import matplotlib.cbook as cbook -import matplotlib._layoutbox as layoutbox _log = logging.getLogger(__name__) - -def _spans_overlap(span0, span1): - return span0.start in span1 or span1.start in span0 - - -def _axes_all_finite_sized(fig): - """Return whether all axes in the figure have a finite width and height.""" - for ax in fig.axes: - if ax._layoutbox is not None: - newpos = ax._poslayoutbox.get_rect() - if newpos[2] <= 0 or newpos[3] <= 0: - return False - return True +""" +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 +analagous 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 doc:`/tutorials/intermediate/constrainedlayout_guide` +for more discussion of the algorithm with examples. +""" ###################################################### @@ -82,181 +73,362 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, renderer : Renderer Renderer to use. - h_pad, w_pad : float - Padding around the axes elements in figure-normalized units. - - hspace, wspace : float - Spacing in fractions of the subplot sizes. + 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. """ - # Steps: - # - # 1. get a list of unique gridspecs in this figure. Each gridspec will be - # constrained separately. - # 2. Check for gaps in the gridspecs. i.e. if not every axes slot in the - # gridspec has been filled. If empty, add a ghost axis that is made so - # that it cannot be seen (though visible=True). This is needed to make - # a blank spot in the layout. - # 3. Compare the tight_bbox of each axes to its `position`, and assume that - # the difference is the space needed by the elements around the edge of - # the axes (decorations) like the title, ticklabels, x-labels, etc. This - # can include legends who overspill the axes boundaries. - # 4. Constrain gridspec elements to line up: - # a) if colnum0 != colnumC, the two subplotspecs are stacked next to - # each other, with the appropriate order. - # b) if colnum0 == colnumC, line up the left or right side of the - # _poslayoutbox (depending if it is the min or max num that is equal). - # c) do the same for rows... - # 5. The above doesn't constrain relative sizes of the _poslayoutboxes - # at all, and indeed zero-size is a solution that the solver often finds - # more convenient than expanding the sizes. Right now the solution is to - # compare subplotspec sizes (i.e. drowsC and drows0) and constrain the - # larger _poslayoutbox to be larger than the ratio of the sizes. i.e. if - # drows0 > drowsC, then ax._poslayoutbox > axc._poslayoutbox*drowsC/drows0. - # This works fine *if* the decorations are similar between the axes. - # If the larger subplotspec has much larger axes decorations, then the - # constraint above is incorrect. - # - # We need the greater than in the above, in general, rather than an equals - # sign. Consider the case of the left column having 2 rows, and the right - # column having 1 row. We want the top and bottom of the _poslayoutboxes - # to line up. So that means if there are decorations on the left column - # axes they will be smaller than half as large as the right hand axis. - # - # This can break down if the decoration size for the right hand axis (the - # margins) is very large. There must be a math way to check for this case. - - invTransFig = fig.transFigure.inverted().transform_bbox - # list of unique gridspecs that contain child axes: gss = set() for ax in fig.axes: if hasattr(ax, 'get_subplotspec'): gs = ax.get_subplotspec().get_gridspec() - if gs._layoutbox is not None: + if gs._layoutgrid is not None: gss.add(gs) + gss = list(gss) if len(gss) == 0: - cbook._warn_external('There are no gridspecs with layoutboxes. ' + cbook._warn_external('There are no gridspecs with layoutgrids. ' 'Possibly did not call parent GridSpec with the' - ' figure= keyword') - - if fig._layoutbox.constrained_layout_called < 1: - for gs in gss: - # fill in any empty gridspec slots w/ ghost axes... - _make_ghost_gridspec_slots(fig, gs) + ' "figure" keyword') for _ in range(2): - # do the algorithm twice. This has to be done because decorators + # 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. - for ax in fig.axes: - _log.debug(ax._layoutbox) - if ax._layoutbox is not None: - # make margins for each layout box based on the size of - # the decorators. - _make_layout_margins(ax, renderer, h_pad, w_pad) - - # do layout for suptitle. - suptitle = fig._suptitle - do_suptitle = (suptitle is not None and - suptitle._layoutbox is not None and - suptitle.get_in_layout()) - if do_suptitle: - bbox = invTransFig( - suptitle.get_window_extent(renderer=renderer)) - height = bbox.height - if np.isfinite(height): - # reserve at top of figure include an h_pad above and below - suptitle._layoutbox.edit_height(height + h_pad * 2) - - # OK, the above lines up ax._poslayoutbox with ax._layoutbox - # now we need to - # 1) arrange the subplotspecs. We do it at this level because - # the subplotspecs are meant to contain other dependent axes - # like colorbars or legends. - # 2) line up the right and left side of the ax._poslayoutbox - # that have the same subplotspec maxes. - - if fig._layoutbox.constrained_layout_called < 1: - # arrange the subplotspecs... This is all done relative to each - # other. Some subplotspecs contain axes, and others contain - # gridspecs the ones that contain gridspecs are a set proportion - # of their parent gridspec. The ones that contain axes are - # not so constrained. - figlb = fig._layoutbox - for child in figlb.children: - if child._is_gridspec_layoutbox(): - # This routine makes all the subplot spec containers - # have the correct arrangement. It just stacks the - # subplot layoutboxes in the correct order... - _arrange_subplotspecs(child, hspace=hspace, wspace=wspace) - - for gs in gss: - _align_spines(fig, gs) - - fig._layoutbox.constrained_layout_called += 1 - fig._layoutbox.update_variables() - - # check if any axes collapsed to zero. If not, don't change positions: - if _axes_all_finite_sized(fig): - # Now set the position of the axes... - for ax in fig.axes: - if ax._layoutbox is not None: - newpos = ax._poslayoutbox.get_rect() - # Now set the new position. - # ax.set_position will zero out the layout for - # this axis, allowing users to hard-code the position, - # so this does the same w/o zeroing layout. - ax._set_position(newpos, which='original') - if do_suptitle: - newpos = suptitle._layoutbox.get_rect() - suptitle.set_y(1.0 - h_pad) - else: - if suptitle is not None and suptitle._layoutbox is not None: - suptitle._layoutbox.edit_height(0) + + # make margins for all the axes and subpanels in the + # figure. Add margins for colorbars... + _make_layout_margins(fig, renderer, h_pad=h_pad, w_pad=w_pad, + hspace=hspace, wspace=wspace) + + _make_margin_suptitles(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(fig) + + # update all the variables in the layout. + fig._layoutgrid.update_variables() + + if _check_no_collapsed_axes(fig): + _reposition_axes(fig, renderer, h_pad=h_pad, w_pad=w_pad, + hspace=hspace, wspace=wspace) else: - cbook._warn_external('constrained_layout not applied. At least ' - 'one axes collapsed to zero width or height.') + cbook._warn_external('constrained_layout not applied because ' + 'axes sizes collapsed to zero. Try making ' + 'figure larger or axes decorations smaller.') + _reset_margins(fig) -def _make_ghost_gridspec_slots(fig, gs): +def _check_no_collapsed_axes(fig): """ - Check for unoccupied gridspec slots and make ghost axes for these - slots... Do for each gs separately. This is a pretty big kludge - but shouldn't have too much ill effect. The worst is that - someone querying the figure will wonder why there are more - axes than they thought. + Check that no axes have collapsed to zero size. """ - nrows, ncols = gs.get_geometry() - hassubplotspec = np.zeros(nrows * ncols, dtype=bool) - axs = [] + for panel in fig.panels: + ok = _check_no_collapsed_axes(panel) + if not ok: + return False + for ax in fig.axes: - if (hasattr(ax, 'get_subplotspec') - and ax._layoutbox is not None - and ax.get_subplotspec().get_gridspec() == gs): - axs += [ax] - for ax in axs: - ss0 = ax.get_subplotspec() - hassubplotspec[ss0.num1:(ss0.num2 + 1)] = True - for nn, hss in enumerate(hassubplotspec): - if not hss: - # this gridspec slot doesn't have an axis so we - # make a "ghost". - ax = fig.add_subplot(gs[nn]) - ax.set_visible(False) - - -def _make_layout_margins(ax, renderer, h_pad, w_pad): + if hasattr(ax, 'get_subplotspec'): + gs = ax.get_subplotspec().get_gridspec() + lg = gs._layoutgrid + if lg is not None: + 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 _get_margin_from_padding(object, *, w_pad=0, h_pad=0, + hspace=0, wspace=0): + + ss = object._subplotspec + gs = ss.get_gridspec() + lg = gs._layoutgrid + + 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(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. + """ + for panel in fig.panels: # recursively make child panel margins + ss = panel._subplotspec + _make_layout_margins(panel, renderer, w_pad=w_pad, h_pad=h_pad, + hspace=hspace, wspace=wspace) + + margins = _get_margin_from_padding(panel, w_pad=0, h_pad=0, + hspace=hspace, wspace=wspace) + panel._layoutgrid.parent.edit_outer_margin_mins(margins, ss) + + # for ax in [a for a in fig._localaxes if hasattr(a, 'get_subplotspec')]: + for ax in fig.get_axes(): + if not hasattr(ax, 'get_subplotspec'): + continue + + ss = ax.get_subplotspec() + gs = ss.get_gridspec() + nrows, ncols = gs.get_geometry() + + if gs._layoutgrid is None: + return + + margin = _get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad, + hspace=hspace, wspace=wspace) + margin0 = margin.copy() + 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(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... + gs._layoutgrid.edit_outer_margin_mins(margin, ss) + + +def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0): + # Figure out how large the suptitle is and make the + # top level figure margin larger. + for panel in fig.panels: + _make_margin_suptitles(panel, renderer, w_pad=w_pad, h_pad=h_pad) + invTransFig = fig.transPanel.inverted().transform_bbox + + w_pad, h_pad = (fig.transPanel - fig.transFigure).transform((w_pad, h_pad)) + + if fig._suptitle is not None and fig._suptitle.get_in_layout(): + bbox = invTransFig(fig._suptitle.get_tightbbox(renderer)) + p = fig._suptitle.get_position() + fig._suptitle.set_position((p[0], 1-h_pad)) + fig._layoutgrid.edit_margin_min('top', bbox.height + 2 * h_pad) + + +def _match_submerged_margins(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 panel in fig.panels: + _match_submerged_margins(panel) + + axs = [a for a in fig.get_axes() if hasattr(a, 'get_subplotspec')] + + for ax1 in axs: + ss1 = ax1.get_subplotspec() + lg1 = ss1.get_gridspec()._layoutgrid + if lg1 is None: + axs.remove(ax1) + continue + + # 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 = ss2.get_gridspec()._layoutgrid + 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 = ss2.get_gridspec()._layoutgrid + 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: + """ + 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 + renderer + + Returns + ------- + pos : Bbox + Position in figure coordinates. + bbox : Bbox + Tight bounding box in figure coordinates. + """ fig = ax.figure - invTransFig = fig.transFigure.inverted().transform_bbox pos = ax.get_position(original=True) + # pos is in panel co-ords, but we need in figure for the layout + pos = pos.transformed(fig.transPanel - fig.transFigure) try: tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True) except TypeError: @@ -265,393 +437,165 @@ def _make_layout_margins(ax, renderer, h_pad, w_pad): if tightbbox is None: bbox = pos else: - bbox = invTransFig(tightbbox) - - # this can go wrong: - if not (np.isfinite(bbox.width) and np.isfinite(bbox.height)): - # just abort, this is likely a bad set of coordinates that - # is transitory... - return - # use stored h_pad if it exists - h_padt = ax._poslayoutbox.h_pad - if h_padt is None: - h_padt = h_pad - w_padt = ax._poslayoutbox.w_pad - if w_padt is None: - w_padt = w_pad - ax._poslayoutbox.edit_left_margin_min(-bbox.x0 + pos.x0 + w_padt) - ax._poslayoutbox.edit_right_margin_min(bbox.x1 - pos.x1 + w_padt) - ax._poslayoutbox.edit_bottom_margin_min(-bbox.y0 + pos.y0 + h_padt) - ax._poslayoutbox.edit_top_margin_min(bbox.y1-pos.y1+h_padt) - _log.debug('left %f', (-bbox.x0 + pos.x0 + w_pad)) - _log.debug('right %f', (bbox.x1 - pos.x1 + w_pad)) - _log.debug('bottom %f', (-bbox.y0 + pos.y0 + h_padt)) - _log.debug('bbox.y0 %f', bbox.y0) - _log.debug('pos.y0 %f', pos.y0) - # Sometimes its possible for the solver to collapse - # rather than expand axes, so they all have zero height - # or width. This stops that... It *should* have been - # taken into account w/ pref_width... - if fig._layoutbox.constrained_layout_called < 1: - ax._poslayoutbox.constrain_height_min(20, strength='weak') - ax._poslayoutbox.constrain_width_min(20, strength='weak') - ax._layoutbox.constrain_height_min(20, strength='weak') - ax._layoutbox.constrain_width_min(20, strength='weak') - ax._poslayoutbox.constrain_top_margin(0, strength='weak') - ax._poslayoutbox.constrain_bottom_margin(0, strength='weak') - ax._poslayoutbox.constrain_right_margin(0, strength='weak') - ax._poslayoutbox.constrain_left_margin(0, strength='weak') - - -def _align_spines(fig, gs): + bbox = tightbbox.transformed(fig.transFigure.inverted()) + return pos, bbox + + +def _reposition_axes(fig, renderer, *, w_pad=0, h_pad=0, hspace=0, wspace=0): """ - - Align right/left and bottom/top spines of appropriate subplots. - - Compare size of subplotspec including height and width ratios - and make sure that the axes spines are at least as large - as they should be. + Reposition all the axes based on the new inner bounding box. """ - # for each gridspec... - nrows, ncols = gs.get_geometry() - width_ratios = gs.get_width_ratios() - height_ratios = gs.get_height_ratios() - - # get axes in this gridspec.... - axs = [ax for ax in fig.axes - if (hasattr(ax, 'get_subplotspec') - and ax._layoutbox is not None - and ax.get_subplotspec().get_gridspec() == gs)] - rowspans = [] - colspans = [] - heights = [] - widths = [] - - for ax in axs: - ss0 = ax.get_subplotspec() - rowspan = ss0.rowspan - colspan = ss0.colspan - rowspans.append(rowspan) - colspans.append(colspan) - heights.append(sum(height_ratios[rowspan.start:rowspan.stop])) - widths.append(sum(width_ratios[colspan.start:colspan.stop])) - - for idx0, ax0 in enumerate(axs): - # Compare ax to all other axs: If the subplotspecs start (/stop) at - # the same column, then line up their left (/right) sides; likewise - # for rows/top/bottom. - rowspan0 = rowspans[idx0] - colspan0 = colspans[idx0] - height0 = heights[idx0] - width0 = widths[idx0] - alignleft = False - alignright = False - alignbot = False - aligntop = False - alignheight = False - alignwidth = False - for idx1 in range(idx0 + 1, len(axs)): - ax1 = axs[idx1] - rowspan1 = rowspans[idx1] - colspan1 = colspans[idx1] - width1 = widths[idx1] - height1 = heights[idx1] - # Horizontally align axes spines if they have the same min or max: - if not alignleft and colspan0.start == colspan1.start: - _log.debug('same start columns; line up layoutbox lefts') - layoutbox.align([ax0._poslayoutbox, ax1._poslayoutbox], - 'left') - alignleft = True - if not alignright and colspan0.stop == colspan1.stop: - _log.debug('same stop columns; line up layoutbox rights') - layoutbox.align([ax0._poslayoutbox, ax1._poslayoutbox], - 'right') - alignright = True - # Vertically align axes spines if they have the same min or max: - if not aligntop and rowspan0.start == rowspan1.start: - _log.debug('same start rows; line up layoutbox tops') - layoutbox.align([ax0._poslayoutbox, ax1._poslayoutbox], - 'top') - aligntop = True - if not alignbot and rowspan0.stop == rowspan1.stop: - _log.debug('same stop rows; line up layoutbox bottoms') - layoutbox.align([ax0._poslayoutbox, ax1._poslayoutbox], - 'bottom') - alignbot = True - - # Now we make the widths and heights of position boxes - # similar. (i.e the spine locations) - # This allows vertically stacked subplots to have different sizes - # if they occupy different amounts of the gridspec, e.g. if - # gs = gridspec.GridSpec(3, 1) - # ax0 = gs[0, :] - # ax1 = gs[1:, :] - # then len(rowspan0) = 1, and len(rowspan1) = 2, - # and ax1 should be at least twice as large as ax0. - # But it can be more than twice as large because - # it needs less room for the labeling. - - # For heights, do it if the subplots share a column. - if not alignheight and len(rowspan0) == len(rowspan1): - ax0._poslayoutbox.constrain_height( - ax1._poslayoutbox.height * height0 / height1) - alignheight = True - elif _spans_overlap(colspan0, colspan1): - if height0 > height1: - ax0._poslayoutbox.constrain_height_min( - ax1._poslayoutbox.height * height0 / height1) - elif height0 < height1: - ax1._poslayoutbox.constrain_height_min( - ax0._poslayoutbox.height * height1 / height0) - # For widths, do it if the subplots share a row. - if not alignwidth and len(colspan0) == len(colspan1): - ax0._poslayoutbox.constrain_width( - ax1._poslayoutbox.width * width0 / width1) - alignwidth = True - elif _spans_overlap(rowspan0, rowspan1): - if width0 > width1: - ax0._poslayoutbox.constrain_width_min( - ax1._poslayoutbox.width * width0 / width1) - elif width0 < width1: - ax1._poslayoutbox.constrain_width_min( - ax0._poslayoutbox.width * width1 / width0) - - -def _arrange_subplotspecs(gs, hspace=0, wspace=0): - """Recursively arrange the subplotspec children of the given gridspec.""" - sschildren = [] - for child in gs.children: - if child._is_subplotspec_layoutbox(): - for child2 in child.children: - # check for gridspec children... - if child2._is_gridspec_layoutbox(): - _arrange_subplotspecs(child2, hspace=hspace, wspace=wspace) - sschildren += [child] - # now arrange the subplots... - for child0 in sschildren: - ss0 = child0.artist - nrows, ncols = ss0.get_gridspec().get_geometry() - rowspan0 = ss0.rowspan - colspan0 = ss0.colspan - sschildren = sschildren[1:] - for child1 in sschildren: - ss1 = child1.artist - rowspan1 = ss1.rowspan - colspan1 = ss1.colspan - # OK, this tells us the relative layout of child0 with child1. - pad = wspace / ncols - if colspan0.stop <= colspan1.start: - layoutbox.hstack([ss0._layoutbox, ss1._layoutbox], padding=pad) - if colspan1.stop <= colspan0.start: - layoutbox.hstack([ss1._layoutbox, ss0._layoutbox], padding=pad) - # vertical alignment - pad = hspace / nrows - if rowspan0.stop <= rowspan1.start: - layoutbox.vstack([ss0._layoutbox, ss1._layoutbox], padding=pad) - if rowspan1.stop <= rowspan0.start: - layoutbox.vstack([ss1._layoutbox, ss0._layoutbox], padding=pad) - - -def layoutcolorbarsingle(ax, cax, shrink, aspect, location, pad=0.05): + trans_fig_to_panel = fig.transFigure - fig.transPanel + for panel in fig.panels: + bbox = panel._layoutgrid.get_outer_bbox() + panel._redo_transform_rel_fig( + bbox=bbox.transformed(trans_fig_to_panel)) + _reposition_axes(panel, renderer, + w_pad=w_pad, h_pad=h_pad, + wspace=wspace, hspace=hspace) + + # for ax in fig._localaxes: + # if not hasattr(a, 'get_subplotspec'): + for ax in fig.get_axes(): + if not hasattr(ax, 'get_subplotspec'): + continue + + # grid bbox is in Figure co-ordinates, but we specify in panel + # co-ordinates... + ss = ax.get_subplotspec() + gs = ss.get_gridspec() + nrows, ncols = gs.get_geometry() + if gs._layoutgrid is None: + return + + bbox = gs._layoutgrid.get_inner_bbox(rows=ss.rowspan, cols=ss.colspan) + + bboxouter = gs._layoutgrid.get_outer_bbox(rows=ss.rowspan, + cols=ss.colspan) + + # transform from figure to panel for set_position: + newbbox = trans_fig_to_panel.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]: + margin = _reposition_colorbar( + cbax, renderer, offset=offset) + + +def _reposition_colorbar(cbax, renderer, *, offset=None): """ - Do the layout for a colorbar, to not overly pollute colorbar.py + Place the colorbar in its new place. + + Parameters + ---------- + cbax : Axes + Axes for the colorbar - *pad* is in fraction of the original axis size. + renderer : + w_pad, h_pad : float + width and height padding (in fraction of figure) + hspace, wspace : float + width and height padding as fraction of figure size divided by + number of columns or rows + margin : array-like + offset the colorbar needs to be pushed to in order to + account for multiple colorbars """ - axlb = ax._layoutbox - axpos = ax._poslayoutbox - axsslb = ax.get_subplotspec()._layoutbox - lb = layoutbox.LayoutBox( - parent=axsslb, - name=axsslb.name + '.cbar', - artist=cax) - if location in ('left', 'right'): - lbpos = layoutbox.LayoutBox( - parent=lb, - name=lb.name + '.pos', - tightwidth=False, - pos=True, - subplot=False, - artist=cax) + parents = cbax._colorbar_info['parents'] + gs = parents[0].get_gridspec() + ncols, nrows = gs.ncols, gs.nrows + fig = cbax.figure + trans_fig_to_panel = fig.transFigure - fig.transPanel + + cb_rspans, cb_cspans = _get_cb_parent_spans(cbax) + bboxparent = gs._layoutgrid.get_bbox_for_cb(rows=cb_rspans, cols=cb_cspans) + pb = gs._layoutgrid.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(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': - # arrange to right of parent axis - layoutbox.hstack([axlb, lb], padding=pad * axlb.width, - strength='strong') + 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: - layoutbox.hstack([lb, axlb], padding=pad * axlb.width) - # constrain the height and center... - layoutbox.match_heights([axpos, lbpos], [1, shrink]) - layoutbox.align([axpos, lbpos], 'v_center') - # set the width of the pos box - lbpos.constrain_width(shrink * axpos.height * (1/aspect), - strength='strong') - elif location in ('bottom', 'top'): - lbpos = layoutbox.LayoutBox( - parent=lb, - name=lb.name + '.pos', - tightheight=True, - pos=True, - subplot=False, - artist=cax) - - if location == 'bottom': - layoutbox.vstack([axlb, lb], padding=pad * axlb.height) + 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: - layoutbox.vstack([lb, axlb], padding=pad * axlb.height) - # constrain the height and center... - layoutbox.match_widths([axpos, lbpos], - [1, shrink], strength='strong') - layoutbox.align([axpos, lbpos], 'h_center') - # set the height of the pos box - lbpos.constrain_height(axpos.width * aspect * shrink, - strength='medium') + 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) - return lb, lbpos + pbcb = trans_fig_to_panel.transform_bbox(pbcb) + cbax.set_transform(fig.transPanel) + cbax._set_position(pbcb) + cbax.set_aspect(aspect, anchor=anchor, adjustable='box') + return offset -def _getmaxminrowcolumn(axs): - """ - Find axes covering the first and last rows and columns of a list of axes. +def _reset_margins(fig): """ - startrow = startcol = np.inf - stoprow = stopcol = -np.inf - startax_row = startax_col = stopax_row = stopax_col = None - for ax in axs: - subspec = ax.get_subplotspec() - if subspec.rowspan.start < startrow: - startrow = subspec.rowspan.start - startax_row = ax - if subspec.rowspan.stop > stoprow: - stoprow = subspec.rowspan.stop - stopax_row = ax - if subspec.colspan.start < startcol: - startcol = subspec.colspan.start - startax_col = ax - if subspec.colspan.stop > stopcol: - stopcol = subspec.colspan.stop - stopax_col = ax - return (startrow, stoprow - 1, startax_row, stopax_row, - startcol, stopcol - 1, startax_col, stopax_col) - - -def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05): - """ - Do the layout for a colorbar, to not overly pollute colorbar.py + Reset the margins in the layoutboxes of fig. - *pad* is in fraction of the original axis size. + 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 span in fig.panels: + _reset_margins(span) + for ax in fig.axes: + if hasattr(ax, 'get_subplotspec'): + ss = ax.get_subplotspec() + gs = ss.get_gridspec() + if gs._layoutgrid is not None: + gs._layoutgrid.reset_margins() + fig._layoutgrid.reset_margins() - gs = parents[0].get_subplotspec().get_gridspec() - # parent layout box.... - gslb = gs._layoutbox - lb = layoutbox.LayoutBox(parent=gslb.parent, - name=gslb.parent.name + '.cbar', - artist=cax) - # figure out the row and column extent of the parents. - (minrow, maxrow, minax_row, maxax_row, - mincol, maxcol, minax_col, maxax_col) = _getmaxminrowcolumn(parents) +def _colorbar_get_pad(cax): + parents = cax._colorbar_info['parents'] + gs = parents[0].get_gridspec() - if location in ('left', 'right'): - lbpos = layoutbox.LayoutBox( - parent=lb, - name=lb.name + '.pos', - tightwidth=False, - pos=True, - subplot=False, - artist=cax) - for ax in parents: - if location == 'right': - order = [ax._layoutbox, lb] - else: - order = [lb, ax._layoutbox] - layoutbox.hstack(order, padding=pad * gslb.width, - strength='strong') - # constrain the height and center... - # This isn't quite right. We'd like the colorbar - # pos to line up w/ the axes poss, not the size of the - # gs. - - # Horizontal Layout: need to check all the axes in this gridspec - for ch in gslb.children: - subspec = ch.artist - if location == 'right': - if subspec.colspan.stop - 1 <= maxcol: - order = [subspec._layoutbox, lb] - # arrange to right of the parents - elif subspec.colspan.start > maxcol: - order = [lb, subspec._layoutbox] - elif location == 'left': - if subspec.colspan.start >= mincol: - order = [lb, subspec._layoutbox] - elif subspec.colspan.stop - 1 < mincol: - order = [subspec._layoutbox, lb] - layoutbox.hstack(order, padding=pad * gslb.width, - strength='strong') - - # Vertical layout: - maxposlb = minax_row._poslayoutbox - minposlb = maxax_row._poslayoutbox - # now we want the height of the colorbar pos to be - # set by the top and bottom of the min/max axes... - # bottom top - # b t - # h = (top-bottom)*shrink - # b = bottom + (top-bottom - h) / 2. - lbpos.constrain_height( - (maxposlb.top - minposlb.bottom) * - shrink, strength='strong') - lbpos.constrain_bottom( - (maxposlb.top - minposlb.bottom) * - (1 - shrink)/2 + minposlb.bottom, - strength='strong') - - # set the width of the pos box - lbpos.constrain_width(lbpos.height * (shrink / aspect), - strength='strong') - elif location in ('bottom', 'top'): - lbpos = layoutbox.LayoutBox( - parent=lb, - name=lb.name + '.pos', - tightheight=True, - pos=True, - subplot=False, - artist=cax) - - for ax in parents: - if location == 'bottom': - order = [ax._layoutbox, lb] - else: - order = [lb, ax._layoutbox] - layoutbox.vstack(order, padding=pad * gslb.width, - strength='strong') - - # Vertical Layout: need to check all the axes in this gridspec - for ch in gslb.children: - subspec = ch.artist - if location == 'bottom': - if subspec.rowspan.stop - 1 <= minrow: - order = [subspec._layoutbox, lb] - elif subspec.rowspan.start > maxrow: - order = [lb, subspec._layoutbox] - elif location == 'top': - if subspec.rowspan.stop - 1 < minrow: - order = [subspec._layoutbox, lb] - elif subspec.rowspan.start >= maxrow: - order = [lb, subspec._layoutbox] - layoutbox.vstack(order, padding=pad * gslb.width, - strength='strong') - - # Do horizontal layout... - maxposlb = maxax_col._poslayoutbox - minposlb = minax_col._poslayoutbox - lbpos.constrain_width((maxposlb.right - minposlb.left) * - shrink) - lbpos.constrain_left( - (maxposlb.right - minposlb.left) * - (1-shrink)/2 + minposlb.left) - # set the height of the pos box - lbpos.constrain_height(lbpos.width * shrink * aspect, - strength='medium') - - return lb, lbpos + cb_rspans, cb_cspans = _get_cb_parent_spans(cax) + bboxouter = gs._layoutgrid.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 diff --git a/lib/matplotlib/_layoutbox.py b/lib/matplotlib/_layoutbox.py deleted file mode 100644 index 2c61890e6bce..000000000000 --- a/lib/matplotlib/_layoutbox.py +++ /dev/null @@ -1,679 +0,0 @@ -""" - -Conventions: - -"constrain_x" means to constrain the variable with either -another kiwisolver variable, or a float. i.e. `constrain_width(0.2)` -will set a constraint that the width has to be 0.2 and this constraint is -permanent - i.e. it will not be removed if it becomes obsolete. - -"edit_x" means to set x to a value (just a float), and that this value can -change. So `edit_width(0.2)` will set width to be 0.2, but `edit_width(0.3)` -will allow it to change to 0.3 later. Note that these values are still just -"suggestions" in `kiwisolver` parlance, and could be over-ridden by -other constrains. - -""" - -import itertools -import kiwisolver as kiwi -import logging -import numpy as np - - -_log = logging.getLogger(__name__) - - -# renderers can be complicated -def get_renderer(fig): - if fig._cachedRenderer: - renderer = fig._cachedRenderer - else: - canvas = fig.canvas - if canvas and hasattr(canvas, "get_renderer"): - renderer = canvas.get_renderer() - else: - # not sure if this can happen - # seems to with PDF... - _log.info("constrained_layout : falling back to Agg renderer") - from matplotlib.backends.backend_agg import FigureCanvasAgg - canvas = FigureCanvasAgg(fig) - renderer = canvas.get_renderer() - - return renderer - - -class LayoutBox: - """ - Basic rectangle representation using kiwi solver variables - """ - - def __init__(self, parent=None, name='', tightwidth=False, - tightheight=False, artist=None, - lower_left=(0, 0), upper_right=(1, 1), pos=False, - subplot=False, h_pad=None, w_pad=None): - Variable = kiwi.Variable - self.parent = parent - self.name = name - sn = self.name + '_' - if parent is None: - self.solver = kiwi.Solver() - self.constrained_layout_called = 0 - else: - self.solver = parent.solver - self.constrained_layout_called = None - # parent wants to know about this child! - parent.add_child(self) - # keep track of artist associated w/ this layout. Can be none - self.artist = artist - # keep track if this box is supposed to be a pos that is constrained - # by the parent. - self.pos = pos - # keep track of whether we need to match this subplot up with others. - self.subplot = subplot - - self.top = Variable(sn + 'top') - self.bottom = Variable(sn + 'bottom') - self.left = Variable(sn + 'left') - self.right = Variable(sn + 'right') - - self.width = Variable(sn + 'width') - self.height = Variable(sn + 'height') - self.h_center = Variable(sn + 'h_center') - self.v_center = Variable(sn + 'v_center') - - self.min_width = Variable(sn + 'min_width') - self.min_height = Variable(sn + 'min_height') - self.pref_width = Variable(sn + 'pref_width') - self.pref_height = Variable(sn + 'pref_height') - # margins are only used for axes-position layout boxes. maybe should - # be a separate subclass: - self.left_margin = Variable(sn + 'left_margin') - self.right_margin = Variable(sn + 'right_margin') - self.bottom_margin = Variable(sn + 'bottom_margin') - self.top_margin = Variable(sn + 'top_margin') - # mins - self.left_margin_min = Variable(sn + 'left_margin_min') - self.right_margin_min = Variable(sn + 'right_margin_min') - self.bottom_margin_min = Variable(sn + 'bottom_margin_min') - self.top_margin_min = Variable(sn + 'top_margin_min') - - right, top = upper_right - left, bottom = lower_left - self.tightheight = tightheight - self.tightwidth = tightwidth - self.add_constraints() - self.children = [] - self.subplotspec = None - if self.pos: - self.constrain_margins() - self.h_pad = h_pad - self.w_pad = w_pad - - def constrain_margins(self): - """ - Only do this for pos. This sets a variable distance - margin between the position of the axes and the outer edge of - the axes. - - Margins are variable because they change with the figure size. - - Margin minimums are set to make room for axes decorations. However, - the margins can be larger if we are mathicng the position size to - other axes. - """ - sol = self.solver - - # left - if not sol.hasEditVariable(self.left_margin_min): - sol.addEditVariable(self.left_margin_min, 'strong') - sol.suggestValue(self.left_margin_min, 0.0001) - c = (self.left_margin == self.left - self.parent.left) - self.solver.addConstraint(c | 'required') - c = (self.left_margin >= self.left_margin_min) - self.solver.addConstraint(c | 'strong') - - # right - if not sol.hasEditVariable(self.right_margin_min): - sol.addEditVariable(self.right_margin_min, 'strong') - sol.suggestValue(self.right_margin_min, 0.0001) - c = (self.right_margin == self.parent.right - self.right) - self.solver.addConstraint(c | 'required') - c = (self.right_margin >= self.right_margin_min) - self.solver.addConstraint(c | 'required') - # bottom - if not sol.hasEditVariable(self.bottom_margin_min): - sol.addEditVariable(self.bottom_margin_min, 'strong') - sol.suggestValue(self.bottom_margin_min, 0.0001) - c = (self.bottom_margin == self.bottom - self.parent.bottom) - self.solver.addConstraint(c | 'required') - c = (self.bottom_margin >= self.bottom_margin_min) - self.solver.addConstraint(c | 'required') - # top - if not sol.hasEditVariable(self.top_margin_min): - sol.addEditVariable(self.top_margin_min, 'strong') - sol.suggestValue(self.top_margin_min, 0.0001) - c = (self.top_margin == self.parent.top - self.top) - self.solver.addConstraint(c | 'required') - c = (self.top_margin >= self.top_margin_min) - self.solver.addConstraint(c | 'required') - - def add_child(self, child): - self.children += [child] - - def remove_child(self, child): - try: - self.children.remove(child) - except ValueError: - _log.info("Tried to remove child that doesn't belong to parent") - - def add_constraints(self): - sol = self.solver - # never let width and height go negative. - for i in [self.min_width, self.min_height]: - sol.addEditVariable(i, 1e9) - sol.suggestValue(i, 0.0) - # define relation ships between things thing width and right and left - self.hard_constraints() - # self.soft_constraints() - if self.parent: - self.parent_constrain() - # sol.updateVariables() - - def parent_constrain(self): - parent = self.parent - hc = [self.left >= parent.left, - self.bottom >= parent.bottom, - self.top <= parent.top, - self.right <= parent.right] - for c in hc: - self.solver.addConstraint(c | 'required') - - def hard_constraints(self): - hc = [self.width == self.right - self.left, - self.height == self.top - self.bottom, - self.h_center == (self.left + self.right) * 0.5, - self.v_center == (self.top + self.bottom) * 0.5, - self.width >= self.min_width, - self.height >= self.min_height] - for c in hc: - self.solver.addConstraint(c | 'required') - - def soft_constraints(self): - sol = self.solver - if self.tightwidth: - suggest = 0. - else: - suggest = 20. - c = (self.pref_width == suggest) - for i in c: - sol.addConstraint(i | 'required') - if self.tightheight: - suggest = 0. - else: - suggest = 20. - c = (self.pref_height == suggest) - for i in c: - sol.addConstraint(i | 'required') - - c = [(self.width >= suggest), - (self.height >= suggest)] - for i in c: - sol.addConstraint(i | 150000) - - def set_parent(self, parent): - """Replace the parent of this with the new parent.""" - self.parent = parent - self.parent_constrain() - - def constrain_geometry(self, left, bottom, right, top, strength='strong'): - hc = [self.left == left, - self.right == right, - self.bottom == bottom, - self.top == top] - for c in hc: - self.solver.addConstraint(c | strength) - # self.solver.updateVariables() - - def constrain_same(self, other, strength='strong'): - """ - Make the layoutbox have same position as other layoutbox - """ - hc = [self.left == other.left, - self.right == other.right, - self.bottom == other.bottom, - self.top == other.top] - for c in hc: - self.solver.addConstraint(c | strength) - - def constrain_left_margin(self, margin, strength='strong'): - c = (self.left == self.parent.left + margin) - self.solver.addConstraint(c | strength) - - def edit_left_margin_min(self, margin): - self.solver.suggestValue(self.left_margin_min, margin) - - def constrain_right_margin(self, margin, strength='strong'): - c = (self.right == self.parent.right - margin) - self.solver.addConstraint(c | strength) - - def edit_right_margin_min(self, margin): - self.solver.suggestValue(self.right_margin_min, margin) - - def constrain_bottom_margin(self, margin, strength='strong'): - c = (self.bottom == self.parent.bottom + margin) - self.solver.addConstraint(c | strength) - - def edit_bottom_margin_min(self, margin): - self.solver.suggestValue(self.bottom_margin_min, margin) - - def constrain_top_margin(self, margin, strength='strong'): - c = (self.top == self.parent.top - margin) - self.solver.addConstraint(c | strength) - - def edit_top_margin_min(self, margin): - self.solver.suggestValue(self.top_margin_min, margin) - - def get_rect(self): - return (self.left.value(), self.bottom.value(), - self.width.value(), self.height.value()) - - def update_variables(self): - """ - Update *all* the variables that are part of the solver this LayoutBox - is created with. - """ - self.solver.updateVariables() - - def edit_height(self, height, strength='strong'): - """ - Set the height of the layout box. - - This is done as an editable variable so that the value can change - due to resizing. - """ - sol = self.solver - for i in [self.height]: - if not sol.hasEditVariable(i): - sol.addEditVariable(i, strength) - sol.suggestValue(self.height, height) - - def constrain_height(self, height, strength='strong'): - """ - Constrain the height of the layout box. height is - either a float or a layoutbox.height. - """ - c = (self.height == height) - self.solver.addConstraint(c | strength) - - def constrain_height_min(self, height, strength='strong'): - c = (self.height >= height) - self.solver.addConstraint(c | strength) - - def edit_width(self, width, strength='strong'): - sol = self.solver - for i in [self.width]: - if not sol.hasEditVariable(i): - sol.addEditVariable(i, strength) - sol.suggestValue(self.width, width) - - def constrain_width(self, width, strength='strong'): - """ - Constrain the width of the layout box. *width* is - either a float or a layoutbox.width. - """ - c = (self.width == width) - self.solver.addConstraint(c | strength) - - def constrain_width_min(self, width, strength='strong'): - c = (self.width >= width) - self.solver.addConstraint(c | strength) - - def constrain_left(self, left, strength='strong'): - c = (self.left == left) - self.solver.addConstraint(c | strength) - - def constrain_bottom(self, bottom, strength='strong'): - c = (self.bottom == bottom) - self.solver.addConstraint(c | strength) - - def constrain_right(self, right, strength='strong'): - c = (self.right == right) - self.solver.addConstraint(c | strength) - - def constrain_top(self, top, strength='strong'): - c = (self.top == top) - self.solver.addConstraint(c | strength) - - def _is_subplotspec_layoutbox(self): - """ - Helper to check if this layoutbox is the layoutbox of a subplotspec. - """ - name = self.name.split('.')[-1] - return name[:2] == 'ss' - - def _is_gridspec_layoutbox(self): - """ - Helper to check if this layoutbox is the layoutbox of a gridspec. - """ - name = self.name.split('.')[-1] - return name[:8] == 'gridspec' - - def find_child_subplots(self): - """ - Find children of this layout box that are subplots. We want to line - poss up, and this is an easy way to find them all. - """ - if self.subplot: - subplots = [self] - else: - subplots = [] - for child in self.children: - subplots += child.find_child_subplots() - return subplots - - def layout_from_subplotspec(self, subspec, - name='', artist=None, pos=False): - """ - Make a layout box from a subplotspec. The layout box is - constrained to be a fraction of the width/height of the parent, - and be a fraction of the parent width/height from the left/bottom - of the parent. Therefore the parent can move around and the - layout for the subplot spec should move with it. - - The parent is *usually* the gridspec that made the subplotspec.?? - """ - lb = LayoutBox(parent=self, name=name, artist=artist, pos=pos) - gs = subspec.get_gridspec() - nrows, ncols = gs.get_geometry() - parent = self.parent - - # OK, now, we want to set the position of this subplotspec - # based on its subplotspec parameters. The new gridspec will inherit - # from gridspec. prob should be new method in gridspec - left = 0.0 - right = 1.0 - bottom = 0.0 - top = 1.0 - totWidth = right-left - totHeight = top-bottom - hspace = 0. - wspace = 0. - - # calculate accumulated heights of columns - cellH = totHeight / (nrows + hspace * (nrows - 1)) - sepH = hspace * cellH - - if gs._row_height_ratios is not None: - netHeight = cellH * nrows - tr = sum(gs._row_height_ratios) - cellHeights = [netHeight * r / tr for r in gs._row_height_ratios] - else: - cellHeights = [cellH] * nrows - - sepHeights = [0] + ([sepH] * (nrows - 1)) - cellHs = np.cumsum(np.column_stack([sepHeights, cellHeights]).flat) - - # calculate accumulated widths of rows - cellW = totWidth / (ncols + wspace * (ncols - 1)) - sepW = wspace * cellW - - if gs._col_width_ratios is not None: - netWidth = cellW * ncols - tr = sum(gs._col_width_ratios) - cellWidths = [netWidth * r / tr for r in gs._col_width_ratios] - else: - cellWidths = [cellW] * ncols - - sepWidths = [0] + ([sepW] * (ncols - 1)) - cellWs = np.cumsum(np.column_stack([sepWidths, cellWidths]).flat) - - figTops = [top - cellHs[2 * rowNum] for rowNum in range(nrows)] - figBottoms = [top - cellHs[2 * rowNum + 1] for rowNum in range(nrows)] - figLefts = [left + cellWs[2 * colNum] for colNum in range(ncols)] - figRights = [left + cellWs[2 * colNum + 1] for colNum in range(ncols)] - - rowNum1, colNum1 = divmod(subspec.num1, ncols) - rowNum2, colNum2 = divmod(subspec.num2, ncols) - figBottom = min(figBottoms[rowNum1], figBottoms[rowNum2]) - figTop = max(figTops[rowNum1], figTops[rowNum2]) - figLeft = min(figLefts[colNum1], figLefts[colNum2]) - figRight = max(figRights[colNum1], figRights[colNum2]) - - # These are numbers relative to (0, 0, 1, 1). Need to constrain - # relative to parent. - - width = figRight - figLeft - height = figTop - figBottom - parent = self.parent - cs = [self.left == parent.left + parent.width * figLeft, - self.bottom == parent.bottom + parent.height * figBottom, - self.width == parent.width * width, - self.height == parent.height * height] - for c in cs: - self.solver.addConstraint(c | 'required') - - return lb - - def __repr__(self): - return (f'LayoutBox: {self.name:25s}, ' - f'(left: {self.left.value():1.3f}) ' - f'(bot: {self.bottom.value():1.3f}) ' - f'(right: {self.right.value():1.3f}) ' - f'(top: {self.top.value():1.3f})') - - -# Utility functions that act on layoutboxes... -def hstack(boxes, padding=0, strength='strong'): - """ - Stack LayoutBox instances from left to right. - *padding* is in figure-relative units. - """ - - for i in range(1, len(boxes)): - c = (boxes[i-1].right + padding <= boxes[i].left) - boxes[i].solver.addConstraint(c | strength) - - -def hpack(boxes, padding=0, strength='strong'): - """Stack LayoutBox instances from left to right.""" - - for i in range(1, len(boxes)): - c = (boxes[i-1].right + padding == boxes[i].left) - boxes[i].solver.addConstraint(c | strength) - - -def vstack(boxes, padding=0, strength='strong'): - """Stack LayoutBox instances from top to bottom.""" - - for i in range(1, len(boxes)): - c = (boxes[i-1].bottom - padding >= boxes[i].top) - boxes[i].solver.addConstraint(c | strength) - - -def vpack(boxes, padding=0, strength='strong'): - """Stack LayoutBox instances from top to bottom.""" - - for i in range(1, len(boxes)): - c = (boxes[i-1].bottom - padding >= boxes[i].top) - boxes[i].solver.addConstraint(c | strength) - - -def match_heights(boxes, height_ratios, strength='medium'): - """Stack LayoutBox instances from top to bottom.""" - for i in range(1, len(boxes)): - c = (boxes[i-1].height == - boxes[i].height*height_ratios[i-1]/height_ratios[i]) - boxes[i].solver.addConstraint(c | strength) - - -def match_widths(boxes, width_ratios, strength='medium'): - """Stack LayoutBox instances from top to bottom.""" - for i in range(1, len(boxes)): - c = (boxes[i-1].width == - boxes[i].width*width_ratios[i-1]/width_ratios[i]) - boxes[i].solver.addConstraint(c | strength) - - -def align(boxes, attr, strength='strong'): - cons = [] - for box in boxes[1:]: - cons = (getattr(boxes[0], attr) == getattr(box, attr)) - boxes[0].solver.addConstraint(cons | strength) - - -def match_top_margins(boxes, levels=1): - box0 = boxes[0] - top0 = box0 - for n in range(levels): - top0 = top0.parent - for box in boxes[1:]: - topb = box - for n in range(levels): - topb = topb.parent - c = (box0.top-top0.top == box.top-topb.top) - box0.solver.addConstraint(c | 'strong') - - -def match_bottom_margins(boxes, levels=1): - box0 = boxes[0] - top0 = box0 - for n in range(levels): - top0 = top0.parent - for box in boxes[1:]: - topb = box - for n in range(levels): - topb = topb.parent - c = (box0.bottom-top0.bottom == box.bottom-topb.bottom) - box0.solver.addConstraint(c | 'strong') - - -def match_left_margins(boxes, levels=1): - box0 = boxes[0] - top0 = box0 - for n in range(levels): - top0 = top0.parent - for box in boxes[1:]: - topb = box - for n in range(levels): - topb = topb.parent - c = (box0.left-top0.left == box.left-topb.left) - box0.solver.addConstraint(c | 'strong') - - -def match_right_margins(boxes, levels=1): - box0 = boxes[0] - top0 = box0 - for n in range(levels): - top0 = top0.parent - for box in boxes[1:]: - topb = box - for n in range(levels): - topb = topb.parent - c = (box0.right-top0.right == box.right-topb.right) - box0.solver.addConstraint(c | 'strong') - - -def match_width_margins(boxes, levels=1): - match_left_margins(boxes, levels=levels) - match_right_margins(boxes, levels=levels) - - -def match_height_margins(boxes, levels=1): - match_top_margins(boxes, levels=levels) - match_bottom_margins(boxes, levels=levels) - - -def match_margins(boxes, levels=1): - match_width_margins(boxes, levels=levels) - match_height_margins(boxes, levels=levels) - - -_layoutboxobjnum = itertools.count() - - -def seq_id(): - """Generate a short sequential id for layoutbox objects.""" - return '%06d' % next(_layoutboxobjnum) - - -def print_children(lb): - """Print the children of the layoutbox.""" - print(lb) - for child in lb.children: - print_children(child) - - -def nonetree(lb): - """ - Make all elements in this tree None, signalling not to do any more layout. - """ - if lb is not None: - if lb.parent is None: - # Clear the solver. Hopefully this garbage collects. - lb.solver.reset() - nonechildren(lb) - else: - nonetree(lb.parent) - - -def nonechildren(lb): - for child in lb.children: - nonechildren(child) - lb.artist._layoutbox = None - lb = None - - -def print_tree(lb): - """Print the tree of layoutboxes.""" - - if lb.parent is None: - print('LayoutBox Tree\n') - print('==============\n') - print_children(lb) - print('\n') - else: - print_tree(lb.parent) - - -def plot_children(fig, box, level=0, printit=True): - """Simple plotting to show where boxes are.""" - import matplotlib - import matplotlib.pyplot as plt - - if isinstance(fig, matplotlib.figure.Figure): - ax = fig.add_axes([0., 0., 1., 1.]) - ax.set_facecolor([1., 1., 1., 0.7]) - ax.set_alpha(0.3) - fig.draw(fig.canvas.get_renderer()) - else: - ax = fig - - import matplotlib.patches as patches - colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] - if printit: - print("Level:", level) - for child in box.children: - if printit: - print(child) - ax.add_patch( - patches.Rectangle( - (child.left.value(), child.bottom.value()), # (x, y) - child.width.value(), # width - child.height.value(), # height - fc='none', - alpha=0.8, - ec=colors[level] - ) - ) - if level > 0: - name = child.name.split('.')[-1] - if level % 2 == 0: - ax.text(child.left.value(), child.bottom.value(), name, - size=12-level, color=colors[level]) - else: - ax.text(child.right.value(), child.top.value(), name, - ha='right', va='top', size=12-level, - color=colors[level]) - - plot_children(ax, child, level=level+1, printit=printit) diff --git a/lib/matplotlib/_layoutgrid.py b/lib/matplotlib/_layoutgrid.py new file mode 100644 index 000000000000..4cf7b9d886c3 --- /dev/null +++ b/lib/matplotlib/_layoutgrid.py @@ -0,0 +1,560 @@ +""" +A layoutgrid is a nrows by ncols set of boxes, meant to be used by +`._constrained_layout`, each box is analagous to a subplotspec element of +a gridspec. + +Each box is defined by left[ncols], right[ncols], bottom[nrows] and top[nrows], +and by two editable margins for each side. The main margin gets its value +set by the size of ticklabels, titles, etc on each axes that is in the figure. +The outer margin is the padding around the axes, and space for any +colorbars. + +The "inner" widths and heights of these boxes are then constrained to be the +same (relative the values of `width_ratios[ncols]` and `height_ratios[nrows]`). + +The layoutgrid is then constrained to be contained within a parent layoutgrid, +its column(s) and row(s) specified when it is created. +""" + +import itertools +import kiwisolver as kiwi +import logging +import numpy as np +from matplotlib.transforms import Bbox + + +_log = logging.getLogger(__name__) + + +class LayoutGrid: + """ + Analagous to a gridspec, and contained in another LayoutGrid. + """ + + def __init__(self, parent=None, parent_pos=(0, 0), + parent_inner=False, name='', ncols=1, nrows=1, + h_pad=None, w_pad=None, width_ratios=None, + height_ratios=None): + Variable = kiwi.Variable + self.parent = parent + self.parent_pos = parent_pos + self.parent_inner = parent_inner + self.name = name + self.nrows = nrows + self.ncols = ncols + self.height_ratios = np.atleast_1d(height_ratios) + if height_ratios is None: + self.height_ratios = np.ones(nrows) + self.width_ratios = np.atleast_1d(width_ratios) + if width_ratios is None: + self.width_ratios = np.ones(ncols) + + sn = self.name + '_' + if parent is None: + self.parent = None + self.solver = kiwi.Solver() + else: + self.parent = parent + parent.add_child(self, *parent_pos) + self.solver = self.parent.solver + # keep track of artist associated w/ this layout. Can be none + self.artists = np.empty((nrows, ncols), dtype=object) + self.children = np.empty((nrows, ncols), dtype=object) + + self.margins = {} + self.margin_vals = {} + # all the boxes in each column share the same left/right margins: + for todo in ['left', 'right', 'leftcb', 'rightcb']: + # track the value so we can change only if a margin is larger + # than the current value + self.margin_vals[todo] = np.zeros(ncols) + + sol = self.solver + + # These are redundant, but make life easier if + # we define them all. All that is really + # needed is left/right, margin['left'], and margin['right'] + self.widths = [Variable(f'{sn}widths[{i}]') for i in range(ncols)] + self.lefts = [Variable(f'{sn}lefts[{i}]') for i in range(ncols)] + self.rights = [Variable(f'{sn}rights[{i}]') for i in range(ncols)] + self.inner_widths = [Variable(f'{sn}inner_widths[{i}]') + for i in range(ncols)] + for todo in ['left', 'right', 'leftcb', 'rightcb']: + self.margins[todo] = [Variable(f'{sn}margins[{todo}][{i}]') + for i in range(ncols)] + for i in range(ncols): + sol.addEditVariable(self.margins[todo][i], 'strong') + + for todo in ['bottom', 'top', 'bottomcb', 'topcb']: + self.margins[todo] = np.empty((nrows), dtype=object) + self.margin_vals[todo] = np.zeros(nrows) + + self.heights = [Variable(f'{sn}heights[{i}]') for i in range(nrows)] + self.inner_heights = [Variable(f'{sn}inner_heights[{i}]') + for i in range(nrows)] + self.bottoms = [Variable(f'{sn}bottoms[{i}]') for i in range(nrows)] + self.tops = [Variable(f'{sn}tops[{i}]') for i in range(nrows)] + for todo in ['bottom', 'top', 'bottomcb', 'topcb']: + self.margins[todo] = [Variable(f'{sn}margins[{todo}][{i}]') + for i in range(nrows)] + for i in range(nrows): + sol.addEditVariable(self.margins[todo][i], 'strong') + + # set these margins to zero by default. They will be edited as + # children are filled. + self.reset_margins() + self.add_constraints() + + self.h_pad = h_pad + self.w_pad = w_pad + + def __repr__(self): + str = f'LayoutBox: {self.name:25s} {self.nrows}x{self.ncols},\n' + for i in range(self.nrows): + for j in range(self.ncols): + str += f'{i}, {j}: '\ + f'L({self.lefts[j].value():1.3f}, ' \ + f'B{self.bottoms[i].value():1.3f}, ' \ + f'W{self.widths[j].value():1.3f}, ' \ + f'H{self.heights[i].value():1.3f}, ' \ + f'innerW{self.inner_widths[j].value():1.3f}, ' \ + f'innerH{self.inner_heights[i].value():1.3f}, ' \ + f'ML{self.margins["left"][j].value():1.3f}, ' \ + f'MR{self.margins["right"][j].value():1.3f}, \n' + return str + + def reset_margins(self): + """ + Reset all the margins to zero. Must do this after changing + figure size, for instance, because the relative size of the + axes labels etc changes. + """ + for todo in ['left', 'right', 'bottom', 'top', + 'leftcb', 'rightcb', 'bottomcb', 'topcb']: + self.edit_margins(todo, 0.0) + + def add_constraints(self): + # define self-consistent constraints + self.hard_constraints() + # define relationship with parent layoutgrid: + self.parent_constraints() + # define relative widths of the grid cells to each other + # and stack horizontally and vertically. + self.grid_constraints() + + def hard_constraints(self): + """ + These are the redundant constraints, plus ones that make the + rest of the code easier. + """ + for i in range(self.ncols): + hc = [self.rights[i] >= self.lefts[i], + (self.rights[i] - self.margins['right'][i] - + self.margins['rightcb'][i] >= + self.lefts[i] - self.margins['left'][i] - + self.margins['leftcb'][i]) + ] + for c in hc: + self.solver.addConstraint(c | 'required') + + for i in range(self.nrows): + hc = [self.tops[i] >= self.bottoms[i], + (self.tops[i] - self.margins['top'][i] - + self.margins['topcb'][i] >= + self.bottoms[i] - self.margins['bottom'][i] - + self.margins['bottomcb'][i]) + ] + for c in hc: + self.solver.addConstraint(c | 'required') + + def add_child(self, child, i=0, j=0): + self.children[i, j] = child + + def parent_constraints(self): + # constraints that are due to the parent... + # i.e. the first column's left is equal to the + # parent's left, the last column right equal to the + # parent's right... + parent = self.parent + if parent is None: + hc = [self.lefts[0] == 0, + self.rights[-1] == 1, + # top and bottom reversed order... + self.tops[0] == 1, + self.bottoms[-1] == 0] + else: + rows, cols = self.parent_pos + rows = np.atleast_1d(rows) + cols = np.atleast_1d(cols) + + left = parent.lefts[cols[0]] + right = parent.rights[cols[-1]] + top = parent.tops[rows[0]] + bottom = parent.bottoms[rows[-1]] + if self.parent_inner: + # the layout grid is contained inside the inner + # grid of the parent. + left += parent.margins['left'][cols[0]] + left += parent.margins['leftcb'][cols[0]] + right -= parent.margins['right'][cols[-1]] + right -= parent.margins['rightcb'][cols[-1]] + top -= parent.margins['top'][rows[0]] + top -= parent.margins['topcb'][rows[0]] + bottom += parent.margins['bottom'][rows[-1]] + bottom += parent.margins['bottomcb'][rows[-1]] + hc = [self.lefts[0] == left, + self.rights[-1] == right, + # from top to bottom + self.tops[0] == top, + self.bottoms[-1] == bottom] + for c in hc: + self.solver.addConstraint(c | 'required') + + def grid_constraints(self): + # constrain the ratio of the inner part of the grids + # to be the same (relative to width_ratios) + + # constrain widths: + w = (self.rights[0] - self.margins['right'][0] - + self.margins['rightcb'][0]) + w = (w - self.lefts[0] - self.margins['left'][0] - + self.margins['leftcb'][0]) + w0 = w / self.width_ratios[0] + # from left to right + for i in range(1, self.ncols): + w = (self.rights[i] - self.margins['right'][i] - + self.margins['rightcb'][i]) + w = (w - self.lefts[i] - self.margins['left'][i] - + self.margins['leftcb'][i]) + c = (w == w0 * self.width_ratios[i]) + self.solver.addConstraint(c | 'strong') + # constrain the grid cells to be directly next to each other. + c = (self.rights[i - 1] == self.lefts[i]) + self.solver.addConstraint(c | 'strong') + + # constrain heights: + h = self.tops[0] - self.margins['top'][0] - self.margins['topcb'][0] + h = (h - self.bottoms[0] - self.margins['bottom'][0] - + self.margins['bottomcb'][0]) + h0 = h / self.height_ratios[0] + # from top to bottom: + for i in range(1, self.nrows): + h = (self.tops[i] - self.margins['top'][i] - + self.margins['topcb'][i]) + h = (h - self.bottoms[i] - self.margins['bottom'][i] - + self.margins['bottomcb'][i]) + c = (h == h0 * self.height_ratios[i]) + self.solver.addConstraint(c | 'strong') + # constrain the grid cells to be directly above each other. + c = (self.bottoms[i - 1] == self.tops[i]) + self.solver.addConstraint(c | 'strong') + + # Margin editing: The margins are variable and meant to + # contain things of a fixed size like axes labels, tick labels, titles + # etc + def edit_margin(self, todo, size, cell): + """ + Change the size of the margin for one cell. + + Parameters + ---------- + todo : string (one of 'left', 'right', 'bottom', 'top') + margin to alter. + + size : float + Size of the margin. If it is larger than the existing minimum it + updates the margin size. Fraction of figure size. + + cell : int + Cell column or row to edit. + """ + self.solver.suggestValue(self.margins[todo][cell], size) + self.margin_vals[todo][cell] = size + + def edit_margin_min(self, todo, size, cell=0): + """ + Change the minimum size of the margin for one cell. + + Parameters + ---------- + todo : string (one of 'left', 'right', 'bottom', 'top') + margin to alter. + + size : float + Minimum size of the margin . If it is larger than the + existing minimum it updates the margin size. Fraction of + figure size. + + cell: int + Cell column or row to edit. + """ + + if size > self.margin_vals[todo][cell]: + self.edit_margin(todo, size, cell) + + def edit_margins(self, todo, size): + """ + Change the size of all the margin of all the cells in the layout grid. + + Parameters + ---------- + todo : string (one of 'left', 'right', 'bottom', 'top') + margin to alter. + + size : float + Size to set the margins. Fraction of figure size. + """ + + for i in range(len(self.margin_vals[todo])): + self.edit_margin(todo, size, i) + + def edit_all_margins_min(self, todo, size): + """ + Change the minimum size of all the margin of all + the cells in the layout grid. + + Parameters + ---------- + todo: string (one of 'left', 'right', 'bottom', 'top') + margin to alter. + + size: float + Minimum size of the margin . If it is larger than the + existing minimum it updates the margin size. Fraction of + figure size. + """ + + for i in range(len(self.margin_vals[todo])): + self.edit_margin_min(todo, size, i) + + def edit_outer_margin_mins(self, margin, ss): + """ + Edit all four margin minimums in one statement. + + Parameters + ---------- + margin: dict + size of margins in a dict with keys 'left', 'right', 'bottom', + 'top' + + ss: SubplotSpec + defines the subplotspec these margins should be applied to + """ + + self.edit_margin_min('left', margin['left'], ss.colspan.start) + self.edit_margin_min('leftcb', margin['leftcb'], ss.colspan.start) + self.edit_margin_min('right', margin['right'], ss.colspan.stop - 1) + self.edit_margin_min('rightcb', margin['rightcb'], ss.colspan.stop - 1) + # rows are from the top down: + self.edit_margin_min('top', margin['top'], ss.rowspan.start) + self.edit_margin_min('topcb', margin['topcb'], ss.rowspan.start) + self.edit_margin_min('bottom', margin['bottom'], ss.rowspan.stop - 1) + self.edit_margin_min('bottomcb', margin['bottomcb'], + ss.rowspan.stop - 1) + + def get_margins(self, todo, col): + """Return the margin at this position""" + return self.margin_vals[todo][col] + + def get_outer_bbox(self, rows=0, cols=0): + """ + Return the outer bounding box of the subplot specs + given by rows and cols. rows and cols can be spans. + """ + rows = np.atleast_1d(rows) + cols = np.atleast_1d(cols) + + bbox = Bbox.from_extents( + self.lefts[cols[0]].value(), + self.bottoms[rows[-1]].value(), + self.rights[cols[-1]].value(), + self.tops[rows[0]].value()) + return bbox + + def get_inner_bbox(self, rows=0, cols=0): + """ + Return the inner bounding box of the subplot specs + given by rows and cols. rows and cols can be spans. + """ + rows = np.atleast_1d(rows) + cols = np.atleast_1d(cols) + + bbox = Bbox.from_extents( + (self.lefts[cols[0]].value() + + self.margins['left'][cols[0]].value() + + self.margins['leftcb'][cols[0]].value()), + (self.bottoms[rows[-1]].value() + + self.margins['bottom'][rows[-1]].value() + + self.margins['bottomcb'][rows[-1]].value()), + (self.rights[cols[-1]].value() - + self.margins['right'][cols[-1]].value() - + self.margins['rightcb'][cols[-1]].value()), + (self.tops[rows[0]].value() - + self.margins['top'][rows[0]].value() - + self.margins['topcb'][rows[0]].value()) + ) + return bbox + + def get_bbox_for_cb(self, rows=0, cols=0): + """ + Return the bounding box that includes the + decorations but, *not* the colorbar... + """ + rows = np.atleast_1d(rows) + cols = np.atleast_1d(cols) + + bbox = Bbox.from_extents( + (self.lefts[cols[0]].value() + + self.margins['leftcb'][cols[0]].value()), + (self.bottoms[rows[-1]].value() + + self.margins['bottomcb'][rows[-1]].value()), + (self.rights[cols[-1]].value() - + self.margins['rightcb'][cols[-1]].value()), + (self.tops[rows[0]].value() - + self.margins['topcb'][rows[0]].value()) + ) + return bbox + + def get_left_margin_bbox(self, rows=0, cols=0): + """ + Return the left margin bounding box of the subplot specs + given by rows and cols. rows and cols can be spans. + """ + rows = np.atleast_1d(rows) + cols = np.atleast_1d(cols) + + bbox = Bbox.from_extents( + (self.lefts[cols[0]].value() + + self.margins['leftcb'][cols[0]].value()), + (self.bottoms[rows[-1]].value()), + (self.lefts[cols[0]].value() + + self.margins['leftcb'][cols[0]].value() + + self.margins['left'][cols[0]].value()), + (self.tops[rows[0]].value())) + return bbox + + def get_bottom_margin_bbox(self, rows=0, cols=0): + """ + Return the left margin bounding box of the subplot specs + given by rows and cols. rows and cols can be spans. + """ + rows = np.atleast_1d(rows) + cols = np.atleast_1d(cols) + + bbox = Bbox.from_extents( + (self.lefts[cols[0]].value()), + (self.bottoms[rows[-1]].value() + + self.margins['bottomcb'][rows[-1]].value()), + (self.rights[cols[-1]].value()), + (self.bottoms[rows[-1]].value() + + self.margins['bottom'][rows[-1]].value() + + self.margins['bottomcb'][rows[-1]].value() + )) + return bbox + + def get_right_margin_bbox(self, rows=0, cols=0): + """ + Return the left margin bounding box of the subplot specs + given by rows and cols. rows and cols can be spans. + """ + rows = np.atleast_1d(rows) + cols = np.atleast_1d(cols) + + bbox = Bbox.from_extents( + (self.rights[cols[-1]].value() - + self.margins['right'][cols[-1]].value() - + self.margins['rightcb'][cols[-1]].value()), + (self.bottoms[rows[-1]].value()), + (self.rights[cols[-1]].value() - + self.margins['rightcb'][cols[-1]].value()), + (self.tops[rows[0]].value())) + return bbox + + def get_top_margin_bbox(self, rows=0, cols=0): + """ + Return the left margin bounding box of the subplot specs + given by rows and cols. rows and cols can be spans. + """ + rows = np.atleast_1d(rows) + cols = np.atleast_1d(cols) + + bbox = Bbox.from_extents( + (self.lefts[cols[0]].value()), + (self.tops[rows[0]].value() - + self.margins['topcb'][rows[0]].value()), + (self.rights[cols[-1]].value()), + (self.tops[rows[0]].value() - + self.margins['topcb'][rows[0]].value() - + self.margins['top'][rows[0]].value())) + return bbox + + def update_variables(self): + """ + Update the variables for the solver attached to this layoutgrid. + """ + self.solver.updateVariables() + +_layoutboxobjnum = itertools.count() + + +def seq_id(): + """Generate a short sequential id for layoutbox objects.""" + return '%06d' % next(_layoutboxobjnum) + + +def print_children(lb): + """Print the children of the layoutbox.""" + for child in lb.children: + print_children(child) + + +def plot_children(fig, lg, level=0, printit=False): + """Simple plotting to show where boxes are.""" + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + + fig.canvas.draw() + + colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] + col = colors[level] + for i in range(lg.nrows): + for j in range(lg.ncols): + bb = lg.get_outer_bbox(rows=i, cols=j) + fig.add_artist( + mpatches.Rectangle(bb.p0, bb.width, bb.height, linewidth=1, + edgecolor='0.7', facecolor='0.7', + alpha=0.2, transform=fig.transFigure, + zorder=-3)) + bbi = lg.get_inner_bbox(rows=i, cols=j) + fig.add_artist( + mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=2, + edgecolor=col, facecolor='none', + transform=fig.transFigure, zorder=-2)) + + bbi = lg.get_left_margin_bbox(rows=i, cols=j) + fig.add_artist( + mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0, + edgecolor='none', alpha=0.2, + facecolor=[0.5, 0.7, 0.5], + transform=fig.transFigure, zorder=-2)) + bbi = lg.get_right_margin_bbox(rows=i, cols=j) + fig.add_artist( + mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0, + edgecolor='none', alpha=0.2, + facecolor=[0.7, 0.5, 0.5], + transform=fig.transFigure, zorder=-2)) + bbi = lg.get_bottom_margin_bbox(rows=i, cols=j) + fig.add_artist( + mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0, + edgecolor='none', alpha=0.2, + facecolor=[0.5, 0.5, 0.7], + transform=fig.transFigure, zorder=-2)) + bbi = lg.get_top_margin_bbox(rows=i, cols=j) + fig.add_artist( + mpatches.Rectangle(bbi.p0, bbi.width, bbi.height, linewidth=0, + edgecolor='none', alpha=0.2, + facecolor=[0.7, 0.2, 0.7], + transform=fig.transFigure, zorder=-2)) + for ch in lg.children.flat: + if ch is not None: + plot_children(fig, ch, level=level+1) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 282517cba817..33fbf1061227 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -496,7 +496,9 @@ def __init__(self, fig, rect, self.set_figure(fig) self.set_box_aspect(box_aspect) self._axes_locator = None # Optionally set via update(kwargs). - + # placeholder for any colorbars added that use this axes. + # (see colorbar.py): + self._colorbars = [] self.spines = self._gen_axes_spines() # this call may differ for non-sep axes, e.g., polar @@ -568,15 +570,10 @@ def __init__(self, fig, rect, rcParams['ytick.major.right']), which='major') - self._layoutbox = None - self._poslayoutbox = None - def __getstate__(self): # The renderer should be re-created by the figure, and then cached at # that point. state = super().__getstate__() - for key in ['_layoutbox', '_poslayoutbox']: - state[key] = None # Prune the sharing & twinning info to only contain the current group. for grouper_name in [ '_shared_x_axes', '_shared_y_axes', '_twinned_axes']: @@ -914,8 +911,6 @@ def set_position(self, pos, which='both'): self._set_position(pos, which=which) # because this is being called externally to the library we # zero the constrained layout parts. - self._layoutbox = None - self._poslayoutbox = None def _set_position(self, pos, which='both'): """ diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index 2cd0934191bb..5f9be67809b8 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -36,8 +36,6 @@ def __init__(self, parent, orientation, location, functions, **kwargs): self._otherstrings = ['top', 'bottom'] self._parentscale = None # this gets positioned w/o constrained_layout so exclude: - self._layoutbox = None - self._poslayoutbox = None self.set_location(location) self.set_functions(functions) diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py index 1b5b181149c4..0273e51a1f79 100644 --- a/lib/matplotlib/axes/_subplots.py +++ b/lib/matplotlib/axes/_subplots.py @@ -5,7 +5,6 @@ import matplotlib.artist as martist from matplotlib.axes._axes import Axes from matplotlib.gridspec import GridSpec, SubplotSpec -import matplotlib._layoutbox as layoutbox class SubplotBase: @@ -40,23 +39,6 @@ def __init__(self, fig, *args, **kwargs): self.update_params() # _axes_class is set in the subplot_class_factory self._axes_class.__init__(self, fig, self.figbox, **kwargs) - # add a layout box to this, for both the full axis, and the poss - # of the axis. We need both because the axes may become smaller - # due to parasitic axes and hence no longer fill the subplotspec. - if self._subplotspec._layoutbox is None: - self._layoutbox = None - self._poslayoutbox = None - else: - name = self._subplotspec._layoutbox.name + '.ax' - name = name + layoutbox.seq_id() - self._layoutbox = layoutbox.LayoutBox( - parent=self._subplotspec._layoutbox, - name=name, - artist=self) - self._poslayoutbox = layoutbox.LayoutBox( - parent=self._layoutbox, - name=self._layoutbox.name+'.pos', - pos=True, subplot=True, artist=self) def __reduce__(self): # get the first axes class which does not inherit from a subplotbase @@ -159,10 +141,6 @@ def _make_twin_axes(self, *args, **kwargs): twin.set_label(real_label) self.set_adjustable('datalim') twin.set_adjustable('datalim') - if self._layoutbox is not None and twin._layoutbox is not None: - # make the layout boxes be explicitly the same - twin._layoutbox.constrain_same(self._layoutbox) - twin._poslayoutbox.constrain_same(self._poslayoutbox) self._twinned_axes.join(self, twin) return twin diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index d479dc19b550..f39813c1ec56 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -47,8 +47,6 @@ import matplotlib.path as mpath import matplotlib.ticker as ticker import matplotlib.transforms as mtransforms -import matplotlib._layoutbox as layoutbox -import matplotlib._constrained_layout as constrained_layout from matplotlib import docstring _log = logging.getLogger(__name__) @@ -1421,21 +1419,11 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, # because `plt.subplots` can return an ndarray and is natural to # pass to `colorbar`. parents = np.atleast_1d(parents).ravel() + fig = parents[0].get_figure() - # check if using constrained_layout: - try: - gs = parents[0].get_subplotspec().get_gridspec() - using_constrained_layout = (gs._layoutbox is not None) - except AttributeError: - using_constrained_layout = False - - # defaults are not appropriate for constrained_layout: - pad0 = loc_settings['pad'] - if using_constrained_layout: - pad0 = 0.02 + pad0 = 0.05 if fig.get_constrained_layout() else loc_settings['pad'] pad = kw.pop('pad', pad0) - fig = parents[0].get_figure() if not all(fig is ax.get_figure() for ax in parents): raise ValueError('Unable to create a colorbar axes as not all ' 'parents share the same figure.') @@ -1474,31 +1462,20 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, ax.set_anchor(parent_anchor) cax = fig.add_axes(pbcb, label="") - - # OK, now make a layoutbox for the cb axis. Later, we will use this - # to make the colorbar fit nicely. - if not using_constrained_layout: - # no layout boxes: - lb = None - lbpos = None - # and we need to set the aspect ratio by hand... - cax.set_aspect(aspect, anchor=anchor, adjustable='box') - else: - if not parents_iterable: - # this is a single axis... - ax = parents[0] - lb, lbpos = constrained_layout.layoutcolorbarsingle( - ax, cax, shrink, aspect, location, pad=pad) - else: # there is more than one parent, so lets use gridspec - # the colorbar will be a sibling of this gridspec, so the - # parent is the same parent as the gridspec. Either the figure, - # or a subplotspec. - - lb, lbpos = constrained_layout.layoutcolorbargridspec( - parents, cax, shrink, aspect, location, pad) - - cax._layoutbox = lb - cax._poslayoutbox = lbpos + for a in parents: + # tell the parent it has a colorbar + a._colorbars += [cax] + cax._colorbar_info = dict( + location=location, + parents=parents, + shrink=shrink, + anchor=anchor, + panchor=parent_anchor, + fraction=fraction, + aspect=aspect, + pad=pad) + # and we need to set the aspect ratio by hand... + cax.set_aspect(aspect, anchor=anchor, adjustable='box') return cax, kw @@ -1555,9 +1532,6 @@ def make_axes_gridspec(parent, *, location=None, orientation=None, pad_s = (1 - shrink) * 0.5 wh_ratios = [pad_s, shrink, pad_s] - # we need to none the tree of layoutboxes because constrained_layout can't - # remove and replace the tree hierarchy w/o a segfault. - layoutbox.nonetree(parent.get_subplotspec().get_gridspec()._layoutbox) if location == "left": gs = parent.get_subplotspec().subgridspec( 1, 2, wspace=wh_space, width_ratios=[fraction, 1-fraction-pad]) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index ce1dfd831721..4b9c7aeb4dd2 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -35,7 +35,7 @@ from matplotlib.text import Text from matplotlib.transforms import (Affine2D, Bbox, BboxTransformTo, TransformedBbox) -import matplotlib._layoutbox as layoutbox +import matplotlib._layoutgrid as layoutgrid _log = logging.getLogger(__name__) @@ -346,10 +346,10 @@ def __init__(self, subplotpars = SubplotParams() self.subplotpars = subplotpars + # constrained_layout: - self._layoutbox = None - # set in set_constrained_layout_pads() - self.set_constrained_layout(constrained_layout) + self._layoutgrid = None + self._constrained = False self.set_tight_layout(tight_layout) @@ -357,6 +357,11 @@ def __init__(self, self.clf() self._cachedRenderer = None + self.set_constrained_layout(constrained_layout) + # stub for subpanels: + self.panels = [] + self.transPanel = self.transFigure + # groupers to keep track of x and y labels we want to align. # see self.align_xlabels and self.align_ylabels and # axis._get_tick_boxes_siblings @@ -515,6 +520,8 @@ def set_constrained_layout(self, constrained): else: self.set_constrained_layout_pads() + self.init_layoutgrid() + self.stale = True def set_constrained_layout_pads(self, **kwargs): @@ -572,7 +579,7 @@ def get_constrained_layout_pads(self, relative=False): hspace = self._constrained_layout_pads['hspace'] if relative and (w_pad is not None or h_pad is not None): - renderer0 = layoutbox.get_renderer(self) + renderer0 = layoutgrid.get_renderer(self) dpi = renderer0.dpi w_pad = w_pad * dpi / renderer0.width h_pad = h_pad * dpi / renderer0.height @@ -731,22 +738,8 @@ def suptitle(self, t, **kwargs): sup.remove() else: self._suptitle = sup - self._suptitle._layoutbox = None - if self._layoutbox is not None and not manual_position: - w_pad, h_pad, wspace, hspace = \ - self.get_constrained_layout_pads(relative=True) - figlb = self._layoutbox - self._suptitle._layoutbox = layoutbox.LayoutBox( - parent=figlb, artist=self._suptitle, - name=figlb.name+'.suptitle') - # stack the suptitle on top of all the children. - # Some day this should be on top of all the children in the - # gridspec only. - for child in figlb.children: - if child is not self._suptitle._layoutbox: - layoutbox.vstack([self._suptitle._layoutbox, - child], - padding=h_pad*2., strength='required') + if manual_position: + self._suptitle.set_in_layout(False) self.stale = True return self._suptitle @@ -1406,6 +1399,7 @@ def add_subplot(self, *args, **kwargs): def _add_axes_internal(self, key, ax): """Private helper for `add_axes` and `add_subplot`.""" + #self._localaxes += [ax] self._axstack.add(key, ax) self.sca(ax) ax._remove_method = self.delaxes @@ -1771,6 +1765,7 @@ def _break_share_link(ax, grouper): return None self._axstack.remove(ax) + # self._localaxes.remove(ax) self._axobservers.process("_axes_change_event", self) self.stale = True @@ -1810,7 +1805,7 @@ def clf(self, keep_observers=False): self._axobservers = cbook.CallbackRegistry() self._suptitle = None if self.get_constrained_layout(): - layoutbox.nonetree(self._layoutbox) + self.init_layoutgrid() self.stale = True def clear(self, keep_observers=False): @@ -2169,12 +2164,9 @@ def __getstate__(self): in _pylab_helpers.Gcf.figs.values(): state['_restore_to_pylab'] = True - # set all the layoutbox information to None. kiwisolver objects can't + # set all the layoutgrid information to None. kiwisolver objects can't # be pickled, so we lose the layout options at this point. - state.pop('_layoutbox', None) - # suptitle: - if self._suptitle is not None: - self._suptitle._layoutbox = None + state.pop('_layoutgrid', None) return state @@ -2191,7 +2183,7 @@ def __setstate__(self, state): # re-initialise some of the unstored state information FigureCanvasBase(self) # Set self.canvas. - self._layoutbox = None + self._layoutgrid = None if restore_to_pylab: # lazy import to avoid circularity @@ -2578,24 +2570,24 @@ def get_tightbbox(self, renderer, bbox_extra_artists=None): return bbox_inches - def init_layoutbox(self): - """Initialize the layoutbox for use in constrained_layout.""" - if self._layoutbox is None: - self._layoutbox = layoutbox.LayoutBox( - parent=None, name='figlb', artist=self) - self._layoutbox.constrain_geometry(0., 0., 1., 1.) + def init_layoutgrid(self): + """Initialize the layoutgrid for use in constrained_layout.""" + del(self._layoutgrid) + self._layoutgrid = layoutgrid.LayoutGrid( + parent=None, name='figlb') def execute_constrained_layout(self, renderer=None): """ - Use ``layoutbox`` to determine pos positions within axes. + Use ``layoutgrid`` to determine pos positions within axes. See also `.set_constrained_layout_pads`. """ from matplotlib._constrained_layout import do_constrained_layout + from matplotlib.tight_layout import get_renderer _log.debug('Executing constrainedlayout') - if self._layoutbox is None: + if self._layoutgrid is None: cbook._warn_external("Calling figure.constrained_layout, but " "figure not setup to do constrained layout. " " You either called GridSpec without the " @@ -2610,7 +2602,7 @@ def execute_constrained_layout(self, renderer=None): w_pad = w_pad / width h_pad = h_pad / height if renderer is None: - renderer = layoutbox.get_renderer(fig) + renderer = get_renderer(fig) do_constrained_layout(fig, renderer, h_pad, w_pad, hspace, wspace) @cbook._delete_parameter("3.2", "renderer") diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 3764d5af46ec..192d22173d5b 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -18,7 +18,8 @@ import matplotlib as mpl from matplotlib import _pylab_helpers, cbook, tight_layout, rcParams from matplotlib.transforms import Bbox -import matplotlib._layoutbox as layoutbox +import matplotlib._layoutgrid as layoutgrid + _log = logging.getLogger(__name__) @@ -397,7 +398,7 @@ def __init__(self, nrows, ncols, figure=None, The number of rows and columns of the grid. figure : `~.figure.Figure`, optional - Only used for constrained layout to create a proper layoutbox. + Only used for constrained layout to create a proper layoutgrid. left, right, top, bottom : float, optional Extent of the subplots as a fraction of figure width or height. @@ -440,22 +441,24 @@ def __init__(self, nrows, ncols, figure=None, width_ratios=width_ratios, height_ratios=height_ratios) + # set up layoutgrid for constrained_layout: + self._layoutgrid = None if self.figure is None or not self.figure.get_constrained_layout(): - self._layoutbox = None + self._layoutgrid = None else: - self.figure.init_layoutbox() - self._layoutbox = layoutbox.LayoutBox( - parent=self.figure._layoutbox, - name='gridspec' + layoutbox.seq_id(), - artist=self) - # by default the layoutbox for a gridspec will fill a figure. - # but this can change below if the gridspec is created from a - # subplotspec. (GridSpecFromSubplotSpec) + self._toplayoutbox = self.figure._layoutgrid + self._layoutgrid = layoutgrid.LayoutGrid( + parent=self.figure._layoutgrid, + parent_inner=True, + name=(self.figure._layoutgrid.name + '.gridspec' + + layoutgrid.seq_id()), + ncols=ncols, nrows=nrows, width_ratios=width_ratios, + height_ratios=height_ratios) _AllowedKeys = ["left", "bottom", "right", "top", "wspace", "hspace"] def __getstate__(self): - return {**self.__dict__, "_layoutbox": None} + return {**self.__dict__, "_layoutgrid": None} def update(self, **kwargs): """ @@ -584,16 +587,26 @@ def __init__(self, nrows, ncols, super().__init__(nrows, ncols, width_ratios=width_ratios, height_ratios=height_ratios) - # do the layoutboxes - subspeclb = subplot_spec._layoutbox + # do the layoutgrids for constrained_layout: + subspeclb = subplot_spec.get_gridspec()._layoutgrid if subspeclb is None: - self._layoutbox = None + self._layoutgrid = None else: - # OK, this is needed to divide the figure. - self._layoutbox = subspeclb.layout_from_subplotspec( - subplot_spec, - name=subspeclb.name + '.gridspec' + layoutbox.seq_id(), - artist=self) + # this _toplayoutbox is a container that spans the cols and + # rows in the parent gridspec. Not yet implemented, + # but we do this so that it is possible to have subgridspec + # level artists. + self._toplayoutgrid = layoutgrid.LayoutGrid( + parent=subspeclb, + name=subspeclb.name + '.top' + layoutgrid.seq_id(), + nrows=1, ncols=1, + parent_pos=(subplot_spec.rowspan, subplot_spec.colspan)) + self._layoutgrid = layoutgrid.LayoutGrid( + parent=self._toplayoutgrid, + name=(self._toplayoutgrid.name + '.gridspec' + + layoutgrid.seq_id()), + nrows=nrows, ncols=ncols, + width_ratios=width_ratios, height_ratios=height_ratios) def get_subplot_params(self, figure=None): """Return a dictionary of subplot layout parameters.""" @@ -642,18 +655,6 @@ def __init__(self, gridspec, num1, num2=None): self._gridspec = gridspec self.num1 = num1 self.num2 = num2 - if gridspec._layoutbox is not None: - glb = gridspec._layoutbox - # So note that here we don't assign any layout yet, - # just make the layoutbox that will contain all items - # associated w/ this axis. This can include other axes like - # a colorbar or a legend. - self._layoutbox = layoutbox.LayoutBox( - parent=glb, - name=glb.name + '.ss' + layoutbox.seq_id(), - artist=self) - else: - self._layoutbox = None def __repr__(self): return (f"{self.get_gridspec()}[" @@ -727,7 +728,7 @@ def num2(self, value): self._num2 = value def __getstate__(self): - return {**self.__dict__, "_layoutbox": None} + return {**self.__dict__} def get_gridspec(self): return self._gridspec diff --git a/lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png b/lib/matplotlib/tests/baseline_images/test_axes/secondary_xy.png index b9c79c1b2a9bf8b2faeba5b20ac5e76158234ed4..bbf9f9e13211db95a236de3696bfd31d5aac2ac7 100644 GIT binary patch literal 48775 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�e8YV$rt@f5{+oe7y+?0y5EL}q&G=~C6MCBahv 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 z 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 b7CtJ&Rm@Kjn_Dht;I($mdaG>9E&KlhWfvw; literal 42023 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