diff --git a/doc/api/figure_api.rst b/doc/api/figure_api.rst index ef577e7ef17f..d0b5cb0592c7 100644 --- a/doc/api/figure_api.rst +++ b/doc/api/figure_api.rst @@ -15,8 +15,9 @@ Classes :toctree: _as_gen/ :template: autosummary.rst :nosignatures: + :inhereted-members: - AxesStack + SubPanel Figure SubplotParams diff --git a/doc/users/prev_whats_new/whats_new_3.0.rst b/doc/users/prev_whats_new/whats_new_3.0.rst index 870f33bb0d5f..8e21b259f0fa 100644 --- a/doc/users/prev_whats_new/whats_new_3.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.0.rst @@ -214,7 +214,7 @@ to a figure. E.g. :: fig.add_artist(circ) In case the added artist has no transform set previously, it will be set to -the figure transform (``fig.transFigure``). +the figure transform (``fig.transPanel``). This new method may be useful for adding artists to figures without axes or to easily position static elements in figure coordinates. diff --git a/examples/axes_grid1/inset_locator_demo.py b/examples/axes_grid1/inset_locator_demo.py index 5fd0bc926794..f4eec13bff7f 100644 --- a/examples/axes_grid1/inset_locator_demo.py +++ b/examples/axes_grid1/inset_locator_demo.py @@ -136,7 +136,7 @@ # Create an inset horizontally centered in figure coordinates and vertically # bound to line up with the axes. from matplotlib.transforms import blended_transform_factory -transform = blended_transform_factory(fig.transFigure, ax2.transAxes) +transform = blended_transform_factory(fig.transPanel, ax2.transAxes) axins4 = inset_axes(ax2, width="16%", height="34%", bbox_to_anchor=(0, 0, 1, 1), bbox_transform=transform, loc=8, borderpad=0) diff --git a/examples/pyplots/auto_subplots_adjust.py b/examples/pyplots/auto_subplots_adjust.py index 40c88a8e07cc..731a507c412d 100644 --- a/examples/pyplots/auto_subplots_adjust.py +++ b/examples/pyplots/auto_subplots_adjust.py @@ -26,7 +26,7 @@ def on_draw(event): bbox = label.get_window_extent() # the figure transform goes from relative coords->pixels and we # want the inverse of that - bboxi = bbox.transformed(fig.transFigure.inverted()) + bboxi = bbox.transformed(fig.transPanel.inverted()) bboxes.append(bboxi) # the bbox that bounds all the bboxes, again in relative figure coords bbox = mtransforms.Bbox.union(bboxes) diff --git a/examples/subplots_axes_and_figures/gridspec_nested.py b/examples/subplots_axes_and_figures/gridspec_nested.py index 20c211689e68..c9e70f409943 100644 --- a/examples/subplots_axes_and_figures/gridspec_nested.py +++ b/examples/subplots_axes_and_figures/gridspec_nested.py @@ -16,7 +16,6 @@ def format_axes(fig): ax.text(0.5, 0.5, "ax%d" % (i+1), va="center", ha="center") ax.tick_params(labelbottom=False, labelleft=False) - # gridspec inside gridspec fig = plt.figure() diff --git a/examples/subplots_axes_and_figures/subpanels.py b/examples/subplots_axes_and_figures/subpanels.py new file mode 100644 index 000000000000..cced05441f37 --- /dev/null +++ b/examples/subplots_axes_and_figures/subpanels.py @@ -0,0 +1,48 @@ +""" +================ +Nested Subpanels +================ + +Sometimes it is desirable to have a figure that has two different +layouts in it. This can be achieved with +:doc:`nested gridspecs` +but having a virtual figure with its own artists is helpful, so +Matplotlib also has "subpanels", usually implimented by calling +``.figure.PanelBase.add_subpanel`` in a way that is analagous to +``.figure.PanelBase.add_subplot``. + +""" +import matplotlib.pyplot as plt +import numpy as np + +def example_plot(ax, fontsize=12, hide_labels=False): + ax.pcolormesh(np.random.randn(30, 30)) + if not hide_labels: + ax.set_xlabel('x-label', fontsize=fontsize) + ax.set_ylabel('y-label', fontsize=fontsize) + ax.set_title('Title', fontsize=fontsize) + + +# gridspec inside gridspec +fig = plt.figure(constrained_layout=True) +subpanels = fig.subpanels(1, 2, wspace=0.07) + +axsLeft = subpanels[0].subplots(1, 2, sharey=True) +subpanels[0].set_facecolor('0.75') +for ax in axsLeft: + example_plot(ax) +subpanels[0].suptitle('Left plots', fontsize='x-large') + +axsRight = subpanels[1].subplots(3, 1, sharex=True) +for nn, ax in enumerate(axsRight): + example_plot(ax, hide_labels=True) + if nn == 2: + ax.set_xlabel('xlabel') + if nn == 1: + ax.set_ylabel('ylabel') +subpanels[1].suptitle('Right plots', fontsize='x-large') + +fig.suptitle('Figure suptitle', fontsize='xx-large') + +plt.show() + diff --git a/examples/subplots_axes_and_figures/subplots_demo.py b/examples/subplots_axes_and_figures/subplots_demo.py index 522275decf1e..2f10e56c0e95 100644 --- a/examples/subplots_axes_and_figures/subplots_demo.py +++ b/examples/subplots_axes_and_figures/subplots_demo.py @@ -144,7 +144,7 @@ # Still there remains an unused empty space between the subplots. # # To precisely control the positioning of the subplots, one can explicitly -# create a `.GridSpec` with `.add_gridspec`, and then call its +# create a `.GridSpec` with `~.Figure.add_gridspec`, and then call its # `~.GridSpecBase.subplots` method. For example, we can reduce the height # between vertical subplots using ``add_gridspec(hspace=0)``. # diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 90faebc1525a..4b80603bafa2 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. Its 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. +""" ###################################################### @@ -87,177 +78,328 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, elements. hspace, wspace : float - are in fractions of the subplot sizes. - + are 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. """ - # 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_ok(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_ok(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_ok(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) + + nrows, ncols = gs.get_geometry() + margin = {'left': w_pad, 'right': w_pad, + 'bottom': h_pad, 'top': h_pad} + if ss.colspan.start > 0: + margin['left'] = _wspace / ncols / 2 + if ss.colspan.stop < ncols: + margin['right'] = _wspace / ncols / 2 + if ss.rowspan.stop < nrows: + margin['bottom'] = _hspace / nrows / 2 + if ss.rowspan.start > 0: + margin['top'] = _hspace / nrows / 2 + + 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')]: + + 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) + + 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'] += -bbox.x0 + pos.x0 + margin['right'] += bbox.x1 - pos.x1 + # remember that rows are ordered from top: + margin['bottom'] += pos.y0 - bbox.y0 + margin['top'] += -pos.y1 + bbox.y1 + + # increase margin for colorbars... + for cbax in ax._colorbars: + # colorbars can be child of more than one subplot spec: + cbp_rspan, cbp_cspan = _get_cb_parent_spans(cbax) + loc = cbax._colorbar_info['location'] + if loc in ['right', 'left']: + pass + if loc == 'right': + if cbp_cspan.stop == ss.colspan.stop: + # only increase if the colorbar is on the right edge + cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer) + margin['right'] += cbbbox.width + w_pad * 2 + elif loc == 'left': + if cbp_cspan.start == ss.colspan.start: + # only increase if the colorbar is on the left edge + cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer) + margin['left'] += cbbbox.width + w_pad * 2 + elif loc == 'top': + if cbp_rspan.start == ss.rowspan.start: + cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer) + margin['top'] += cbbbox.height + h_pad * 2 + else: + if cbp_rspan.stop == ss.rowspan.stop: + cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer) + margin['bottom'] += cbbbox.height + h_pad * 2 + + # 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 + pan2fig = fig.transPanel + fig.transFigure.inverted() + + w_pad, h_pad = pan2fig.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) + + if 0: + # Not implimented yet! + if fig._supxlabel is not None: + bbox = invTransFig(fig._supxlabel.get_tightbbox(renderer)) + fig._layoutgrid.edit_margin_min('bottom', bbox.height + 2 * h_pad) + + if fig._supylabel is not None: + bbox = invTransFig(fig._supylabel.get_tightbbox(renderer)) + fig._layoutgrid.edit_margin_min('left', bbox.width + 2 * w_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. + + i.e. if in row 0, the axes is at columns 0, 1 and for row 1, + the axes is at columns 1 and 2, then the right margin at row 0 + needs to be the same size as the right margin at row 1 and + the left margin for rows 1 and 2 should be the same. + + 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._localaxes 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: + nc = len(ss1.colspan) + if nc > 1: + maxsubl = np.max( + lg1.margin_vals['left'][ss1.colspan[1:]]) + maxsubr = np.max( + lg1.margin_vals['right'][ss1.colspan[:-1]]) + for ax2 in axs: + ss2 = ax2.get_subplotspec() + lg2 = ss2.get_gridspec()._layoutgrid + if lg2 is not None: + nc = len(ss2.colspan) + if nc > 1: + maxsubl2 = np.max( + lg2.margin_vals['left'][ss2.colspan[1:]]) + if maxsubl2 > maxsubl: + maxsubl = maxsubl2 + maxsubr2 = np.max( + lg2.margin_vals['right'][ss2.colspan[:-1]]) + if maxsubr2 > maxsubr: + maxsubr = maxsubr2 + for i in ss1.colspan[1:]: + lg1.edit_margin_min('left', maxsubl, col=i) + for i in ss1.colspan[:-1]: + lg1.edit_margin_min('right', maxsubr, col=i) + + # interior rows: + nc = len(ss1.rowspan) + if nc > 1: + maxsubt = np.max( + lg1.margin_vals['top'][ss1.rowspan[1:]]) + maxsubb = np.max( + lg1.margin_vals['bottom'][ss1.rowspan[:-1]]) + + for ax2 in axs: + ss2 = ax2.get_subplotspec() + lg2 = ss2.get_gridspec()._layoutgrid + if lg2 is not None: + nc = len(ss2.rowspan) + if nc > 1: + maxsubt2 = np.max( + lg2.margin_vals['top'][ss2.rowspan[1:]]) + if maxsubt2 > maxsubt: + maxsubt = maxsubt2 + maxsubb2 = np.max( + lg2.margin_vals['bottom'][ss2.rowspan[:-1]]) + if maxsubb2 > maxsubb: + maxsubb = maxsubb2 + for i in ss1.rowspan[1:]: + lg1.edit_margin_min('top', maxsubt, col=i) + for i in ss1.rowspan[:-1]: + lg1.edit_margin_min('bottom', maxsubb, col=i) + + +def _get_cb_parent_spans(cbax): + """ + Figure out which subplotspecs this colorbar belongs to + """ + rowspan = range(100, -100, -100) + colspan = range(100, -100, -100) + for parent in cbax._colorbar_info['parents']: + ss = parent.get_subplotspec() + mn = min([ss.rowspan.start, rowspan.start]) + mx = max([ss.rowspan.stop, rowspan.stop]) + rowspan = range(mn, mx) + colspan = range(min([ss.colspan.start, colspan.start]), + max([ss.colspan.stop, colspan.stop])) + + 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 co-oridnates + bbox : Bbox + tight bounding box in figure co-ordinates. + """ fig = ax.figure - invTransFig = fig.transFigure.inverted().transform_bbox + invTransFig = fig.transFigure.inverted() + 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+invTransFig) try: tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True) except TypeError: @@ -266,397 +408,154 @@ 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(invTransFig) + 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() - if width_ratios is None: - width_ratios = np.ones(ncols) - if height_ratios is None: - height_ratios = np.ones(nrows) - - # 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.inverted() + + 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 [a for a in fig._localaxes if hasattr(a, 'get_subplotspec')]: + + # grid bbox is in Figure co-ordinates, but we specify in panel + # co-ordinates... + ss = ax.get_subplotspec() + gs = ss.get_gridspec() + 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: + oldw = {'right': 0, 'left': 0} + oldh = {'bottom': 0, 'top': 0} + for nn, cbax in enumerate(ax._colorbars): + if ax == cbax._colorbar_info['parents'][0]: + oldw, oldh = _reposition_colorbar( + cbax, renderer, + w_pad=w_pad, h_pad=h_pad, + wspace=wspace, hspace=hspace, oldw=oldw, oldh=oldh) + + +def _reposition_colorbar(cbax, renderer, *, w_pad=0, h_pad=0, hspace=0, + wspace=0, oldw=0, oldh=0): + """ - Do the layout for a colorbar, to not overly pollute colorbar.py + Place the colorbar in its new place. - *pad* is in fraction of the original axis size. + Parameters + ---------- + cbax : Axes for the colorbar + + 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 + oldw, oldh : float + 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) + 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.inverted() + + cb_rspans, cb_cspans = _get_cb_parent_spans(cbax) + bboxouter = gs._layoutgrid.get_outer_bbox(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'] + pad = cbax._colorbar_info['pad'] + parent_anchor = cbax._colorbar_info['panchor'] + + cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer) + + if cb_cspans.stop == ncols: + wspace = 0 + if cb_rspans.start == 0: + hspace = 0 + + # 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: if location in ('left', 'right'): - lbpos = layoutbox.LayoutBox( - parent=lb, - name=lb.name + '.pos', - tightwidth=False, - pos=True, - subplot=False, - artist=cax) - + # fraction and shrink are fractions of parent + pbcb = pb.shrunk(fraction, shrink).anchored(anchor, pb) + # now we need to translate the parent. if location == 'right': - # arrange to right of parent axis - layoutbox.hstack([axlb, lb], padding=pad * axlb.width, - strength='strong') + margin = cbbbox.x1 - cbpos.x1 # decorators on CB + dx = bboxouter.x1 - pbcb.x1 + dx = dx - w_pad - oldw['right'] - margin - wspace / ncols / 2 + oldw['right'] += cbbbox.width + 2 * w_pad + 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) + margin = cbpos.x0 - cbbbox.x0 + dx = bboxouter.x0 - pbcb.x0 + dx = dx + w_pad + oldw['left'] + margin + wspace / ncols / 2 + oldw['left'] += cbbbox.width + 2 * w_pad + pbcb = pbcb.translated(dx, 0) + else: # horizontal axes: + pbcb = pb.shrunk(shrink, fraction).anchored(anchor, pb) + if location == 'top': + margin = cbbbox.y1 - cbpos.y1 + dy = bboxouter.y1 - pbcb.y1 + dy = dy - h_pad - oldh['top'] - margin - hspace / nrows / 2 + oldh['top'] += cbbbox.height + 2 * h_pad + 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') + margin = cbpos.y0 - cbbbox.y0 + dy = bboxouter.y0 - pbcb.y0 + dy = dy + h_pad + oldh['bottom'] + margin + hspace / nrows / 2 + oldh['bottom'] += cbbbox.height + 2 * h_pad + 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 oldw, oldh -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 usualy set as a minimum, so if the figure gets smaller + the minimum needs to be zero in order for it to grow again. """ - - 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) - - 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 + 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() diff --git a/lib/matplotlib/_layoutbox.py b/lib/matplotlib/_layoutbox.py deleted file mode 100644 index 0afa2e4829f2..000000000000 --- a/lib/matplotlib/_layoutbox.py +++ /dev/null @@ -1,695 +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=None, strength='medium'): - """Stack LayoutBox instances from top to bottom.""" - - if height_ratios is None: - height_ratios = np.ones(len(boxes)) - 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=None, strength='medium'): - """Stack LayoutBox instances from top to bottom.""" - - if width_ratios is None: - width_ratios = np.ones(len(boxes)) - 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 vstackeq(boxes, padding=0, height_ratios=None): - vstack(boxes, padding=padding) - match_heights(boxes, height_ratios=height_ratios) - - -def hstackeq(boxes, padding=0, width_ratios=None): - hstack(boxes, padding=padding) - match_widths(boxes, width_ratios=width_ratios) - - -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..66a1963568dd --- /dev/null +++ b/lib/matplotlib/_layoutgrid.py @@ -0,0 +1,551 @@ +""" +A layoutgrid is a nrows by ncols set of boxes, meant to be used by +`._constrained_layout`, each box is aalagous to a subplotspec element of +a gridspec. + +Each box is defined by left[ncols], right[ncols], bottom[nrow] and top[nrows], +and by an editable margin for each side that gets its value set by +the size of ticklabels, titles, etc on each axes that is in the figure. 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__) + + +# 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 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']: + self.margins[todo] = np.empty((ncols), dtype=object) + # track the value so we can change only if a margin is larger + # than the current value + self.margin_vals[todo] = np.zeros(ncols) + + # 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 = np.empty((ncols), dtype=object) + self.lefts = np.empty((ncols), dtype=object) + self.rights = np.empty((ncols), dtype=object) + self.inner_widths = np.empty((ncols), dtype=object) + + # make the variables: + sol = self.solver + for i in range(self.ncols): + for todo in ['left', 'right']: + self.margins[todo][i] = Variable(f'{sn}margins[{todo}][{i}]') + sol.addEditVariable(self.margins[todo][i], 'strong') + self.rights[i] = Variable(f'{sn}rights[{i}]') + self.lefts[i] = Variable(f'{sn}lefts[{i}]') + self.widths[i] = Variable(f'{sn}widths[{i}]') + self.inner_widths[i] = Variable(f'{sn}inner_widths[{i}]') + + for todo in ['bottom', 'top']: + self.margins[todo] = np.empty((nrows), dtype=object) + self.margin_vals[todo] = np.zeros(nrows) + + self.heights = np.empty((nrows), dtype=object) + self.inner_heights = np.empty((nrows), dtype=object) + self.bottoms = np.empty((nrows), dtype=object) + self.tops = np.empty((nrows), dtype=object) + + for i in range(self.nrows): + for todo in ['bottom', 'top']: + self.margins[todo][i] = Variable(f'{sn}margins[{todo}][{i}]') + sol.addEditVariable(self.margins[todo][i], 'strong') + self.bottoms[i] = Variable(f'{sn}bottoms[{i}]') + self.tops[i] = Variable(f'{sn}tops[{i}]') + self.inner_heights[i] = Variable(f'{sn}inner_heights[{i}]') + self.heights[i] = Variable(f'{sn}heights[{i}]') + + # 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 sinatnce, because the relative size of the + axes labels etc changes. + """ + for todo in ['left', 'right', 'bottom', 'top']: + self.edit_margins(todo, 0.0) + + def add_constraints(self): + # define self-consistent constraints + self.hard_constraints() + # define relationship with parent layoutgrid: + self.parent_constrain() + # 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.lefts[i] - self.margins['left'][i])] + for c in hc: + self.solver.addConstraint(c | 'required') + + for i in range(self.nrows): + hc = [self.heights[i] == self.tops[i] - self.bottoms[i], + self.tops[i] >= self.bottoms[i], + (self.tops[i] - self.margins['top'][i] >= + self.bottoms[i] - self.margins['bottom'][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_constrain(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 self.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]] + right -= parent.margins['right'][cols[-1]] + top -= parent.margins['top'][rows[0]] + bottom += parent.margins['bottom'][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: + iw = self.rights[0] - self.margins['right'][0] + iw = iw - self.lefts[0] - self.margins['left'][0] + w0 = iw / self.width_ratios[0] + # from left to right + for i in range(1, self.ncols): + iw = self.rights[i] - self.margins['right'][i] + iw = iw - self.lefts[i] - self.margins['left'][i] + w = iw + 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: + ih = self.tops[0] - self.margins['top'][0] + ih = ih - self.bottoms[0] - self.margins['bottom'][0] + h0 = ih / self.height_ratios[0] + # from top to bottom: + for i in range(1, self.nrows): + ih = self.tops[i] - self.margins['top'][i] + h = ih - self.bottoms[i] - self.margins['bottom'][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 fixes size like axes labels, tick labels, titles + # etc + def edit_margin(self, todo, width, col): + """ + Change the size of the margin for one cell. + + Parameters + ---------- + todo : string (one of 'left', 'right', 'bottom', 'top') + margin to alter. + + width : float + Size of the margin. If it is larger than the existing minimum it + updates the margin size. Fraction of figure size. + + col : int + Cell column or row to edit. + """ + + self.solver.suggestValue(self.margins[todo][col], width) + self.margin_vals[todo][col] = width + + def edit_margin_min(self, todo, width, col=0): + """ + Change the minimum size of the margin for one cell. + + Parameters + ---------- + todo : string (one of 'left', 'right', 'bottom', 'top') + margin to alter. + + width : float + Minimum size of the margin . If it is larger than the + existig minimum it updates the margin size. Fraction of + figure size. + + col: int + Cell column or row to edit. + """ + + if width > self.margin_vals[todo][col]: + self.edit_margin(todo, width, col) + + def edit_margins(self, todo, width): + """ + 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. + + width : float + Size to set the margins. Fraction of figure size. + """ + + for i in range(len(self.margin_vals[todo])): + self.edit_margin(todo, width, i) + + def edit_margins_min(self, todo, width): + """ + 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. + + width: float + Minimum size of the margin . If it is larger than the + existig minimum it updates the margin size. Fraction of + figure size. + """ + + for i in range(len(self.margin_vals[todo])): + self.edit_margin_min(todo, width, 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('right', margin['right'], ss.colspan.stop - 1) + # rows are from the top down: + self.edit_margin_min('top', margin['top'], ss.rowspan.start) + self.edit_margin_min('bottom', margin['bottom'], 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.bottoms[rows[-1]].value() + + self.margins['bottom'][rows[-1]].value()), + (self.rights[cols[-1]].value() - + self.margins['right'][cols[-1]].value()), + (self.tops[rows[0]].value() - + self.margins['top'][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.bottoms[rows[-1]].value()), + (self.lefts[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.rights[cols[-1]].value()), + (self.bottoms[rows[-1]].value() + + self.margins['bottom'][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.bottoms[rows[-1]].value()), + (self.rights[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.rights[cols[-1]].value()), + (self.tops[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 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): + if lb is None: + return + for child in lb.children.flat: + nonechildren(child) + lb = None + + +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/_axes.py b/lib/matplotlib/axes/_axes.py index f2d5fbc7f95a..c902273d3712 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -57,7 +57,7 @@ def _make_inset_locator(bounds, trans, parent): def inset_locator(ax, renderer): bbox = _bounds bb = mtransforms.TransformedBbox(bbox, _trans) - tr = _parent.figure.transFigure.inverted() + tr = _parent.figure.transPanel.inverted() bb = mtransforms.TransformedBbox(bb, tr) return bb @@ -577,7 +577,7 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, # decide which two of the lines to keep visible.... pos = inset_ax.get_position() - bboxins = pos.transformed(self.figure.transFigure) + bboxins = pos.transformed(self.figure.transPanel) rectbbox = mtransforms.Bbox.from_bounds( *bounds ).transformed(transform) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 8852f12bfdd9..18581eb40037 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -564,9 +564,7 @@ def __init__(self, fig, rect, labelright=(rcParams['ytick.labelright'] and rcParams['ytick.major.right']), which='major') - - self._layoutbox = None - self._poslayoutbox = None + self._colorbars = [] def __getstate__(self): # The renderer should be re-created by the figure, and then cached at @@ -625,7 +623,7 @@ def set_figure(self, fig): martist.Artist.set_figure(self, fig) self.bbox = mtransforms.TransformedBbox(self._position, - fig.transFigure) + fig.transPanel) # these will be updated later as data is added self.dataLim = mtransforms.Bbox.null() self._viewLim = mtransforms.Bbox.unit() @@ -905,10 +903,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'): """ @@ -1601,8 +1595,10 @@ def apply_aspect(self, position=None): self._set_position(position, which='active') return - fig_width, fig_height = self.get_figure().get_size_inches() - fig_aspect = fig_height / fig_width + trans = self.get_figure().transPanel + bb = mtransforms.Bbox.from_bounds(0, 0, 1, 1).transformed(trans) + # this is the physical aspect of the panel (or figure): + fig_aspect = bb.height / bb.width if self._adjustable == 'box': if self in self._twinned_axes: diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index 505bf80b2a8a..3e534d35fd07 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -27,7 +27,7 @@ def secondary_locator(ax, renderer): # delay evaluating transform until draw time because the # parent transform may have changed (i.e. if window reesized) bb = mtransforms.TransformedBbox(_rect, parent.transAxes) - tr = parent.figure.transFigure.inverted() + tr = parent.figure.transPanel.inverted() bb = mtransforms.TransformedBbox(bb, tr) return bb diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py index e40754a2acbc..67d7a82c7e2b 100644 --- a/lib/matplotlib/axes/_subplots.py +++ b/lib/matplotlib/axes/_subplots.py @@ -5,8 +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: """ @@ -37,23 +35,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 @@ -156,10 +137,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 7860631e0ce0..97f917d181dd 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -48,7 +48,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 @@ -1433,17 +1432,8 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, # pass to `colorbar`. parents = np.atleast_1d(parents).ravel() - # 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 pad = kw.pop('pad', pad0) fig = parents[0].get_figure() @@ -1485,31 +1475,21 @@ 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 = {} + cax._colorbar_info['location'] = location + cax._colorbar_info['parents'] = parents + cax._colorbar_info['shrink'] = shrink + cax._colorbar_info['anchor'] = anchor + cax._colorbar_info['panchor'] = parent_anchor + cax._colorbar_info['fraction'] = fraction + cax._colorbar_info['aspect'] = aspect + cax._colorbar_info['pad'] = pad + + # and we need to set the aspect ratio by hand... + cax.set_aspect(aspect, anchor=anchor, adjustable='box') return cax, kw @@ -1564,7 +1544,6 @@ def make_axes_gridspec(parent, *, fraction=0.15, shrink=1.0, aspect=20, **kw): # constrained_layout can't remove and replace the tree # hierarchy w/o a seg fault. gs = parent.get_subplotspec().get_gridspec() - layoutbox.nonetree(gs._layoutbox) gs_from_subplotspec = gridspec.GridSpecFromSubplotSpec if orientation == 'vertical': pad = kw.pop('pad', 0.05) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 846e112bf5dc..2cb1a58cf33a 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1,8 +1,19 @@ """ +Figures are typically created via `~.pyplot.figure` or `~.pyplot.subplots`. + `matplotlib.figure` implements the following classes: +`PanelBase` + Abstract `~matplotlib.artist.Artist`, which holds all plot elements + typically contained in a Figure + `Figure` - Top level `~matplotlib.artist.Artist`, which holds all plot elements. + Top-level `.PanelBase`, which holds all plot elements. + +`SubPanel` + A panel within a figure (or a subpanel of a figure) that + behaves like a figure, has a `~.PanelBase.suptitle`, + `~.PanelBase.legend`, and axes within its own logical container. `SubplotParams` Control the default spacing between subplots. @@ -27,13 +38,13 @@ from matplotlib.axes import Axes, SubplotBase, subplot_class_factory from matplotlib.blocking_input import BlockingMouseInput, BlockingKeyMouseInput -from matplotlib.gridspec import GridSpec, SubplotSpec +from matplotlib.gridspec import GridSpec import matplotlib.legend as mlegend from matplotlib.patches import Rectangle 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__) @@ -43,118 +54,6 @@ def _stale_figure_callback(self, val): self.figure.stale = val -class _AxesStack(cbook.Stack): - """ - Specialization of `.Stack`, to handle all tracking of `~.axes.Axes` in a - `.Figure`. - - This stack stores ``key, (ind, axes)`` pairs, where: - - * **key** is a hash of the args and kwargs used in generating the Axes. - * **ind** is a serial index tracking the order in which axes were added. - - AxesStack is a callable; calling it returns the current axes. - The `current_key_axes` method returns the current key and associated axes. - """ - - def __init__(self): - super().__init__() - self._ind = 0 - - def as_list(self): - """ - Return a list of the Axes instances that have been added to the figure. - """ - ia_list = [a for k, a in self._elements] - ia_list.sort() - return [a for i, a in ia_list] - - def get(self, key): - """ - Return the Axes instance that was added with *key*. - If it is not present, return *None*. - """ - item = dict(self._elements).get(key) - if item is None: - return None - cbook.warn_deprecated( - "2.1", - message="Adding an axes using the same arguments as a previous " - "axes currently reuses the earlier instance. In a future " - "version, a new instance will always be created and returned. " - "Meanwhile, this warning can be suppressed, and the future " - "behavior ensured, by passing a unique label to each axes " - "instance.") - return item[1] - - def _entry_from_axes(self, e): - ind, k = {a: (ind, k) for k, (ind, a) in self._elements}[e] - return (k, (ind, e)) - - def remove(self, a): - """Remove the axes from the stack.""" - super().remove(self._entry_from_axes(a)) - - def bubble(self, a): - """ - Move the given axes, which must already exist in the - stack, to the top. - """ - return super().bubble(self._entry_from_axes(a)) - - def add(self, key, a): - """ - Add Axes *a*, with key *key*, to the stack, and return the stack. - - If *key* is unhashable, replace it by a unique, arbitrary object. - - If *a* is already on the stack, don't add it again, but - return *None*. - """ - # All the error checking may be unnecessary; but this method - # is called so seldom that the overhead is negligible. - cbook._check_isinstance(Axes, a=a) - try: - hash(key) - except TypeError: - key = object() - - a_existing = self.get(key) - if a_existing is not None: - super().remove((key, a_existing)) - cbook._warn_external( - "key {!r} already existed; Axes is being replaced".format(key)) - # I don't think the above should ever happen. - - if a in self: - return None - self._ind += 1 - return super().push((key, (self._ind, a))) - - def current_key_axes(self): - """ - Return a tuple of ``(key, axes)`` for the active axes. - - If no axes exists on the stack, then returns ``(None, None)``. - """ - if not len(self._elements): - return self._default, self._default - else: - key, (index, axes) = self._elements[self._pos] - return key, axes - - def __call__(self): - return self.current_key_axes()[1] - - def __contains__(self, a): - return a in self.as_list() - - -@cbook.deprecated("3.2") -class AxesStack(_AxesStack): - pass - - class SubplotParams: """ A class to hold the parameters for a subplot. @@ -216,144 +115,22 @@ def update(self, left=None, bottom=None, right=None, top=None, self.hspace = hspace -class Figure(Artist): +class PanelBase(Artist): """ - The top level container for all the plot elements. - - The Figure instance supports callbacks through a *callbacks* attribute - which is a `.CallbackRegistry` instance. The events you can connect to - are 'dpi_changed', and the callback will be called with ``func(fig)`` where - fig is the `Figure` instance. - - Attributes - ---------- - patch - The `.Rectangle` instance representing the figure background patch. - - suppressComposite - For multiple figure images, the figure will make composite images - depending on the renderer option_image_nocomposite function. If - *suppressComposite* is a boolean, this will override the renderer. + Base class for `.figure.Figure` and `.figure.SubPanel` containing the + methods that add artists to the figure, create axes, etc. """ - - def __str__(self): - return "Figure(%gx%g)" % tuple(self.bbox.size) - - def __repr__(self): - return "<{clsname} size {h:g}x{w:g} with {naxes} Axes>".format( - clsname=self.__class__.__name__, - h=self.bbox.size[0], w=self.bbox.size[1], - naxes=len(self.axes), - ) - - def __init__(self, - figsize=None, - dpi=None, - facecolor=None, - edgecolor=None, - linewidth=0.0, - frameon=None, - subplotpars=None, # rc figure.subplot.* - tight_layout=None, # rc figure.autolayout - constrained_layout=None, # rc figure.constrained_layout.use - ): - """ - Parameters - ---------- - figsize : 2-tuple of floats, default: :rc:`figure.figsize` - Figure dimension ``(width, height)`` in inches. - - dpi : float, default: :rc:`figure.dpi` - Dots per inch. - - facecolor : default: :rc:`figure.facecolor` - The figure patch facecolor. - - edgecolor : default: :rc:`figure.edgecolor` - The figure patch edge color. - - linewidth : float - The linewidth of the frame (i.e. the edge linewidth of the figure - patch). - - frameon : bool, default: :rc:`figure.frameon` - If ``False``, suppress drawing the figure background patch. - - subplotpars : `SubplotParams` - Subplot parameters. If not given, the default subplot - parameters :rc:`figure.subplot.*` are used. - - tight_layout : bool or dict, default: :rc:`figure.autolayout` - If ``False`` use *subplotpars*. If ``True`` adjust subplot - parameters using `.tight_layout` with default padding. - When providing a dict containing the keys ``pad``, ``w_pad``, - ``h_pad``, and ``rect``, the default `.tight_layout` paddings - will be overridden. - - constrained_layout : bool, default: :rc:`figure.constrained_layout.use` - If ``True`` use constrained layout to adjust positioning of plot - elements. Like ``tight_layout``, but designed to be more - flexible. See - :doc:`/tutorials/intermediate/constrainedlayout_guide` - for examples. (Note: does not work with `add_subplot` or - `~.pyplot.subplot2grid`.) - """ + def __init__(self): super().__init__() # remove the non-figure artist _axes property # as it makes no sense for a figure to be _in_ an axes # this is used by the property methods in the artist base class # which are over-ridden in this class - del self._axes - self.callbacks = cbook.CallbackRegistry() - - if figsize is None: - figsize = mpl.rcParams['figure.figsize'] - if dpi is None: - dpi = mpl.rcParams['figure.dpi'] - if facecolor is None: - facecolor = mpl.rcParams['figure.facecolor'] - if edgecolor is None: - edgecolor = mpl.rcParams['figure.edgecolor'] - if frameon is None: - frameon = mpl.rcParams['figure.frameon'] - - if not np.isfinite(figsize).all() or (np.array(figsize) <= 0).any(): - raise ValueError('figure size must be positive finite not ' - f'{figsize}') - self.bbox_inches = Bbox.from_bounds(0, 0, *figsize) - - self.dpi_scale_trans = Affine2D().scale(dpi) - # do not use property as it will trigger - self._dpi = dpi - self.bbox = TransformedBbox(self.bbox_inches, self.dpi_scale_trans) - - self.transFigure = BboxTransformTo(self.bbox) - - self.patch = Rectangle( - xy=(0, 0), width=1, height=1, visible=frameon, - facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, - # Don't let the figure patch influence bbox calculation. - in_layout=False) - self._set_artist_props(self.patch) - self.patch.set_antialiased(False) - FigureCanvasBase(self) # Set self.canvas. self._suptitle = None - if subplotpars is None: - subplotpars = SubplotParams() - - self.subplotpars = subplotpars # constrained_layout: - self._layoutbox = None - # set in set_constrained_layout_pads() - self.set_constrained_layout(constrained_layout) - - self.set_tight_layout(tight_layout) - - self._axstack = _AxesStack() # track all figure axes and current axes - self.clf() - self._cachedRenderer = None + self._layoutgrid = None # groupers to keep track of x and y labels we want to align. # see self.align_xlabels and self.align_ylabels and @@ -361,221 +138,54 @@ def __init__(self, self._align_xlabel_grp = cbook.Grouper() self._align_ylabel_grp = cbook.Grouper() + self.figure = self # list of child gridspecs for this figure self._gridspecs = [] - # TODO: I'd like to dynamically add the _repr_html_ method - # to the figure in the right context, but then IPython doesn't - # use it, for some reason. + def clf(self): + self._localaxes = [] # keep track of axes at this level + self.artists = [] + self.lines = [] + self.patches = [] + self.texts = [] + self.images = [] + self.legends = [] + self.panels = [] + self._suptitle = None + self.stale = True - def _repr_html_(self): - # We can't use "isinstance" here, because then we'd end up importing - # webagg unconditionally. - if 'WebAgg' in type(self.canvas).__name__: - from matplotlib.backends import backend_webagg - return backend_webagg.ipython_inline_display(self) + def _get_draw_artists(self, renderer): + """Also runs apply_aspect""" + artists = self.get_children() - def show(self, warn=True): - """ - If using a GUI backend with pyplot, display the figure window. - - If the figure was not created using `~.pyplot.figure`, it will lack - a `~.backend_bases.FigureManagerBase`, and this method will raise an - AttributeError. - - .. warning:: - - This does not manage an GUI event loop. Consequently, the figure - may only be shown briefly or not shown at all if you or your - environment are not managing an event loop. - - Proper use cases for `.Figure.show` include running this from a - GUI application or an IPython shell. - - If you're running a pure python shell or executing a non-GUI - python script, you should use `matplotlib.pyplot.show` instead, - which takes care of managing the event loop for you. - - Parameters - ---------- - warn : bool, default: True - If ``True`` and we are not running headless (i.e. on Linux with an - unset DISPLAY), issue warning when called on a non-GUI backend. - """ - if self.canvas.manager is None: - raise AttributeError( - "Figure.show works only for figures managed by pyplot, " - "normally created by pyplot.figure()") - try: - self.canvas.manager.show() - except NonGuiException as exc: - cbook._warn_external(str(exc)) - - def get_axes(self): - """ - Return a list of axes in the Figure. You can access and modify the - axes in the Figure through this list. - - Do not modify the list itself. Instead, use `~Figure.add_axes`, - `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an axes. - - Note: This is equivalent to the property `~.Figure.axes`. - """ - return self._axstack.as_list() - - axes = property(get_axes, doc=""" - List of axes in the Figure. You can access and modify the axes in the - Figure through this list. - - Do not modify the list itself. Instead, use "`~Figure.add_axes`, - `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an axes. - """) - - def _get_dpi(self): - return self._dpi - - def _set_dpi(self, dpi, forward=True): - """ - Parameters - ---------- - dpi : float - - forward : bool - Passed on to `~.Figure.set_size_inches` - """ - if dpi == self._dpi: - # We don't want to cause undue events in backends. - return - self._dpi = dpi - self.dpi_scale_trans.clear().scale(dpi) - w, h = self.get_size_inches() - self.set_size_inches(w, h, forward=forward) - self.callbacks.process('dpi_changed', self) - - dpi = property(_get_dpi, _set_dpi, doc="The resolution in dots per inch.") - - def get_tight_layout(self): - """Return whether `.tight_layout` is called when drawing.""" - return self._tight - - def set_tight_layout(self, tight): - """ - Set whether and how `.tight_layout` is called when drawing. - - Parameters - ---------- - tight : bool or dict with keys "pad", "w_pad", "h_pad", "rect" or None - If a bool, sets whether to call `.tight_layout` upon drawing. - If ``None``, use the ``figure.autolayout`` rcparam instead. - If a dict, pass it as kwargs to `.tight_layout`, overriding the - default paddings. - """ - if tight is None: - tight = mpl.rcParams['figure.autolayout'] - self._tight = bool(tight) - self._tight_parameters = tight if isinstance(tight, dict) else {} - self.stale = True - - def get_constrained_layout(self): - """ - Return whether constrained layout is being used. - - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. - """ - return self._constrained - - def set_constrained_layout(self, constrained): - """ - Set whether ``constrained_layout`` is used upon drawing. If None, - :rc:`figure.constrained_layout.use` value will be used. - - When providing a dict containing the keys `w_pad`, `h_pad` - the default ``constrained_layout`` paddings will be - overridden. These pads are in inches and default to 3.0/72.0. - ``w_pad`` is the width padding and ``h_pad`` is the height padding. - - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. - - Parameters - ---------- - constrained : bool or dict or None - """ - self._constrained_layout_pads = dict() - self._constrained_layout_pads['w_pad'] = None - self._constrained_layout_pads['h_pad'] = None - self._constrained_layout_pads['wspace'] = None - self._constrained_layout_pads['hspace'] = None - if constrained is None: - constrained = mpl.rcParams['figure.constrained_layout.use'] - self._constrained = bool(constrained) - if isinstance(constrained, dict): - self.set_constrained_layout_pads(**constrained) - else: - self.set_constrained_layout_pads() - - self.stale = True - - def set_constrained_layout_pads(self, **kwargs): - """ - Set padding for ``constrained_layout``. Note the kwargs can be passed - as a dictionary ``fig.set_constrained_layout(**paddict)``. - - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. - - Parameters - ---------- - w_pad : float - Width padding in inches. This is the pad around axes - and is meant to make sure there is enough room for fonts to - look good. Defaults to 3 pts = 0.04167 inches - - h_pad : float - Height padding in inches. Defaults to 3 pts. - - wspace : float - Width padding between subplots, expressed as a fraction of the - subplot width. The total padding ends up being w_pad + wspace. - - hspace : float - Height padding between subplots, expressed as a fraction of the - subplot width. The total padding ends up being h_pad + hspace. - - """ + for sfig in self.panels: + artists.remove(sfig) + childa = sfig.get_children() + for child in childa: + if child in artists: + artists.remove(child) - todo = ['w_pad', 'h_pad', 'wspace', 'hspace'] - for td in todo: - if td in kwargs and kwargs[td] is not None: - self._constrained_layout_pads[td] = kwargs[td] + artists.remove(self.patch) + artists = sorted( + (artist for artist in artists if not artist.get_animated()), + key=lambda artist: artist.get_zorder()) + for ax in self._localaxes: + locator = ax.get_axes_locator() + if locator: + pos = locator(ax, renderer) + ax.apply_aspect(pos) else: - self._constrained_layout_pads[td] = ( - mpl.rcParams['figure.constrained_layout.' + td]) - - def get_constrained_layout_pads(self, relative=False): - """ - Get padding for ``constrained_layout``. - - Returns a list of ``w_pad, h_pad`` in inches and - ``wspace`` and ``hspace`` as fractions of the subplot. - - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. - - Parameters - ---------- - relative : bool - If `True`, then convert from inches to figure relative. - """ - w_pad = self._constrained_layout_pads['w_pad'] - h_pad = self._constrained_layout_pads['h_pad'] - wspace = self._constrained_layout_pads['wspace'] - 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) - dpi = renderer0.dpi - w_pad = w_pad * dpi / renderer0.width - h_pad = h_pad * dpi / renderer0.height + ax.apply_aspect() - return w_pad, h_pad, wspace, hspace + for child in ax.get_children(): + if hasattr(child, 'apply_aspect'): + locator = child.get_axes_locator() + if locator: + pos = locator(child, renderer) + child.apply_aspect(pos) + else: + child.apply_aspect() + return artists def autofmt_xdate( self, bottom=0.2, rotation=30, ha='right', which='major'): @@ -628,12 +238,13 @@ def get_children(self): """Get a list of artists contained in the figure.""" return [self.patch, *self.artists, - *self.axes, + *self._localaxes, *self.lines, *self.patches, *self.texts, *self.images, - *self.legends] + *self.legends, + *self.panels] def contains(self, mouseevent): """ @@ -729,227 +340,53 @@ 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') + # will need something to do with layoutgrid in here + + if manual_position: + self._suptitle.set_in_layout(False) + self.stale = True return self._suptitle - def set_canvas(self, canvas): + def get_edgecolor(self): + """Get the edge color of the Figure rectangle.""" + return self.patch.get_edgecolor() + + def get_facecolor(self): + """Get the face color of the Figure rectangle.""" + return self.patch.get_facecolor() + + def get_frameon(self): """ - Set the canvas that contains the figure + Return the figure's background patch visibility, i.e. + whether the figure background will be drawn. Equivalent to + ``Figure.patch.get_visible()``. + """ + return self.patch.get_visible() + + def set_linewidth(self, linewidth): + """ + Set the edge color of the Figure rectangle. Parameters ---------- - canvas : FigureCanvas + linewidth : number """ - self.canvas = canvas + self.patch.set_linewidth(linewidth) - def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, - vmin=None, vmax=None, origin=None, resize=False, **kwargs): + def set_edgecolor(self, color): """ - Add a non-resampled image to the figure. - - The image is attached to the lower or upper left corner depending on - *origin*. + Set the edge color of the Figure rectangle. Parameters ---------- - X - The image data. This is an array of one of the following shapes: + color : color + """ + self.patch.set_edgecolor(color) - - MxN: luminance (grayscale) values - - MxNx3: RGB values - - MxNx4: RGBA values - - xo, yo : int - The *x*/*y* image offset in pixels. - - alpha : None or float - The alpha blending value. - - norm : `matplotlib.colors.Normalize` - A `.Normalize` instance to map the luminance to the - interval [0, 1]. - - cmap : str or `matplotlib.colors.Colormap`, default: :rc:`image.cmap` - The colormap to use. - - vmin, vmax : float - If *norm* is not given, these values set the data limits for the - colormap. - - origin : {'upper', 'lower'}, default: :rc:`image.origin` - Indicates where the [0, 0] index of the array is in the upper left - or lower left corner of the axes. - - resize : bool - If *True*, resize the figure to match the given image size. - - Returns - ------- - `matplotlib.image.FigureImage` - - Other Parameters - ---------------- - **kwargs - Additional kwargs are `.Artist` kwargs passed on to `.FigureImage`. - - Notes - ----- - figimage complements the axes image (`~matplotlib.axes.Axes.imshow`) - which will be resampled to fit the current axes. If you want - a resampled image to fill the entire figure, you can define an - `~matplotlib.axes.Axes` with extent [0, 0, 1, 1]. - - Examples - -------- - :: - - f = plt.figure() - nx = int(f.get_figwidth() * f.dpi) - ny = int(f.get_figheight() * f.dpi) - data = np.random.random((ny, nx)) - f.figimage(data) - plt.show() - """ - if resize: - dpi = self.get_dpi() - figsize = [x / dpi for x in (X.shape[1], X.shape[0])] - self.set_size_inches(figsize, forward=True) - - im = mimage.FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) - im.stale_callback = _stale_figure_callback - - im.set_array(X) - im.set_alpha(alpha) - if norm is None: - im.set_clim(vmin, vmax) - self.images.append(im) - im._remove_method = self.images.remove - self.stale = True - return im - - def set_size_inches(self, w, h=None, forward=True): - """ - Set the figure size in inches. - - Call signatures:: - - fig.set_size_inches(w, h) # OR - fig.set_size_inches((w, h)) - - Parameters - ---------- - w : (float, float) or float - Width and height in inches (if height not specified as a separate - argument) or width. - h : float - Height in inches. - forward : bool, default: True - If ``True``, the canvas size is automatically updated, e.g., - you can resize the figure window from the shell. - - See Also - -------- - matplotlib.figure.Figure.get_size_inches - matplotlib.figure.Figure.set_figwidth - matplotlib.figure.Figure.set_figheight - - Notes - ----- - To transform from pixels to inches divide by `Figure.dpi`. - """ - if h is None: # Got called with a single pair as argument. - w, h = w - size = np.array([w, h]) - if not np.isfinite(size).all() or (size <= 0).any(): - raise ValueError(f'figure size must be positive finite not {size}') - self.bbox_inches.p1 = size - if forward: - canvas = getattr(self, 'canvas') - if canvas is not None: - dpi_ratio = getattr(canvas, '_dpi_ratio', 1) - manager = getattr(canvas, 'manager', None) - if manager is not None: - manager.resize(*(size * self.dpi / dpi_ratio).astype(int)) - self.stale = True - - def get_size_inches(self): - """ - Return the current size of the figure in inches. - - Returns - ------- - ndarray - The size (width, height) of the figure in inches. - - See Also - -------- - matplotlib.figure.Figure.set_size_inches - matplotlib.figure.Figure.get_figwidth - matplotlib.figure.Figure.get_figheight - - Notes - ----- - The size in pixels can be obtained by multiplying with `Figure.dpi`. - """ - return np.array(self.bbox_inches.p1) - - def get_edgecolor(self): - """Get the edge color of the Figure rectangle.""" - return self.patch.get_edgecolor() - - def get_facecolor(self): - """Get the face color of the Figure rectangle.""" - return self.patch.get_facecolor() - - def get_figwidth(self): - """Return the figure width in inches.""" - return self.bbox_inches.width - - def get_figheight(self): - """Return the figure height in inches.""" - return self.bbox_inches.height - - def get_dpi(self): - """Return the resolution in dots per inch as a float.""" - return self.dpi - - def get_frameon(self): - """ - Return the figure's background patch visibility, i.e. - whether the figure background will be drawn. Equivalent to - ``Figure.patch.get_visible()``. - """ - return self.patch.get_visible() - - def set_edgecolor(self, color): - """ - Set the edge color of the Figure rectangle. - - Parameters - ---------- - color : color - """ - self.patch.set_edgecolor(color) - - def set_facecolor(self, color): - """ - Set the face color of the Figure rectangle. + def set_facecolor(self, color): + """ + Set the face color of the Figure rectangle. Parameters ---------- @@ -957,51 +394,6 @@ def set_facecolor(self, color): """ self.patch.set_facecolor(color) - def set_dpi(self, val): - """ - Set the resolution of the figure in dots-per-inch. - - Parameters - ---------- - val : float - """ - self.dpi = val - self.stale = True - - def set_figwidth(self, val, forward=True): - """ - Set the width of the figure in inches. - - Parameters - ---------- - val : float - forward : bool - See `set_size_inches`. - - See Also - -------- - matplotlib.figure.Figure.set_figheight - matplotlib.figure.Figure.set_size_inches - """ - self.set_size_inches(val, self.get_figheight(), forward=forward) - - def set_figheight(self, val, forward=True): - """ - Set the height of the figure in inches. - - Parameters - ---------- - val : float - forward : bool - See `set_size_inches`. - - See Also - -------- - matplotlib.figure.Figure.set_figwidth - matplotlib.figure.Figure.set_size_inches - """ - self.set_size_inches(self.get_figwidth(), val, forward=forward) - def set_frameon(self, b): """ Set the figure's background patch visibility, i.e. @@ -1030,7 +422,7 @@ def add_artist(self, artist, clip=False): artist : `~matplotlib.artist.Artist` The artist to add to the figure. If the added artist has no transform previously set, its transform will be set to - ``figure.transFigure``. + ``figure.transPanel``. clip : bool, default: False Whether the added artist should be clipped by the figure patch. @@ -1044,7 +436,7 @@ def add_artist(self, artist, clip=False): artist._remove_method = self.artists.remove if not artist.is_transform_set(): - artist.set_transform(self.transFigure) + artist.set_transform(self.transPanel) if clip: artist.set_clip_path(self.patch) @@ -1052,77 +444,15 @@ def add_artist(self, artist, clip=False): self.stale = True return artist - def _make_key(self, *args, **kwargs): - """Make a hashable key out of args and kwargs.""" - - def fixitems(items): - # items may have arrays and lists in them, so convert them - # to tuples for the key - ret = [] - for k, v in items: - # some objects can define __getitem__ without being - # iterable and in those cases the conversion to tuples - # will fail. So instead of using the np.iterable(v) function - # we simply try and convert to a tuple, and proceed if not. - try: - v = tuple(v) - except Exception: - pass - ret.append((k, v)) - return tuple(ret) + @docstring.dedent_interpd + def add_axes(self, *args, **kwargs): + """ + Add an axes to the figure. - def fixlist(args): - ret = [] - for a in args: - if np.iterable(a): - a = tuple(a) - ret.append(a) - return tuple(ret) + Call signatures:: - key = fixlist(args), fixitems(kwargs.items()) - return key - - def _process_projection_requirements( - self, *args, polar=False, projection=None, **kwargs): - """ - Handle the args/kwargs to add_axes/add_subplot/gca, returning:: - - (axes_proj_class, proj_class_kwargs, proj_stack_key) - - which can be used for new axes initialization/identification. - """ - if polar: - if projection is not None and projection != 'polar': - raise ValueError( - "polar=True, yet projection=%r. " - "Only one of these arguments should be supplied." % - projection) - projection = 'polar' - - if isinstance(projection, str) or projection is None: - projection_class = projections.get_projection_class(projection) - elif hasattr(projection, '_as_mpl_axes'): - projection_class, extra_kwargs = projection._as_mpl_axes() - kwargs.update(**extra_kwargs) - else: - raise TypeError('projection must be a string, None or implement a ' - '_as_mpl_axes method. Got %r' % projection) - - # Make the key without projection kwargs, this is used as a unique - # lookup for axes instances - key = self._make_key(*args, **kwargs) - - return projection_class, kwargs, key - - @docstring.dedent_interpd - def add_axes(self, *args, **kwargs): - """ - Add an axes to the figure. - - Call signatures:: - - add_axes(rect, projection=None, polar=False, **kwargs) - add_axes(ax) + add_axes(rect, projection=None, polar=False, **kwargs) + add_axes(ax) Parameters ---------- @@ -1265,16 +595,13 @@ def add_subplot(self, *args, **kwargs): Parameters ---------- - *args : int, (int, int, *index*), or `.SubplotSpec`, default: (1, 1, 1) + *args, int or (int, int, int) or `SubplotSpec`, default: (1, 1, 1) The position of the subplot described by one of - Three integers (*nrows*, *ncols*, *index*). The subplot will take the *index* position on a grid with *nrows* rows and *ncols* columns. *index* starts at 1 in the upper left corner - and increases to the right. *index* can also be a two-tuple - specifying the (*first*, *last*) indices (1-based, and including - *last*) of the subplot, e.g., ``fig.add_subplot(3, 1, (1, 2))`` - makes a subplot that spans the upper 2/3 of the figure. + and increases to the right. - A 3-digit integer. The digits are interpreted as if given separately as three single-digit integers, i.e. ``fig.add_subplot(235)`` is the same as @@ -1345,21 +672,19 @@ def add_subplot(self, *args, **kwargs): Examples -------- - :: + Some simple examples:: + rect = l, b, w, h fig = plt.figure() - - fig.add_subplot(231) - ax1 = fig.add_subplot(2, 3, 1) # equivalent but more general - - fig.add_subplot(232, frameon=False) # subplot with no frame - fig.add_subplot(233, projection='polar') # polar subplot - fig.add_subplot(234, sharex=ax1) # subplot sharing x-axis with ax1 - fig.add_subplot(235, facecolor="red") # red subplot - - ax1.remove() # delete ax1 from the figure - fig.add_subplot(ax1) # add ax1 back to the figure + fig.add_axes(rect, label=label1) + fig.add_axes(rect, label=label2) + fig.add_axes(rect, frameon=False, facecolor='g') + fig.add_axes(rect, polar=True) + ax = fig.add_axes(rect, projection='polar') + fig.delaxes(ax) + fig.add_axes(ax) """ + if 'figure' in kwargs: # Axes itself allows for a 'figure' kwarg, but since we want to # bind the created Axes to self, it is not allowed here. @@ -1406,6 +731,7 @@ def add_subplot(self, *args, **kwargs): def _add_axes_internal(self, key, ax): """Private helper for `add_axes` and `add_subplot`.""" self._axstack.add(key, ax) + self._localaxes += [ax] self.sca(ax) ax._remove_method = self.delaxes self.stale = True @@ -1519,9 +845,10 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, """ if gridspec_kw is None: gridspec_kw = {} - return (self.add_gridspec(nrows, ncols, figure=self, **gridspec_kw) - .subplots(sharex=sharex, sharey=sharey, squeeze=squeeze, - subplot_kw=subplot_kw)) + gs = self.add_gridspec(nrows, ncols, figure=self, **gridspec_kw) + axs = gs.subplots(sharex=sharex, sharey=sharey, squeeze=squeeze, + subplot_kw=subplot_kw) + return axs def delaxes(self, ax): """ @@ -1572,6 +899,7 @@ def _break_share_link(ax, grouper): self._axstack.remove(ax) self._axobservers.process("_axes_change_event", self) self.stale = True + self._localaxes.remove(ax) last_ax = _break_share_link(ax, ax._shared_y_axes) if last_ax is not None: @@ -1581,105 +909,6 @@ def _break_share_link(ax, grouper): if last_ax is not None: _reset_locators_and_formatters(last_ax.xaxis) - def clf(self, keep_observers=False): - """ - Clear the figure. - - Set *keep_observers* to True if, for example, - a gui widget is tracking the axes in the figure. - """ - self.suppressComposite = None - self.callbacks = cbook.CallbackRegistry() - - for ax in tuple(self.axes): # Iterate over the copy. - ax.cla() - self.delaxes(ax) # removes ax from self._axstack - - toolbar = getattr(self.canvas, 'toolbar', None) - if toolbar is not None: - toolbar.update() - self._axstack.clear() - self.artists = [] - self.lines = [] - self.patches = [] - self.texts = [] - self.images = [] - self.legends = [] - if not keep_observers: - self._axobservers = cbook.CallbackRegistry() - self._suptitle = None - if self.get_constrained_layout(): - layoutbox.nonetree(self._layoutbox) - self.stale = True - - def clear(self, keep_observers=False): - """Clear the figure -- synonym for `clf`.""" - self.clf(keep_observers=keep_observers) - - @allow_rasterization - def draw(self, renderer): - # docstring inherited - self._cachedRenderer = renderer - - # draw the figure bounding box, perhaps none for white figure - if not self.get_visible(): - return - - artists = self.get_children() - artists.remove(self.patch) - artists = sorted( - (artist for artist in artists if not artist.get_animated()), - key=lambda artist: artist.get_zorder()) - - for ax in self.axes: - locator = ax.get_axes_locator() - if locator: - pos = locator(ax, renderer) - ax.apply_aspect(pos) - else: - ax.apply_aspect() - - for child in ax.get_children(): - if hasattr(child, 'apply_aspect'): - locator = child.get_axes_locator() - if locator: - pos = locator(child, renderer) - child.apply_aspect(pos) - else: - child.apply_aspect() - - try: - renderer.open_group('figure', gid=self.get_gid()) - if self.get_constrained_layout() and self.axes: - self.execute_constrained_layout(renderer) - if self.get_tight_layout() and self.axes: - try: - self.tight_layout(**self._tight_parameters) - except ValueError: - pass - # ValueError can occur when resizing a window. - - self.patch.draw(renderer) - mimage._draw_list_compositing_images( - renderer, self, artists, self.suppressComposite) - - renderer.close_group('figure') - finally: - self.stale = False - - self.canvas.draw_event(renderer) - - def draw_artist(self, a): - """ - Draw `.Artist` instance *a* only. - - This can only be called after the figure has been drawn. - """ - if self._cachedRenderer is None: - raise AttributeError("draw_artist can only be used after an " - "initial draw which caches the renderer") - a.draw(self._cachedRenderer) - # Note: in the docstring below, the newlines in the examples after the # calls to legend() allow replacing it with figlegend() to generate the # docstring of pyplot.figlegend. @@ -1796,7 +1025,7 @@ def text(self, x, y, s, fontdict=None, **kwargs): .pyplot.text """ effective_kwargs = { - 'transform': self.transFigure, + 'transform': self.transPanel, **(fontdict if fontdict is not None else {}), **kwargs, } @@ -1809,82 +1038,376 @@ def text(self, x, y, s, fontdict=None, **kwargs): self.stale = True return text - def _set_artist_props(self, a): - if a != self: - a.set_figure(self) - a.stale_callback = _stale_figure_callback - a.set_transform(self.transFigure) - @docstring.dedent_interpd - def gca(self, **kwargs): + def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw): """ - Get the current axes, creating one if necessary. - - The following kwargs are supported for ensuring the returned axes - adheres to the given projection etc., and for axes creation if - the active axes does not exist: - - %(Axes)s + Create a colorbar for a ScalarMappable instance, *mappable*. + Documentation for the pyplot thin wrapper: """ - ckey, cax = self._axstack.current_key_axes() - # if there exists an axes on the stack see if it matches - # the desired axes configuration - if cax is not None: + # %(colorbar_doc)s + if ax is None: + ax = self.gca() - # if no kwargs are given just return the current axes - # this is a convenience for gca() on axes such as polar etc. - if not kwargs: - return cax + # Store the value of gca so that we can set it back later on. + current_ax = self.gca() - # if the user has specified particular projection detail - # then build up a key which can represent this + if cax is None: + if use_gridspec and isinstance(ax, SubplotBase) \ + and (not self.get_constrained_layout()): + cax, kw = cbar.make_axes_gridspec(ax, **kw) else: - projection_class, _, key = \ - self._process_projection_requirements(**kwargs) - - # let the returned axes have any gridspec by removing it from - # the key - ckey = ckey[1:] - key = key[1:] - - # if the cax matches this key then return the axes, otherwise - # continue and a new axes will be created - if key == ckey and isinstance(cax, projection_class): - return cax - else: - cbook._warn_external('Requested projection is different ' - 'from current axis projection, ' - 'creating new axis with requested ' - 'projection.') + cax, kw = cbar.make_axes(ax, **kw) - # no axes found, so create one which spans the figure - return self.add_subplot(1, 1, 1, **kwargs) + # need to remove kws that cannot be passed to Colorbar + NON_COLORBAR_KEYS = ['fraction', 'pad', 'shrink', 'aspect', 'anchor', + 'panchor'] + cb_kw = {k: v for k, v in kw.items() if k not in NON_COLORBAR_KEYS} + cb = cbar.colorbar_factory(cax, mappable, **cb_kw) - def sca(self, a): - """Set the current axes to be *a* and return *a*.""" - self._axstack.bubble(a) - self._axobservers.process("_axes_change_event", self) - return a + self.sca(current_ax) + self.stale = True + return cb - def _gci(self): - # Helper for `~matplotlib.pyplot.gci`. Do not use elsewhere. + def subplots_adjust(self, left=None, bottom=None, right=None, top=None, + wspace=None, hspace=None): """ - Get the current colorable artist. - - Specifically, returns the current `.ScalarMappable` instance (`.Image` - created by `imshow` or `figimage`, `.Collection` created by `pcolor` or - `scatter`, etc.), or *None* if no such instance has been defined. + Adjust the subplot layout parameters. - The current image is an attribute of the current axes, or the nearest - earlier axes in the current figure that contains an image. + Unset parameters are left unmodified; initial values are given by + :rc:`figure.subplot.[name]`. - Notes - ----- - Historically, the only colorable artists were images; hence the name - ``gci`` (get current image). + Parameters + ---------- + left : float, optional + The position of the left edge of the subplots, + as a fraction of the figure width. + right : float, optional + The position of the right edge of the subplots, + as a fraction of the figure width. + bottom : float, optional + The position of the bottom edge of the subplots, + as a fraction of the figure height. + top : float, optional + The position of the top edge of the subplots, + as a fraction of the figure height. + wspace : float, optional + The width of the padding between subplots, + as a fraction of the average axes width. + hspace : float, optional + The height of the padding between subplots, + as a fraction of the average axes height. """ - # Look first for an image in the current Axes: + if self.get_constrained_layout(): + self.set_constrained_layout(False) + cbook._warn_external("This figure was using " + "constrained_layout==True, but that is " + "incompatible with subplots_adjust and or " + "tight_layout: setting " + "constrained_layout==False. ") + self.subplotpars.update(left, bottom, right, top, wspace, hspace) + for ax in self.axes: + if not isinstance(ax, SubplotBase): + # Check if sharing a subplots axis + if isinstance(ax._sharex, SubplotBase): + ax._sharex.update_params() + ax.set_position(ax._sharex.figbox) + elif isinstance(ax._sharey, SubplotBase): + ax._sharey.update_params() + ax.set_position(ax._sharey.figbox) + else: + ax.update_params() + ax.set_position(ax.figbox) + self.stale = True + + def align_xlabels(self, axs=None): + """ + Align the ylabels of subplots in the same subplot column if label + alignment is being done automatically (i.e. the label position is + not manually set). + + Alignment persists for draw events after this is called. + + If a label is on the bottom, it is aligned with labels on axes that + also have their label on the bottom and that have the same + bottom-most subplot row. If the label is on the top, + it is aligned with labels on axes with the same top-most row. + + Parameters + ---------- + axs : list of `~matplotlib.axes.Axes` + Optional list of (or ndarray) `~matplotlib.axes.Axes` + to align the xlabels. + Default is to align all axes on the figure. + + See Also + -------- + matplotlib.figure.Figure.align_ylabels + matplotlib.figure.Figure.align_labels + + Notes + ----- + This assumes that ``axs`` are from the same `.GridSpec`, so that + their `.SubplotSpec` positions correspond to figure positions. + + Examples + -------- + Example with rotated xtick labels:: + + fig, axs = plt.subplots(1, 2) + for tick in axs[0].get_xticklabels(): + tick.set_rotation(55) + axs[0].set_xlabel('XLabel 0') + axs[1].set_xlabel('XLabel 1') + fig.align_xlabels() + """ + if axs is None: + axs = self.axes + axs = np.ravel(axs) + for ax in axs: + _log.debug(' Working on: %s', ax.get_xlabel()) + rowspan = ax.get_subplotspec().rowspan + pos = ax.xaxis.get_label_position() # top or bottom + # Search through other axes for label positions that are same as + # this one and that share the appropriate row number. + # Add to a grouper associated with each axes of siblings. + # This list is inspected in `axis.draw` by + # `axis._update_label_position`. + for axc in axs: + if axc.xaxis.get_label_position() == pos: + rowspanc = axc.get_subplotspec().rowspan + if (pos == 'top' and rowspan.start == rowspanc.start or + pos == 'bottom' and rowspan.stop == rowspanc.stop): + # grouper for groups of xlabels to align + self._align_xlabel_grp.join(ax, axc) + + def align_ylabels(self, axs=None): + """ + Align the ylabels of subplots in the same subplot column if label + alignment is being done automatically (i.e. the label position is + not manually set). + + Alignment persists for draw events after this is called. + + If a label is on the left, it is aligned with labels on axes that + also have their label on the left and that have the same + left-most subplot column. If the label is on the right, + it is aligned with labels on axes with the same right-most column. + + Parameters + ---------- + axs : list of `~matplotlib.axes.Axes` + Optional list (or ndarray) of `~matplotlib.axes.Axes` + to align the ylabels. + Default is to align all axes on the figure. + + See Also + -------- + matplotlib.figure.Figure.align_xlabels + matplotlib.figure.Figure.align_labels + + Notes + ----- + This assumes that ``axs`` are from the same `.GridSpec`, so that + their `.SubplotSpec` positions correspond to figure positions. + + Examples + -------- + Example with large yticks labels:: + + fig, axs = plt.subplots(2, 1) + axs[0].plot(np.arange(0, 1000, 50)) + axs[0].set_ylabel('YLabel 0') + axs[1].set_ylabel('YLabel 1') + fig.align_ylabels() + """ + if axs is None: + axs = self.axes + axs = np.ravel(axs) + for ax in axs: + _log.debug(' Working on: %s', ax.get_ylabel()) + colspan = ax.get_subplotspec().colspan + pos = ax.yaxis.get_label_position() # left or right + # Search through other axes for label positions that are same as + # this one and that share the appropriate column number. + # Add to a list associated with each axes of siblings. + # This list is inspected in `axis.draw` by + # `axis._update_label_position`. + for axc in axs: + if axc.yaxis.get_label_position() == pos: + colspanc = axc.get_subplotspec().colspan + if (pos == 'left' and colspan.start == colspanc.start or + pos == 'right' and colspan.stop == colspanc.stop): + # grouper for groups of ylabels to align + self._align_ylabel_grp.join(ax, axc) + + def align_labels(self, axs=None): + """ + Align the xlabels and ylabels of subplots with the same subplots + row or column (respectively) if label alignment is being + done automatically (i.e. the label position is not manually set). + + Alignment persists for draw events after this is called. + + Parameters + ---------- + axs : list of `~matplotlib.axes.Axes` + Optional list (or ndarray) of `~matplotlib.axes.Axes` + to align the labels. + Default is to align all axes on the figure. + + See Also + -------- + matplotlib.figure.Figure.align_xlabels + + matplotlib.figure.Figure.align_ylabels + """ + self.align_xlabels(axs=axs) + self.align_ylabels(axs=axs) + + def add_gridspec(self, nrows=1, ncols=1, **kwargs): + """ + Return a `.GridSpec` that has this figure as a parent. This allows + complex layout of axes in the figure. + + Parameters + ---------- + nrows : int, default: 1 + Number of rows in grid. + + ncols : int, default: 1 + Number or columns in grid. + + Returns + ------- + `.GridSpec` + + Other Parameters + ---------------- + **kwargs + Keyword arguments are passed to `.GridSpec`. + + See Also + -------- + matplotlib.pyplot.subplots + + Examples + -------- + Adding a subplot that spans two rows:: + + fig = plt.figure() + gs = fig.add_gridspec(2, 2) + ax1 = fig.add_subplot(gs[0, 0]) + ax2 = fig.add_subplot(gs[1, 0]) + # spans two rows: + ax3 = fig.add_subplot(gs[:, 1]) + + """ + + _ = kwargs.pop('figure', None) # pop in case user has added this... + gs = GridSpec(nrows=nrows, ncols=ncols, figure=self, **kwargs) + self._gridspecs.append(gs) + return gs + + def subpanels(self, nrows=1, ncols=1, squeeze=True, + wspace=None, hspace=None, + width_ratios=None, height_ratios=None, + **kwargs): + gs = GridSpec(nrows=nrows, ncols=ncols, figure=self, + wspace=wspace, hspace=hspace, + width_ratios=width_ratios, + height_ratios=height_ratios) + + sfarr = np.empty((nrows, ncols), dtype=object) + for i in range(ncols): + for j in range(nrows): + sfarr[j, i] = self.add_subpanel(gs[j, i], **kwargs) + + if squeeze: + # Discarding unneeded dimensions that equal 1. If we only have one + # subpanel, just return it instead of a 1-element array. + return sfarr.item() if sfarr.size == 1 else sfarr.squeeze() + else: + # Returned axis array will be always 2-d, even if nrows=ncols=1. + return sfarr + + return sfarr + + def add_subpanel(self, subplotspec, **kwargs): + sf = SubPanel(subplotspec, self, **kwargs) + self.panels += [sf] + return sf + + def sca(self, a): + """Set the current axes to be *a* and return *a*.""" + self._axstack.bubble(a) + self._axobservers.process("_axes_change_event", self) + return a + + @docstring.dedent_interpd + def gca(self, **kwargs): + """ + Get the current axes, creating one if necessary. + + The following kwargs are supported for ensuring the returned axes + adheres to the given projection etc., and for axes creation if + the active axes does not exist: + + %(Axes)s + + """ + ckey, cax = self._axstack.current_key_axes() + # if there exists an axes on the stack see if it matches + # the desired axes configuration + if cax is not None: + + # if no kwargs are given just return the current axes + # this is a convenience for gca() on axes such as polar etc. + if not kwargs: + return cax + + # if the user has specified particular projection detail + # then build up a key which can represent this + else: + projection_class, _, key = \ + self._process_projection_requirements(**kwargs) + + # let the returned axes have any gridspec by removing it from + # the key + ckey = ckey[1:] + key = key[1:] + + # if the cax matches this key then return the axes, otherwise + # continue and a new axes will be created + if key == ckey and isinstance(cax, projection_class): + return cax + else: + cbook._warn_external('Requested projection is different ' + 'from current axis projection, ' + 'creating new axis with requested ' + 'projection.') + + # no axes found, so create one which spans the figure + return self.add_subplot(1, 1, 1, **kwargs) + + def _gci(self): + # Helper for `~matplotlib.pyplot.gci`. Do not use elsewhere. + """ + Get the current colorable artist. + + Specifically, returns the current `.ScalarMappable` instance (`.Image` + created by `imshow` or `figimage`, `.Collection` created by `pcolor` or + `scatter`, etc.), or *None* if no such instance has been defined. + + The current image is an attribute of the current axes, or the nearest + earlier axes in the current figure that contains an image. + + Notes + ----- + Historically, the only colorable artists were images; hence the name + ``gci`` (get current image). + """ + # Look first for an image in the current Axes: cax = self._axstack.current_key_axes()[1] if cax is None: return None @@ -1892,14 +1415,868 @@ def _gci(self): if im is not None: return im - # If there is no image in the current Axes, search for - # one in a previously created Axes. Whether this makes - # sense is debatable, but it is the documented behavior. - for ax in reversed(self.axes): - im = ax._gci() - if im is not None: - return im - return None + # If there is no image in the current Axes, search for + # one in a previously created Axes. Whether this makes + # sense is debatable, but it is the documented behavior. + for ax in reversed(self.axes): + im = ax._gci() + if im is not None: + return im + return None + + def _process_projection_requirements( + self, *args, polar=False, projection=None, **kwargs): + """ + Handle the args/kwargs to add_axes/add_subplot/gca, returning:: + + (axes_proj_class, proj_class_kwargs, proj_stack_key) + + which can be used for new axes initialization/identification. + """ + if polar: + if projection is not None and projection != 'polar': + raise ValueError( + "polar=True, yet projection=%r. " + "Only one of these arguments should be supplied." % + projection) + projection = 'polar' + + if isinstance(projection, str) or projection is None: + projection_class = projections.get_projection_class(projection) + elif hasattr(projection, '_as_mpl_axes'): + projection_class, extra_kwargs = projection._as_mpl_axes() + kwargs.update(**extra_kwargs) + else: + raise TypeError('projection must be a string, None or implement a ' + '_as_mpl_axes method. Got %r' % projection) + + # Make the key without projection kwargs, this is used as a unique + # lookup for axes instances + key = self._make_key(*args, **kwargs) + + return projection_class, kwargs, key + + def _make_key(self, *args, **kwargs): + """Make a hashable key out of args and kwargs.""" + + def fixitems(items): + # items may have arrays and lists in them, so convert them + # to tuples for the key + ret = [] + for k, v in items: + # some objects can define __getitem__ without being + # iterable and in those cases the conversion to tuples + # will fail. So instead of using the np.iterable(v) function + # we simply try and convert to a tuple, and proceed if not. + try: + v = tuple(v) + except Exception: + pass + ret.append((k, v)) + return tuple(ret) + + def fixlist(args): + ret = [] + for a in args: + if np.iterable(a): + a = tuple(a) + ret.append(a) + return tuple(ret) + + key = fixlist(args), fixitems(kwargs.items()) + return key + + def get_default_bbox_extra_artists(self): + bbox_artists = [artist for artist in self.get_children() + if (artist.get_visible() and artist.get_in_layout())] + for ax in self.axes: + if ax.get_visible(): + bbox_artists.extend(ax.get_default_bbox_extra_artists()) + return bbox_artists + + def get_tightbbox(self, renderer, bbox_extra_artists=None): + """ + Return a (tight) bounding box of the figure in inches. + + Artists that have ``artist.set_in_layout(False)`` are not included + in the bbox. + + Parameters + ---------- + renderer : `.RendererBase` subclass + renderer that will be used to draw the figures (i.e. + ``fig.canvas.get_renderer()``) + + bbox_extra_artists : list of `.Artist` or ``None`` + List of artists to include in the tight bounding box. If + ``None`` (default), then all artist children of each axes are + included in the tight bounding box. + + Returns + ------- + `.BboxBase` + containing the bounding box (in figure inches). + """ + + bb = [] + if bbox_extra_artists is None: + artists = self.get_default_bbox_extra_artists() + else: + artists = bbox_extra_artists + + for a in artists: + bbox = a.get_tightbbox(renderer) + if bbox is not None and (bbox.width != 0 or bbox.height != 0): + bb.append(bbox) + + for ax in self.axes: + if ax.get_visible(): + # some axes don't take the bbox_extra_artists kwarg so we + # need this conditional.... + try: + bbox = ax.get_tightbbox( + renderer, bbox_extra_artists=bbox_extra_artists) + except TypeError: + bbox = ax.get_tightbbox(renderer) + bb.append(bbox) + bb = [b for b in bb + if (np.isfinite(b.width) and np.isfinite(b.height) + and (b.width != 0 or b.height != 0))] + + if len(bb) == 0: + return self.bbox_inches + + _bbox = Bbox.union(bb) + + bbox_inches = TransformedBbox(_bbox, Affine2D().scale(1 / self.dpi)) + + return bbox_inches + + +class SubPanel(PanelBase): + """ + Logical panel that can be place in a figure typically using + `.Figure.add_subpanel` or `.SubPanel.add_subpanel`, or + `.SubPanel.subpanels`. A panel has the same methods as a figure + except for those particularly tied to the size or dpi of the figure, and + is confined to a prescribed region of the figure. For example the + following puts two subpanels side-by-side:: + + fig = plt.figure() + spanels = fig.subpanels(1, 2) + axsL = spanels[0].subplots(1, 2) + axsR = spanels[0].subplots(2, 1) + + See :doc:`/gallery/subplots_axes_and_figures/subpanels` + """ + + def __init__(self, subplotspec, parent, *, + facecolor=None, + edgecolor=None, + linewidth=0.0, + frameon=None): + """ + Parameters + ---------- + subplotspec : `.gridspec.SubplotSpec` + defines the region in a parent gridspec where the subpanel will + be placed + + parent : `.figure.Figure` or `.figure.SubPanel` + Figure or subpanel that contains the SubPanel. SubPanels + can be nested. + + facecolor : default: :rc:`figure.facecolor` + The figure patch facecolor. + + edgecolor : default: :rc:`figure.edgecolor` + The figure patch edge color. + + linewidth : float + The linewidth of the frame (i.e. the edge linewidth of the figure + patch). + + frameon : boolean, default: :rc:`figure.frameon` + If ``False``, suppress drawing the figure background patch. + """ + super().__init__() + if facecolor is None: + facecolor = mpl.rcParams['figure.facecolor'] + if edgecolor is None: + edgecolor = mpl.rcParams['figure.edgecolor'] + if frameon is None: + frameon = mpl.rcParams['figure.frameon'] + + self.clf() + self._subplotspec = subplotspec + self._parent = parent + self.figure = parent.figure + # subpanels use the parent axstack + self._axstack = parent._axstack + self.subplotpars = parent.subplotpars + self.dpi_scale_trans = parent.dpi_scale_trans + self._axobservers = parent._axobservers + self.dpi = parent.dpi + self.canvas = parent.canvas + self.transFigure = self._parent.transFigure + self.bbox_relative = None + self._redo_transform_rel_fig() + self.bbox = TransformedBbox(self.bbox_relative, + self._parent.transPanel) + self.transPanel = BboxTransformTo(self.bbox) + + self.patch = Rectangle( + xy=(0, 0), width=1, height=1, visible=frameon, + facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, + # Don't let the figure patch influence bbox calculation. + in_layout=False, transform=self.transPanel) + self.patch.set_antialiased(False) + + if parent._layoutgrid is not None: + self.init_layoutgrid() + + def _redo_transform_rel_fig(self, margins=(0, 0, 0, 0), bbox=None): + """ + make the transPanel bbox relative to Figure transform + :param: margins + """ + + if bbox is not None: + self.bbox_relative.p0 = bbox.p0 + self.bbox_relative.p1 = bbox.p1 + return + + left, bottom, right, top = margins + + gs = self._subplotspec.get_gridspec() + # need to figure out *where* this subplotspec is. + wr = gs.get_width_ratios() + hr = gs.get_height_ratios() + nrows, ncols = gs.get_geometry() + if wr is None: + wr = np.ones(ncols) + else: + wr = np.array(wr) + if hr is None: + hr = np.ones(nrows) + else: + hr = np.array(hr) + widthf = np.sum(wr[self._subplotspec.colspan]) / np.sum(wr) + heightf = np.sum(hr[self._subplotspec.rowspan]) / np.sum(hr) + widthf *= 1-(left + right) + heightf *= 1-(bottom + top) + + x0 = left + if self._subplotspec.colspan[0] > 0: + x0 += np.sum(wr[self._subplotspec.colspan[0] - 1]) / np.sum(wr) + + y0 = bottom + if self._subplotspec.rowspan[-1] < nrows - 1: + y0 += 1 - np.sum(hr[self._subplotspec.rowspan[-1]-1]) / np.sum(hr) + + if self.bbox_relative is None: + self.bbox_relative = Bbox.from_bounds(x0, y0, widthf, heightf) + else: + self.bbox_relative.p0 = (x0, y0) + self.bbox_relative.p1 = (x0 + widthf, y0 + heightf) + + def get_size_inches(self): + return self._parent.get_size_inches() + + def get_constrained_layout(self): + return self._parent.get_constrained_layout() + + def get_constrained_layout_pads(self, relative=False): + return self._parent.get_constrained_layout_pads(relative=relative) + + def init_layoutgrid(self): + """Initialize the layoutgrid for use in constrained_layout.""" + if self._layoutgrid is None: + gs = self._subplotspec.get_gridspec() + parent = gs._layoutgrid + self._layoutgrid = layoutgrid.LayoutGrid( + parent=parent, + name=(parent.name + '.' + 'panellb' + + layoutgrid.seq_id()), + parent_inner=True, + nrows=1, ncols=1, + parent_pos=(self._subplotspec.rowspan, + self._subplotspec.colspan)) + + def get_axes(self): + """ + Return a list of axes in the SubPanel. You can access and modify the + axes in the Figure through this list. + """ + return self._localaxes + + def add_axes(self, *args, **kwargs): + ax = super().add_axes(*args, **kwargs) + self._localaxes += [ax] + return ax + + axes = property(get_axes, doc=""" + List of axes in the Figure. You can access and modify the axes + in the Figure through this list. + + Do not modify the list itself. Instead, use "`~.SubPanel.add_axes`, + `~.SubPanel.add_subplot` or `~.SubPanel.delaxes` to add or remove an + axes. + """) + + def draw(self, renderer): + # docstring inherited + self._cachedRenderer = renderer + + # draw the figure bounding box, perhaps none for white figure + if not self.get_visible(): + return + + artists = self._get_draw_artists(renderer) + + try: + self.patch.draw(renderer) + mimage._draw_list_compositing_images(renderer, self, artists) + for sfig in self.panels: + sfig.draw(renderer) + + finally: + self.stale = False + + +class Figure(PanelBase): + """ + The top level container for all the plot elements. + + The Figure instance supports callbacks through a *callbacks* attribute + which is a `.CallbackRegistry` instance. The events you can connect to + are 'dpi_changed', and the callback will be called with ``func(fig)`` where + fig is the `Figure` instance. + + Attributes + ---------- + patch + The `.Rectangle` instance representing the figure background patch. + + suppressComposite + For multiple figure images, the figure will make composite images + depending on the renderer option_image_nocomposite function. If + *suppressComposite* is a boolean, this will override the renderer. + """ + + def __str__(self): + return "Figure(%gx%g)" % tuple(self.bbox.size) + + def __repr__(self): + return "<{clsname} size {h:g}x{w:g} with {naxes} Axes>".format( + clsname=self.__class__.__name__, + h=self.bbox.size[0], w=self.bbox.size[1], + naxes=len(self.axes), + ) + + def __init__(self, + figsize=None, + dpi=None, + facecolor=None, + edgecolor=None, + linewidth=0.0, + frameon=None, + subplotpars=None, # rc figure.subplot.* + tight_layout=None, # rc figure.autolayout + constrained_layout=None, # rc figure.constrained_layout.use + ): + """ + Parameters + ---------- + figsize : 2-tuple of floats, default: :rc:`figure.figsize` + Figure dimension ``(width, height)`` in inches. + + dpi : float, default: :rc:`figure.dpi` + Dots per inch. + + facecolor : default: :rc:`figure.facecolor` + The figure patch facecolor. + + edgecolor : default: :rc:`figure.edgecolor` + The figure patch edge color. + + linewidth : float + The linewidth of the frame (i.e. the edge linewidth of the figure + patch). + + frameon : bool, default: :rc:`figure.frameon` + If ``False``, suppress drawing the figure background patch. + + subplotpars : `SubplotParams` + Subplot parameters. If not given, the default subplot + parameters :rc:`figure.subplot.*` are used. + + tight_layout : bool or dict, default: :rc:`figure.autolayout` + If ``False`` use *subplotpars*. If ``True`` adjust subplot + parameters using `.tight_layout` with default padding. + When providing a dict containing the keys ``pad``, ``w_pad``, + ``h_pad``, and ``rect``, the default `.tight_layout` paddings + will be overridden. + + constrained_layout : bool, default: :rc:`figure.constrained_layout.use` + If ``True`` use constrained layout to adjust positioning of plot + elements. Like ``tight_layout``, but designed to be more + flexible. See + :doc:`/tutorials/intermediate/constrainedlayout_guide` + for examples. (Note: does not work with `add_subplot` or + `~.pyplot.subplot2grid`.) + """ + super().__init__() + if facecolor is None: + facecolor = mpl.rcParams['figure.facecolor'] + if edgecolor is None: + edgecolor = mpl.rcParams['figure.edgecolor'] + if frameon is None: + frameon = mpl.rcParams['figure.frameon'] + + # remove the non-figure artist _axes property + # as it makes no sense for a figure to be _in_ an axes + # this is used by the property methods in the artist base class + # which are over-ridden in this class + del self._axes + self.callbacks = cbook.CallbackRegistry() + + if figsize is None: + figsize = mpl.rcParams['figure.figsize'] + if dpi is None: + dpi = mpl.rcParams['figure.dpi'] + + if not np.isfinite(figsize).all() or (np.array(figsize) <= 0).any(): + raise ValueError('figure size must be positive finite not ' + f'{figsize}') + self.bbox_inches = Bbox.from_bounds(0, 0, *figsize) + self.dpi_scale_trans = Affine2D().scale(dpi) + # do not use property as it will trigger + self._dpi = dpi + + self.bbox = TransformedBbox(self.bbox_inches, self.dpi_scale_trans) + self.transFigure = BboxTransformTo(self.bbox) + # a figure is top level so transPanel and transFigure are the same + self.transPanel = self.transFigure + + self.patch = Rectangle( + xy=(0, 0), width=1, height=1, visible=frameon, + facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, + # Don't let the figure patch influence bbox calculation. + in_layout=False) + self._set_artist_props(self.patch) + self.patch.set_antialiased(False) + + self.set_constrained_layout(constrained_layout) + self.set_tight_layout(tight_layout) + + FigureCanvasBase(self) # Set self.canvas. + + self._axstack = _AxesStack() # track all figure axes and current axes + self.clf() + self._cachedRenderer = None + self.subplotpars = SubplotParams() + self.panels = [] + + self.patch = Rectangle( + xy=(0, 0), width=1, height=1, visible=frameon, + facecolor=facecolor, edgecolor=edgecolor, linewidth=linewidth, + # Don't let the figure patch influence bbox calculation. + in_layout=False, transform=self.transPanel) + self.patch.set_antialiased(False) + + self.figure = self + self._layoutgrid = None + if self.get_constrained_layout(): + self.init_layoutgrid() + + # TODO: I'd like to dynamically add the _repr_html_ method + # to the figure in the right context, but then IPython doesn't + # use it, for some reason. + + def _repr_html_(self): + # We can't use "isinstance" here, because then we'd end up importing + # webagg unconditionally. + if 'WebAgg' in type(self.canvas).__name__: + from matplotlib.backends import backend_webagg + return backend_webagg.ipython_inline_display(self) + + def show(self, warn=True): + """ + If using a GUI backend with pyplot, display the figure window. + + If the figure was not created using `~.pyplot.figure`, it will lack + a `~.backend_bases.FigureManagerBase`, and this method will raise an + AttributeError. + + .. warning:: + This does not manage an GUI event loop. Consequently, the figure + may only be shown briefly or not shown at all if you or your + environment are not managing an event loop. + + Proper use cases for `.Figure.show` include running this from a + GUI application or an IPython shell. + + If you're running a pure python shell or executing a non-GUI + python script, you should use `matplotlib.pyplot.show` instead, + which takes care of managing the event loop for you. + + Parameters + ---------- + warn : bool, default: True + If ``True`` and we are not running headless (i.e. on Linux with an + unset DISPLAY), issue warning when called on a non-GUI backend. + """ + if self.canvas.manager is None: + raise AttributeError( + "Figure.show works only for figures managed by pyplot, " + "normally created by pyplot.figure()") + try: + self.canvas.manager.show() + except NonGuiException as exc: + cbook._warn_external(str(exc)) + + def get_axes(self): + """ + Return a list of axes in the Figure. You can access and modify the + axes in the Figure through this list. + + Do not modify the list itself. Instead, use `~Figure.add_axes`, + `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an axes. + + Note: This is equivalent to the property `~.Figure.axes`. + """ + return self._axstack.as_list() + + axes = property(get_axes, doc=""" + List of axes in the Figure. You can access and modify the axes in the + Figure through this list. + + Do not modify the list itself. Instead, use "`~Figure.add_axes`, + `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an axes. + """) + + def _get_dpi(self): + return self._dpi + + def _set_dpi(self, dpi, forward=True): + """ + Parameters + ---------- + dpi : float + + forward : bool + Passed on to `~.Figure.set_size_inches` + """ + if dpi == self._dpi: + # We don't want to cause undue events in backends. + return + self._dpi = dpi + self.dpi_scale_trans.clear().scale(dpi) + w, h = self.get_size_inches() + self.set_size_inches(w, h, forward=forward) + self.callbacks.process('dpi_changed', self) + + dpi = property(_get_dpi, _set_dpi, doc="The resolution in dots per inch.") + + def set_canvas(self, canvas): + """ + Set the canvas that contains the figure + + Parameters + ---------- + canvas : FigureCanvas + """ + self.canvas = canvas + + def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, + vmin=None, vmax=None, origin=None, resize=False, **kwargs): + """ + Add a non-resampled image to the figure. + + The image is attached to the lower or upper left corner depending on + *origin*. + + Parameters + ---------- + X + The image data. This is an array of one of the following shapes: + + - MxN: luminance (grayscale) values + - MxNx3: RGB values + - MxNx4: RGBA values + + xo, yo : int + The *x*/*y* image offset in pixels. + + alpha : None or float + The alpha blending value. + + norm : `matplotlib.colors.Normalize` + A `.Normalize` instance to map the luminance to the + interval [0, 1]. + + cmap : str or `matplotlib.colors.Colormap`, default: :rc:`image.cmap` + The colormap to use. + + vmin, vmax : float + If *norm* is not given, these values set the data limits for the + colormap. + + origin : {'upper', 'lower'}, default: :rc:`image.origin` + Indicates where the [0, 0] index of the array is in the upper left + or lower left corner of the axes. + + resize : bool + If *True*, resize the figure to match the given image size. + + Returns + ------- + `matplotlib.image.FigureImage` + + Other Parameters + ---------------- + **kwargs + Additional kwargs are `.Artist` kwargs passed on to `.FigureImage`. + + Notes + ----- + figimage complements the axes image (`~matplotlib.axes.Axes.imshow`) + which will be resampled to fit the current axes. If you want + a resampled image to fill the entire figure, you can define an + `~matplotlib.axes.Axes` with extent [0, 0, 1, 1]. + + Examples + -------- + :: + + f = plt.figure() + nx = int(f.get_figwidth() * f.dpi) + ny = int(f.get_figheight() * f.dpi) + data = np.random.random((ny, nx)) + f.figimage(data) + plt.show() + """ + if resize: + dpi = self.get_dpi() + figsize = [x / dpi for x in (X.shape[1], X.shape[0])] + self.set_size_inches(figsize, forward=True) + + im = mimage.FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) + im.stale_callback = _stale_figure_callback + + im.set_array(X) + im.set_alpha(alpha) + if norm is None: + im.set_clim(vmin, vmax) + self.images.append(im) + im._remove_method = self.images.remove + self.stale = True + return im + + def set_size_inches(self, w, h=None, forward=True): + """ + Set the figure size in inches. + + Call signatures:: + + fig.set_size_inches(w, h) # OR + fig.set_size_inches((w, h)) + + Parameters + ---------- + w : (float, float) or float + Width and height in inches (if height not specified as a separate + argument) or width. + h : float + Height in inches. + forward : bool, default: True + If ``True``, the canvas size is automatically updated, e.g., + you can resize the figure window from the shell. + + See Also + -------- + matplotlib.figure.Figure.get_size_inches + matplotlib.figure.Figure.set_figwidth + matplotlib.figure.Figure.set_figheight + + Notes + ----- + To transform from pixels to inches divide by `Figure.dpi`. + """ + if h is None: # Got called with a single pair as argument. + w, h = w + size = np.array([w, h]) + if not np.isfinite(size).all() or (size <= 0).any(): + raise ValueError(f'figure size must be positive finite not {size}') + self.bbox_inches.p1 = size + if forward: + canvas = getattr(self, 'canvas') + if canvas is not None: + dpi_ratio = getattr(canvas, '_dpi_ratio', 1) + manager = getattr(canvas, 'manager', None) + if manager is not None: + manager.resize(*(size * self.dpi / dpi_ratio).astype(int)) + self.stale = True + + def get_size_inches(self): + """ + Return the current size of the figure in inches. + + Returns + ------- + ndarray + The size (width, height) of the figure in inches. + + See Also + -------- + matplotlib.figure.Figure.set_size_inches + matplotlib.figure.Figure.get_figwidth + matplotlib.figure.Figure.get_figheight + + Notes + ----- + The size in pixels can be obtained by multiplying with `Figure.dpi`. + """ + return np.array(self.bbox_inches.p1) + + def get_figwidth(self): + """Return the figure width in inches.""" + return self.bbox_inches.width + + def get_figheight(self): + """Return the figure height in inches.""" + return self.bbox_inches.height + + def get_dpi(self): + """Return the resolution in dots per inch as a float.""" + return self.dpi + + def set_dpi(self, val): + """ + Set the resolution of the figure in dots-per-inch. + + Parameters + ---------- + val : float + """ + self.dpi = val + self.stale = True + + def set_figwidth(self, val, forward=True): + """ + Set the width of the figure in inches. + + Parameters + ---------- + val : float + forward : bool + See `set_size_inches`. + + See Also + -------- + matplotlib.figure.Figure.set_figheight + matplotlib.figure.Figure.set_size_inches + """ + self.set_size_inches(val, self.get_figheight(), forward=forward) + + def set_figheight(self, val, forward=True): + """ + Set the height of the figure in inches. + + Parameters + ---------- + val : float + forward : bool + See `set_size_inches`. + + See Also + -------- + matplotlib.figure.Figure.set_figwidth + matplotlib.figure.Figure.set_size_inches + """ + self.set_size_inches(self.get_figwidth(), val, forward=forward) + + def clf(self, keep_observers=False): + """ + Clear the figure. + + Set *keep_observers* to True if, for example, + a gui widget is tracking the axes in the figure. + """ + self.suppressComposite = None + self.callbacks = cbook.CallbackRegistry() + toolbar = getattr(self.canvas, 'toolbar', None) + if toolbar is not None: + toolbar.update() + + if not keep_observers: + self._axobservers = cbook.CallbackRegistry() + if self.get_constrained_layout(): + layoutgrid.nonetree(self._layoutgrid) + + super().clf() + for ax in tuple(self._localaxes): # Iterate over the copy. + ax.cla() + self.delaxes(ax) # removes ax from self._axstack + + self._axstack.clear() + + def clear(self, keep_observers=False): + """Clear the figure -- synonym for `clf`.""" + self.clf(keep_observers=keep_observers) + + @allow_rasterization + def draw(self, renderer): + # docstring inherited + self._cachedRenderer = renderer + + # draw the figure bounding box, perhaps none for white figure + if not self.get_visible(): + return + + artists = self._get_draw_artists(renderer) + + try: + renderer.open_group('figure', gid=self.get_gid()) + if self.get_constrained_layout(): + self.execute_constrained_layout(renderer) + if self.get_tight_layout() and self.axes: + try: + self.tight_layout(**self._tight_parameters) + except ValueError: + pass + # ValueError can occur when resizing a window. + + self.patch.draw(renderer) + mimage._draw_list_compositing_images( + renderer, self, artists, self.suppressComposite) + + for sfig in self.panels: + sfig.draw(renderer) + + renderer.close_group('figure') + finally: + self.stale = False + + self.canvas.draw_event(renderer) + + def draw_artist(self, a): + """ + Draw `.Artist` instance *a* only. + + This can only be called after the figure has been drawn. + """ + if self._cachedRenderer is None: + raise AttributeError("draw_artist can only be used after an " + "initial draw which caches the renderer") + a.draw(self._cachedRenderer) + + def _set_artist_props(self, a): + if a != self: + a.set_figure(self) + a.stale_callback = _stale_figure_callback + a.set_transform(self.transPanel) def __getstate__(self): state = super().__getstate__() @@ -1919,12 +2296,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 @@ -1939,661 +2313,446 @@ def __setstate__(self, state): self.__dict__ = state - # re-initialise some of the unstored state information - FigureCanvasBase(self) # Set self.canvas. - self._layoutbox = None - - if restore_to_pylab: - # lazy import to avoid circularity - import matplotlib.pyplot as plt - import matplotlib._pylab_helpers as pylab_helpers - allnums = plt.get_fignums() - num = max(allnums) + 1 if allnums else 1 - mgr = plt._backend_mod.new_figure_manager_given_figure(num, self) - pylab_helpers.Gcf._set_new_active_manager(mgr) - plt.draw_if_interactive() - - self.stale = True - - def add_axobserver(self, func): - """Whenever the axes state change, ``func(self)`` will be called.""" - # Connect a wrapper lambda and not func itself, to avoid it being - # weakref-collected. - self._axobservers.connect("_axes_change_event", lambda arg: func(arg)) - - def savefig(self, fname, *, transparent=None, **kwargs): - """ - Save the current figure. - - Call signature:: - - savefig(fname, dpi=None, facecolor='w', edgecolor='w', - orientation='portrait', papertype=None, format=None, - transparent=False, bbox_inches=None, pad_inches=0.1, - frameon=None, metadata=None) - - The available output formats depend on the backend being used. - - Parameters - ---------- - fname : str or path-like or file-like - A path, or a Python file-like object, or - possibly some backend-dependent object such as - `matplotlib.backends.backend_pdf.PdfPages`. - - If *format* is set, it determines the output format, and the file - is saved as *fname*. Note that *fname* is used verbatim, and there - is no attempt to make the extension, if any, of *fname* match - *format*, and no extension is appended. - - If *format* is not set, then the format is inferred from the - extension of *fname*, if there is one. If *format* is not - set and *fname* has no extension, then the file is saved with - :rc:`savefig.format` and the appropriate extension is appended to - *fname*. - - Other Parameters - ---------------- - dpi : float or 'figure', default: :rc:`savefig.dpi` - The resolution in dots per inch. If 'figure', use the figure's - dpi value. - - quality : int, default: :rc:`savefig.jpeg_quality` - Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. - - The image quality, on a scale from 1 (worst) to 95 (best). - Values above 95 should be avoided; 100 disables portions of - the JPEG compression algorithm, and results in large files - with hardly any gain in image quality. - - This parameter is deprecated. - - optimize : bool, default: False - Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. - - Whether the encoder should make an extra pass over the image - in order to select optimal encoder settings. - - This parameter is deprecated. - - progressive : bool, default: False - Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. - - Whether the image should be stored as a progressive JPEG file. - - This parameter is deprecated. - - facecolor : color or 'auto', default: :rc:`savefig.facecolor` - The facecolor of the figure. If 'auto', use the current figure - facecolor. - - edgecolor : color or 'auto', default: :rc:`savefig.edgecolor` - The edgecolor of the figure. If 'auto', use the current figure - edgecolor. - - orientation : {'landscape', 'portrait'} - Currently only supported by the postscript backend. - - papertype : str - One of 'letter', 'legal', 'executive', 'ledger', 'a0' through - 'a10', 'b0' through 'b10'. Only supported for postscript - output. - - format : str - The file format, e.g. 'png', 'pdf', 'svg', ... The behavior when - this is unset is documented under *fname*. - - transparent : bool - If *True*, the axes patches will all be transparent; the - figure patch will also be transparent unless facecolor - and/or edgecolor are specified via kwargs. - This is useful, for example, for displaying - a plot on top of a colored background on a web page. The - transparency of these patches will be restored to their - original values upon exit of this function. - - bbox_inches : str or `.Bbox`, default: :rc:`savefig.bbox` - Bounding box in inches: only the given portion of the figure is - saved. If 'tight', try to figure out the tight bbox of the figure. - - pad_inches : float, default: :rc:`savefig.pad_inches` - Amount of padding around the figure when bbox_inches is 'tight'. - - bbox_extra_artists : list of `~matplotlib.artist.Artist`, optional - A list of extra artists that will be considered when the - tight bbox is calculated. - - backend : str, optional - Use a non-default backend to render the file, e.g. to render a - png file with the "cairo" backend rather than the default "agg", - or a pdf file with the "pgf" backend rather than the default - "pdf". Note that the default backend is normally sufficient. See - :ref:`the-builtin-backends` for a list of valid backends for each - file format. Custom backends can be referenced as "module://...". - - metadata : dict, optional - Key/value pairs to store in the image metadata. The supported keys - and defaults depend on the image format and backend: - - - 'png' with Agg backend: See the parameter ``metadata`` of - `~.FigureCanvasAgg.print_png`. - - 'pdf' with pdf backend: See the parameter ``metadata`` of - `~.backend_pdf.PdfPages`. - - 'eps' and 'ps' with PS backend: Only 'Creator' is supported. - - pil_kwargs : dict, optional - Additional keyword arguments that are passed to - `PIL.Image.Image.save` when saving the figure. - """ - - kwargs.setdefault('dpi', mpl.rcParams['savefig.dpi']) - if transparent is None: - transparent = mpl.rcParams['savefig.transparent'] - - if transparent: - kwargs.setdefault('facecolor', 'none') - kwargs.setdefault('edgecolor', 'none') - original_axes_colors = [] - for ax in self.axes: - patch = ax.patch - original_axes_colors.append((patch.get_facecolor(), - patch.get_edgecolor())) - patch.set_facecolor('none') - patch.set_edgecolor('none') - - self.canvas.print_figure(fname, **kwargs) - - if transparent: - for ax, cc in zip(self.axes, original_axes_colors): - ax.patch.set_facecolor(cc[0]) - ax.patch.set_edgecolor(cc[1]) - - @docstring.dedent_interpd - def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw): - """ - Create a colorbar for a ScalarMappable instance, *mappable*. - - Documentation for the pyplot thin wrapper: - %(colorbar_doc)s - """ - if ax is None: - ax = self.gca() - - # Store the value of gca so that we can set it back later on. - current_ax = self.gca() - - if cax is None: - if use_gridspec and isinstance(ax, SubplotBase) \ - and (not self.get_constrained_layout()): - cax, kw = cbar.make_axes_gridspec(ax, **kw) - else: - cax, kw = cbar.make_axes(ax, **kw) - - # need to remove kws that cannot be passed to Colorbar - NON_COLORBAR_KEYS = ['fraction', 'pad', 'shrink', 'aspect', 'anchor', - 'panchor'] - cb_kw = {k: v for k, v in kw.items() if k not in NON_COLORBAR_KEYS} - cb = cbar.colorbar_factory(cax, mappable, **cb_kw) - - self.sca(current_ax) - self.stale = True - return cb - - def subplots_adjust(self, left=None, bottom=None, right=None, top=None, - wspace=None, hspace=None): - """ - Adjust the subplot layout parameters. - - Unset parameters are left unmodified; initial values are given by - :rc:`figure.subplot.[name]`. - - Parameters - ---------- - left : float, optional - The position of the left edge of the subplots, - as a fraction of the figure width. - right : float, optional - The position of the right edge of the subplots, - as a fraction of the figure width. - bottom : float, optional - The position of the bottom edge of the subplots, - as a fraction of the figure height. - top : float, optional - The position of the top edge of the subplots, - as a fraction of the figure height. - wspace : float, optional - The width of the padding between subplots, - as a fraction of the average axes width. - hspace : float, optional - The height of the padding between subplots, - as a fraction of the average axes height. - """ - if self.get_constrained_layout(): - self.set_constrained_layout(False) - cbook._warn_external("This figure was using " - "constrained_layout==True, but that is " - "incompatible with subplots_adjust and or " - "tight_layout: setting " - "constrained_layout==False. ") - self.subplotpars.update(left, bottom, right, top, wspace, hspace) - for ax in self.axes: - if not isinstance(ax, SubplotBase): - # Check if sharing a subplots axis - if isinstance(ax._sharex, SubplotBase): - ax._sharex.update_params() - ax.set_position(ax._sharex.figbox) - elif isinstance(ax._sharey, SubplotBase): - ax._sharey.update_params() - ax.set_position(ax._sharey.figbox) - else: - ax.update_params() - ax.set_position(ax.figbox) + # re-initialise some of the unstored state information + FigureCanvasBase(self) # Set self.canvas. + self._layoutgrid = None + + if restore_to_pylab: + # lazy import to avoid circularity + import matplotlib.pyplot as plt + import matplotlib._pylab_helpers as pylab_helpers + allnums = plt.get_fignums() + num = max(allnums) + 1 if allnums else 1 + mgr = plt._backend_mod.new_figure_manager_given_figure(num, self) + pylab_helpers.Gcf._set_new_active_manager(mgr) + plt.draw_if_interactive() + self.stale = True - def ginput(self, n=1, timeout=30, show_clicks=True, - mouse_add=MouseButton.LEFT, - mouse_pop=MouseButton.RIGHT, - mouse_stop=MouseButton.MIDDLE): - """ - Blocking call to interact with a figure. + def add_axobserver(self, func): + """Whenever the axes state change, ``func(self)`` will be called.""" + # Connect a wrapper lambda and not func itself, to avoid it being + # weakref-collected. + self._axobservers.connect("_axes_change_event", lambda arg: func(arg)) - Wait until the user clicks *n* times on the figure, and return the - coordinates of each click in a list. + def savefig(self, fname, *, transparent=None, **kwargs): + """ + Save the current figure. - There are three possible interactions: + Call signature:: - - Add a point. - - Remove the most recently added point. - - Stop the interaction and return the points added so far. + savefig(fname, dpi=None, facecolor='w', edgecolor='w', + orientation='portrait', papertype=None, format=None, + transparent=False, bbox_inches=None, pad_inches=0.1, + frameon=None, metadata=None) - The actions are assigned to mouse buttons via the arguments - *mouse_add*, *mouse_pop* and *mouse_stop*. + The available output formats depend on the backend being used. Parameters ---------- - n : int, default: 1 - Number of mouse clicks to accumulate. If negative, accumulate - clicks until the input is terminated manually. - timeout : float, default: 30 seconds - Number of seconds to wait before timing out. If zero or negative - will never timeout. - show_clicks : bool, default: True - If True, show a red cross at the location of each click. - mouse_add : `.MouseButton` or None, default: `.MouseButton.LEFT` - Mouse button used to add points. - mouse_pop : `.MouseButton` or None, default: `.MouseButton.RIGHT` - Mouse button used to remove the most recently added point. - mouse_stop : `.MouseButton` or None, default: `.MouseButton.MIDDLE` - Mouse button used to stop input. - - Returns - ------- - list of tuples - A list of the clicked (x, y) coordinates. + fname : str or path-like or file-like + A path, or a Python file-like object, or + possibly some backend-dependent object such as + `matplotlib.backends.backend_pdf.PdfPages`. - Notes - ----- - The keyboard can also be used to select points in case your mouse - does not have one or more of the buttons. The delete and backspace - keys act like right clicking (i.e., remove last point), the enter key - terminates input and any other key (not already used by the window - manager) selects a point. - """ - blocking_mouse_input = BlockingMouseInput(self, - mouse_add=mouse_add, - mouse_pop=mouse_pop, - mouse_stop=mouse_stop) - return blocking_mouse_input(n=n, timeout=timeout, - show_clicks=show_clicks) + If *format* is set, it determines the output format, and the file + is saved as *fname*. Note that *fname* is used verbatim, and there + is no attempt to make the extension, if any, of *fname* match + *format*, and no extension is appended. - def waitforbuttonpress(self, timeout=-1): - """ - Blocking call to interact with the figure. + If *format* is not set, then the format is inferred from the + extension of *fname*, if there is one. If *format* is not + set and *fname* has no extension, then the file is saved with + :rc:`savefig.format` and the appropriate extension is appended to + *fname*. - Wait for user input and return True if a key was pressed, False if a - mouse button was pressed and None if no input was given within - *timeout* seconds. Negative values deactivate *timeout*. - """ - blocking_input = BlockingKeyMouseInput(self) - return blocking_input(timeout=timeout) + Other Parameters + ---------------- + dpi : float or 'figure', default: :rc:`savefig.dpi` + The resolution in dots per inch. If 'figure', use the figure's + dpi value. - def get_default_bbox_extra_artists(self): - bbox_artists = [artist for artist in self.get_children() - if (artist.get_visible() and artist.get_in_layout())] - for ax in self.axes: - if ax.get_visible(): - bbox_artists.extend(ax.get_default_bbox_extra_artists()) - return bbox_artists + quality : int, default: :rc:`savefig.jpeg_quality` + Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. - def get_tightbbox(self, renderer, bbox_extra_artists=None): - """ - Return a (tight) bounding box of the figure in inches. + The image quality, on a scale from 1 (worst) to 95 (best). + Values above 95 should be avoided; 100 disables portions of + the JPEG compression algorithm, and results in large files + with hardly any gain in image quality. - Artists that have ``artist.set_in_layout(False)`` are not included - in the bbox. + This parameter is deprecated. - Parameters - ---------- - renderer : `.RendererBase` subclass - renderer that will be used to draw the figures (i.e. - ``fig.canvas.get_renderer()``) + optimize : bool, default: False + Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. - bbox_extra_artists : list of `.Artist` or ``None`` - List of artists to include in the tight bounding box. If - ``None`` (default), then all artist children of each axes are - included in the tight bounding box. + Whether the encoder should make an extra pass over the image + in order to select optimal encoder settings. - Returns - ------- - `.BboxBase` - containing the bounding box (in figure inches). - """ + This parameter is deprecated. - bb = [] - if bbox_extra_artists is None: - artists = self.get_default_bbox_extra_artists() - else: - artists = bbox_extra_artists + progressive : bool, default: False + Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. - for a in artists: - bbox = a.get_tightbbox(renderer) - if bbox is not None and (bbox.width != 0 or bbox.height != 0): - bb.append(bbox) + Whether the image should be stored as a progressive JPEG file. - for ax in self.axes: - if ax.get_visible(): - # some axes don't take the bbox_extra_artists kwarg so we - # need this conditional.... - try: - bbox = ax.get_tightbbox( - renderer, bbox_extra_artists=bbox_extra_artists) - except TypeError: - bbox = ax.get_tightbbox(renderer) - bb.append(bbox) - bb = [b for b in bb - if (np.isfinite(b.width) and np.isfinite(b.height) - and (b.width != 0 or b.height != 0))] + This parameter is deprecated. - if len(bb) == 0: - return self.bbox_inches + facecolor : color or 'auto', default: :rc:`savefig.facecolor` + The facecolor of the figure. If 'auto', use the current figure + facecolor. - _bbox = Bbox.union(bb) + edgecolor : color or 'auto', default: :rc:`savefig.edgecolor` + The edgecolor of the figure. If 'auto', use the current figure + edgecolor. - bbox_inches = TransformedBbox(_bbox, Affine2D().scale(1 / self.dpi)) + orientation : {'landscape', 'portrait'} + Currently only supported by the postscript backend. - return bbox_inches + papertype : str + One of 'letter', 'legal', 'executive', 'ledger', 'a0' through + 'a10', 'b0' through 'b10'. Only supported for postscript + output. - 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.) + format : str + The file format, e.g. 'png', 'pdf', 'svg', ... The behavior when + this is unset is documented under *fname*. - def execute_constrained_layout(self, renderer=None): - """ - Use ``layoutbox`` to determine pos positions within axes. + transparent : bool + If *True*, the axes patches will all be transparent; the + figure patch will also be transparent unless facecolor + and/or edgecolor are specified via kwargs. + This is useful, for example, for displaying + a plot on top of a colored background on a web page. The + transparency of these patches will be restored to their + original values upon exit of this function. - See also `.set_constrained_layout_pads`. - """ + bbox_inches : str or `.Bbox`, default: :rc:`savefig.bbox` + Bounding box in inches: only the given portion of the figure is + saved. If 'tight', try to figure out the tight bbox of the figure. - from matplotlib._constrained_layout import do_constrained_layout + pad_inches : float, default: :rc:`savefig.pad_inches` + Amount of padding around the figure when bbox_inches is 'tight'. - _log.debug('Executing constrainedlayout') - if self._layoutbox is None: - cbook._warn_external("Calling figure.constrained_layout, but " - "figure not setup to do constrained layout. " - " You either called GridSpec without the " - "fig keyword, you are using plt.subplot, " - "or you need to call figure or subplots " - "with the constrained_layout=True kwarg.") - return - w_pad, h_pad, wspace, hspace = self.get_constrained_layout_pads() - # convert to unit-relative lengths - fig = self - width, height = fig.get_size_inches() - w_pad = w_pad / width - h_pad = h_pad / height - if renderer is None: - renderer = layoutbox.get_renderer(fig) - do_constrained_layout(fig, renderer, h_pad, w_pad, hspace, wspace) + bbox_extra_artists : list of `~matplotlib.artist.Artist`, optional + A list of extra artists that will be considered when the + tight bbox is calculated. - @cbook._delete_parameter("3.2", "renderer") - def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None, - rect=None): - """ - Adjust the padding between and around subplots. + backend : str, optional + Use a non-default backend to render the file, e.g. to render a + png file with the "cairo" backend rather than the default "agg", + or a pdf file with the "pgf" backend rather than the default + "pdf". Note that the default backend is normally sufficient. See + :ref:`the-builtin-backends` for a list of valid backends for each + file format. Custom backends can be referenced as "module://...". - To exclude an artist on the axes from the bounding box calculation - that determines the subplot parameters (i.e. legend, or annotation), - set ``a.set_in_layout(False)`` for that artist. + metadata : dict, optional + Key/value pairs to store in the image metadata. The supported keys + and defaults depend on the image format and backend: - Parameters - ---------- - renderer : subclass of `~.backend_bases.RendererBase`, optional - Defaults to the renderer for the figure. Deprecated. - pad : float, default: 1.08 - Padding between the figure edge and the edges of subplots, - as a fraction of the font size. - h_pad, w_pad : float, default: *pad* - Padding (height/width) between edges of adjacent subplots, - as a fraction of the font size. - rect : tuple (left, bottom, right, top), default: (0, 0, 1, 1) - A rectangle in normalized figure coordinates into which the whole - subplots area (including labels) will fit. + - 'png' with Agg backend: See the parameter ``metadata`` of + `~.FigureCanvasAgg.print_png`. + - 'pdf' with pdf backend: See the parameter ``metadata`` of + `~.backend_pdf.PdfPages`. + - 'eps' and 'ps' with PS backend: Only 'Creator' is supported. - See Also - -------- - .Figure.set_tight_layout - .pyplot.tight_layout + pil_kwargs : dict, optional + Additional keyword arguments that are passed to + `PIL.Image.Image.save` when saving the figure. """ - from .tight_layout import ( - get_renderer, get_subplotspec_list, get_tight_layout_figure) + kwargs.setdefault('dpi', mpl.rcParams['savefig.dpi']) + if transparent is None: + transparent = mpl.rcParams['savefig.transparent'] - subplotspec_list = get_subplotspec_list(self.axes) - if None in subplotspec_list: - cbook._warn_external("This figure includes Axes that are not " - "compatible with tight_layout, so results " - "might be incorrect.") + if transparent: + kwargs.setdefault('facecolor', 'none') + kwargs.setdefault('edgecolor', 'none') + original_axes_colors = [] + for ax in self.axes: + patch = ax.patch + original_axes_colors.append((patch.get_facecolor(), + patch.get_edgecolor())) + patch.set_facecolor('none') + patch.set_edgecolor('none') - if renderer is None: - renderer = get_renderer(self) + self.canvas.print_figure(fname, **kwargs) - kwargs = get_tight_layout_figure( - self, self.axes, subplotspec_list, renderer, - pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) - if kwargs: - self.subplots_adjust(**kwargs) + if transparent: + for ax, cc in zip(self.axes, original_axes_colors): + ax.patch.set_facecolor(cc[0]) + ax.patch.set_edgecolor(cc[1]) - def align_xlabels(self, axs=None): + def ginput(self, n=1, timeout=30, show_clicks=True, + mouse_add=MouseButton.LEFT, + mouse_pop=MouseButton.RIGHT, + mouse_stop=MouseButton.MIDDLE): """ - Align the ylabels of subplots in the same subplot column if label - alignment is being done automatically (i.e. the label position is - not manually set). + Blocking call to interact with a figure. - Alignment persists for draw events after this is called. + Wait until the user clicks *n* times on the figure, and return the + coordinates of each click in a list. - If a label is on the bottom, it is aligned with labels on axes that - also have their label on the bottom and that have the same - bottom-most subplot row. If the label is on the top, - it is aligned with labels on axes with the same top-most row. + There are three possible interactions: + + - Add a point. + - Remove the most recently added point. + - Stop the interaction and return the points added so far. + + The actions are assigned to mouse buttons via the arguments + *mouse_add*, *mouse_pop* and *mouse_stop*. Parameters ---------- - axs : list of `~matplotlib.axes.Axes` - Optional list of (or ndarray) `~matplotlib.axes.Axes` - to align the xlabels. - Default is to align all axes on the figure. + n : int, default: 1 + Number of mouse clicks to accumulate. If negative, accumulate + clicks until the input is terminated manually. + timeout : float, default: 30 seconds + Number of seconds to wait before timing out. If zero or negative + will never timeout. + show_clicks : bool, default: True + If True, show a red cross at the location of each click. + mouse_add : `.MouseButton` or None, default: `.MouseButton.LEFT` + Mouse button used to add points. + mouse_pop : `.MouseButton` or None, default: `.MouseButton.RIGHT` + Mouse button used to remove the most recently added point. + mouse_stop : `.MouseButton` or None, default: `.MouseButton.MIDDLE` + Mouse button used to stop input. - See Also - -------- - matplotlib.figure.Figure.align_ylabels - matplotlib.figure.Figure.align_labels + Returns + ------- + list of tuples + A list of the clicked (x, y) coordinates. Notes ----- - This assumes that ``axs`` are from the same `.GridSpec`, so that - their `.SubplotSpec` positions correspond to figure positions. - - Examples - -------- - Example with rotated xtick labels:: + The keyboard can also be used to select points in case your mouse + does not have one or more of the buttons. The delete and backspace + keys act like right clicking (i.e., remove last point), the enter key + terminates input and any other key (not already used by the window + manager) selects a point. + """ + blocking_mouse_input = BlockingMouseInput(self, + mouse_add=mouse_add, + mouse_pop=mouse_pop, + mouse_stop=mouse_stop) + return blocking_mouse_input(n=n, timeout=timeout, + show_clicks=show_clicks) - fig, axs = plt.subplots(1, 2) - for tick in axs[0].get_xticklabels(): - tick.set_rotation(55) - axs[0].set_xlabel('XLabel 0') - axs[1].set_xlabel('XLabel 1') - fig.align_xlabels() + def waitforbuttonpress(self, timeout=-1): """ - if axs is None: - axs = self.axes - axs = np.ravel(axs) - for ax in axs: - _log.debug(' Working on: %s', ax.get_xlabel()) - rowspan = ax.get_subplotspec().rowspan - pos = ax.xaxis.get_label_position() # top or bottom - # Search through other axes for label positions that are same as - # this one and that share the appropriate row number. - # Add to a grouper associated with each axes of siblings. - # This list is inspected in `axis.draw` by - # `axis._update_label_position`. - for axc in axs: - if axc.xaxis.get_label_position() == pos: - rowspanc = axc.get_subplotspec().rowspan - if (pos == 'top' and rowspan.start == rowspanc.start or - pos == 'bottom' and rowspan.stop == rowspanc.stop): - # grouper for groups of xlabels to align - self._align_xlabel_grp.join(ax, axc) + Blocking call to interact with the figure. - def align_ylabels(self, axs=None): + Wait for user input and return True if a key was pressed, False if a + mouse button was pressed and None if no input was given within + *timeout* seconds. Negative values deactivate *timeout*. """ - Align the ylabels of subplots in the same subplot column if label - alignment is being done automatically (i.e. the label position is - not manually set). + blocking_input = BlockingKeyMouseInput(self) + return blocking_input(timeout=timeout) - Alignment persists for draw events after this is called. + def get_tight_layout(self): + """Return whether `.tight_layout` is called when drawing.""" + return self._tight - If a label is on the left, it is aligned with labels on axes that - also have their label on the left and that have the same - left-most subplot column. If the label is on the right, - it is aligned with labels on axes with the same right-most column. + def set_tight_layout(self, tight): + """ + Set whether and how `.tight_layout` is called when drawing. Parameters ---------- - axs : list of `~matplotlib.axes.Axes` - Optional list (or ndarray) of `~matplotlib.axes.Axes` - to align the ylabels. - Default is to align all axes on the figure. + tight : bool or dict with keys "pad", "w_pad", "h_pad", "rect" or None + If a bool, sets whether to call `.tight_layout` upon drawing. + If ``None``, use the ``figure.autolayout`` rcparam instead. + If a dict, pass it as kwargs to `.tight_layout`, overriding the + default paddings. + """ + if tight is None: + tight = mpl.rcParams['figure.autolayout'] + self._tight = bool(tight) + self._tight_parameters = tight if isinstance(tight, dict) else {} + self.stale = True - See Also - -------- - matplotlib.figure.Figure.align_xlabels - matplotlib.figure.Figure.align_labels + def get_constrained_layout(self): + """ + Return whether constrained layout is being used. - Notes - ----- - This assumes that ``axs`` are from the same `.GridSpec`, so that - their `.SubplotSpec` positions correspond to figure positions. + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + """ + return self._constrained - Examples - -------- - Example with large yticks labels:: + def set_constrained_layout(self, constrained): + """ + Set whether ``constrained_layout`` is used upon drawing. If None, + :rc:`figure.constrained_layout.use` value will be used. - fig, axs = plt.subplots(2, 1) - axs[0].plot(np.arange(0, 1000, 50)) - axs[0].set_ylabel('YLabel 0') - axs[1].set_ylabel('YLabel 1') - fig.align_ylabels() + When providing a dict containing the keys `w_pad`, `h_pad` + the default ``constrained_layout`` paddings will be + overridden. These pads are in inches and default to 3.0/72.0. + ``w_pad`` is the width padding and ``h_pad`` is the height padding. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + + Parameters + ---------- + constrained : bool or dict or None """ - if axs is None: - axs = self.axes - axs = np.ravel(axs) - for ax in axs: - _log.debug(' Working on: %s', ax.get_ylabel()) - colspan = ax.get_subplotspec().colspan - pos = ax.yaxis.get_label_position() # left or right - # Search through other axes for label positions that are same as - # this one and that share the appropriate column number. - # Add to a list associated with each axes of siblings. - # This list is inspected in `axis.draw` by - # `axis._update_label_position`. - for axc in axs: - if axc.yaxis.get_label_position() == pos: - colspanc = axc.get_subplotspec().colspan - if (pos == 'left' and colspan.start == colspanc.start or - pos == 'right' and colspan.stop == colspanc.stop): - # grouper for groups of ylabels to align - self._align_ylabel_grp.join(ax, axc) + self._constrained_layout_pads = dict() + self._constrained_layout_pads['w_pad'] = None + self._constrained_layout_pads['h_pad'] = None + self._constrained_layout_pads['wspace'] = None + self._constrained_layout_pads['hspace'] = None + if constrained is None: + constrained = mpl.rcParams['figure.constrained_layout.use'] + self._constrained = bool(constrained) + if isinstance(constrained, dict): + self.set_constrained_layout_pads(**constrained) + else: + self.set_constrained_layout_pads() + + self.stale = True + + def set_constrained_layout_pads(self, **kwargs): + """ + Set padding for ``constrained_layout``. Note the kwargs can be passed + as a dictionary ``fig.set_constrained_layout(**paddict)``. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + + Parameters + ---------- + w_pad : float + Width padding in inches. This is the pad around axes + and is meant to make sure there is enough room for fonts to + look good. Defaults to 3 pts = 0.04167 inches + + h_pad : float + Height padding in inches. Defaults to 3 pts. + + wspace : float + Width padding between subplots, expressed as a fraction of the + subplot width. The total padding ends up being w_pad + wspace. + + hspace : float + Height padding between subplots, expressed as a fraction of the + subplot width. The total padding ends up being h_pad + hspace. - def align_labels(self, axs=None): """ - Align the xlabels and ylabels of subplots with the same subplots - row or column (respectively) if label alignment is being - done automatically (i.e. the label position is not manually set). - Alignment persists for draw events after this is called. + todo = ['w_pad', 'h_pad', 'wspace', 'hspace'] + for td in todo: + if td in kwargs and kwargs[td] is not None: + self._constrained_layout_pads[td] = kwargs[td] + else: + self._constrained_layout_pads[td] = ( + mpl.rcParams['figure.constrained_layout.' + td]) + + def get_constrained_layout_pads(self, relative=False): + """ + Get padding for ``constrained_layout``. + + Returns a list of ``w_pad, h_pad`` in inches and + ``wspace`` and ``hspace`` as fractions of the subplot. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide`. Parameters ---------- - axs : list of `~matplotlib.axes.Axes` - Optional list (or ndarray) of `~matplotlib.axes.Axes` - to align the labels. - Default is to align all axes on the figure. + relative : bool + If `True`, then convert from inches to figure relative. + """ + w_pad = self._constrained_layout_pads['w_pad'] + h_pad = self._constrained_layout_pads['h_pad'] + wspace = self._constrained_layout_pads['wspace'] + hspace = self._constrained_layout_pads['hspace'] - See Also - -------- - matplotlib.figure.Figure.align_xlabels + if relative and (w_pad is not None or h_pad is not None): + renderer0 = layoutgrid.get_renderer(self) + dpi = renderer0.dpi + w_pad = w_pad * dpi / renderer0.width + h_pad = h_pad * dpi / renderer0.height - matplotlib.figure.Figure.align_ylabels + return w_pad, h_pad, wspace, hspace + + def execute_constrained_layout(self, renderer=None): """ - self.align_xlabels(axs=axs) - self.align_ylabels(axs=axs) + Use ``layoutgrid`` to determine pos positions within axes. - def add_gridspec(self, nrows=1, ncols=1, **kwargs): + See also `.set_constrained_layout_pads`. """ - Return a `.GridSpec` that has this figure as a parent. This allows - complex layout of axes in the figure. - Parameters - ---------- - nrows : int, default: 1 - Number of rows in grid. + from matplotlib._constrained_layout import do_constrained_layout - ncols : int, default: 1 - Number or columns in grid. + _log.debug('Executing constrainedlayout') + 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 " + "fig keyword, you are using plt.subplot, " + "or you need to call figure or subplots " + "with the constrained_layout=True kwarg.") + return + w_pad, h_pad, wspace, hspace = self.get_constrained_layout_pads() + # convert to unit-relative lengths + fig = self + width, height = fig.get_size_inches() + w_pad = w_pad / width + h_pad = h_pad / height + if renderer is None: + renderer = layoutgrid.get_renderer(fig) + do_constrained_layout(fig, renderer, h_pad, w_pad, hspace, wspace) - Returns - ------- - `.GridSpec` + @cbook._delete_parameter("3.2", "renderer") + def tight_layout(self, renderer=None, pad=1.08, h_pad=None, w_pad=None, + rect=None): + """ + Adjust the padding between and around subplots. - Other Parameters - ---------------- - **kwargs - Keyword arguments are passed to `.GridSpec`. + To exclude an artist on the axes from the bounding box calculation + that determines the subplot parameters (i.e. legend, or annotation), + set ``a.set_in_layout(False)`` for that artist. + + Parameters + ---------- + renderer : subclass of `~.backend_bases.RendererBase`, optional + Defaults to the renderer for the figure. Deprecated. + pad : float, default: 1.08 + Padding between the figure edge and the edges of subplots, + as a fraction of the font size. + h_pad, w_pad : float, default: *pad* + Padding (height/width) between edges of adjacent subplots, + as a fraction of the font size. + rect : tuple (left, bottom, right, top), default: (0, 0, 1, 1) + A rectangle in normalized figure coordinates into which the whole + subplots area (including labels) will fit. See Also -------- - matplotlib.pyplot.subplots + .Figure.set_tight_layout + .pyplot.tight_layout + """ - Examples - -------- - Adding a subplot that spans two rows:: + from .tight_layout import ( + get_renderer, get_subplotspec_list, get_tight_layout_figure) - fig = plt.figure() - gs = fig.add_gridspec(2, 2) - ax1 = fig.add_subplot(gs[0, 0]) - ax2 = fig.add_subplot(gs[1, 0]) - # spans two rows: - ax3 = fig.add_subplot(gs[:, 1]) + subplotspec_list = get_subplotspec_list(self.axes) + if None in subplotspec_list: + cbook._warn_external("This figure includes Axes that are not " + "compatible with tight_layout, so results " + "might be incorrect.") - """ + if renderer is None: + renderer = get_renderer(self) - _ = kwargs.pop('figure', None) # pop in case user has added this... - gs = GridSpec(nrows=nrows, ncols=ncols, figure=self, **kwargs) - self._gridspecs.append(gs) - return gs + kwargs = get_tight_layout_figure( + self, self.axes, subplotspec_list, renderer, + pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) + if kwargs: + self.subplots_adjust(**kwargs) + + def init_layoutgrid(self): + """Initialize the layoutgrid for use in constrained_layout.""" + if self._layoutgrid is None: + self._layoutgrid = layoutgrid.LayoutGrid( + parent=None, name='figlb') def figaspect(arg): @@ -2676,3 +2835,116 @@ def figaspect(arg): return newsize docstring.interpd.update(Figure=martist.kwdoc(Figure)) + + + +class _AxesStack(cbook.Stack): + """ + Specialization of `.Stack`, to handle all tracking of `~.axes.Axes` in a + `.Figure`. + + This stack stores ``key, (ind, axes)`` pairs, where: + + * **key** is a hash of the args and kwargs used in generating the Axes. + * **ind** is a serial index tracking the order in which axes were added. + + AxesStack is a callable; calling it returns the current axes. + The `current_key_axes` method returns the current key and associated axes. + """ + + def __init__(self): + super().__init__() + self._ind = 0 + + def as_list(self): + """ + Return a list of the Axes instances that have been added to the figure. + """ + ia_list = [a for k, a in self._elements] + ia_list.sort() + return [a for i, a in ia_list] + + def get(self, key): + """ + Return the Axes instance that was added with *key*. + If it is not present, return *None*. + """ + item = dict(self._elements).get(key) + if item is None: + return None + cbook.warn_deprecated( + "2.1", + message="Adding an axes using the same arguments as a previous " + "axes currently reuses the earlier instance. In a future " + "version, a new instance will always be created and returned. " + "Meanwhile, this warning can be suppressed, and the future " + "behavior ensured, by passing a unique label to each axes " + "instance.") + return item[1] + + def _entry_from_axes(self, e): + ind, k = {a: (ind, k) for k, (ind, a) in self._elements}[e] + return (k, (ind, e)) + + def remove(self, a): + """Remove the axes from the stack.""" + super().remove(self._entry_from_axes(a)) + + def bubble(self, a): + """ + Move the given axes, which must already exist in the + stack, to the top. + """ + return super().bubble(self._entry_from_axes(a)) + + def add(self, key, a): + """ + Add Axes *a*, with key *key*, to the stack, and return the stack. + + If *key* is unhashable, replace it by a unique, arbitrary object. + + If *a* is already on the stack, don't add it again, but + return *None*. + """ + # All the error checking may be unnecessary; but this method + # is called so seldom that the overhead is negligible. + cbook._check_isinstance(Axes, a=a) + try: + hash(key) + except TypeError: + key = object() + + a_existing = self.get(key) + if a_existing is not None: + super().remove((key, a_existing)) + cbook._warn_external( + "key {!r} already existed; Axes is being replaced".format(key)) + # I don't think the above should ever happen. + + if a in self: + return None + self._ind += 1 + return super().push((key, (self._ind, a))) + + def current_key_axes(self): + """ + Return a tuple of ``(key, axes)`` for the active axes. + + If no axes exists on the stack, then returns ``(None, None)``. + """ + if not len(self._elements): + return self._default, self._default + else: + key, (index, axes) = self._elements[self._pos] + return key, axes + + def __call__(self): + return self.current_key_axes()[1] + + def __contains__(self, a): + return a in self.as_list() + + +@cbook.deprecated("3.2") +class AxesStack(_AxesStack): + pass diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 5aa425b0a66a..104981459c76 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__) @@ -419,22 +420,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): """ @@ -563,16 +566,26 @@ def __init__(self, nrows, ncols, GridSpecBase.__init__(self, 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 implimented, + # but we do this so that its possible to have subgridspec + # level artists. + self._toplayoutbox = 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._toplayoutbox, + name=(self._toplayoutbox.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.""" @@ -621,18 +634,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()}[" @@ -703,7 +704,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/patches.py b/lib/matplotlib/patches.py index d94155550d27..4d51a790fa18 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -4254,6 +4254,8 @@ def _get_xy(self, xy, s, axes=None): s = s.replace("points", "pixels") elif s == "figure fraction": s = self.figure.transFigure + elif s == "panel fraction": + s = self.figure.transPanel elif s == "axes fraction": s = axes.transAxes x, y = xy diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 32f63db834c1..38d618f5f1bd 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -40,7 +40,7 @@ from matplotlib import cbook from matplotlib import docstring from matplotlib.backend_bases import FigureCanvasBase, MouseButton -from matplotlib.figure import Figure, figaspect +from matplotlib.figure import Figure, figaspect, PanelBase from matplotlib.gridspec import GridSpec from matplotlib import rcParams, rcParamsDefault, get_backend, rcParamsOrig from matplotlib.rcsetup import interactive_bk as _interactive_bk @@ -1068,7 +1068,7 @@ def subplot(*args, **kwargs): two subplots that are otherwise identical to be added to the figure, make sure you give them unique labels. - In rare circumstances, `.add_subplot` may be called with a single + In rare circumstances, `.Figure.add_subplot` may be called with a single argument, a subplot axes instance already created in the present figure but not in the figure's list of axes. @@ -2258,7 +2258,7 @@ def figtext(x, y, s, fontdict=None, **kwargs): # Autogenerated by boilerplate.py. Do not edit as changes will be lost. -@_copy_docstring_and_deprecators(Figure.gca) +@_copy_docstring_and_deprecators(PanelBase.gca) def gca(**kwargs): return gcf().gca(**kwargs) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index bafcb6fe8108..377948bdc4e5 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -355,6 +355,7 @@ def _set_transform(self): self.set_transform(cbook._check_getitem({ "data": self.Q.axes.transData, "axes": self.Q.axes.transAxes, + "panel": self.Q.axes.figure.transPanel, "figure": self.Q.axes.figure.transFigure, "inches": self.Q.axes.figure.dpi_scale_trans, }, coordinates=self.coord)) diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout1.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout1.png index a12759614401..8d77462f028e 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout1.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout1.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout10.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout10.png index 32c5089c6404..5f03141f8d74 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout10.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout10.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11.png index e8974806d993..f729c84d99d3 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11rat.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11rat.png index 403a3acc64ca..4994f11defee 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11rat.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11rat.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11subpanel.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11subpanel.png new file mode 100644 index 000000000000..0edb8aad4718 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11subpanel.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png index 3e24209e18b0..1f63b43a1257 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout13.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout13.png index 92c18947f9e6..1709bf3ed8ca 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout13.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout13.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout14.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout14.png index 82fba5397e36..07e5c1ec985a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout14.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout14.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout15.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout15.png index fb3915574226..f13c16914a50 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout15.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout15.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout16.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout16.png index 2c20f43dcf5f..245083733675 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout16.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout16.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout17.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout17.png index 41ceab8f38d2..6ef2ae56da04 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout17.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout17.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout2.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout2.png index 7a72a8117335..215d373a2026 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout2.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout2.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout3.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout3.png index 61337c0eeb21..79a73e6dd8b8 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout3.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout3.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.pdf b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.pdf deleted file mode 100644 index 0cf2edcae0be..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.png index a1e26c37c1a2..3ea195899553 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout5.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout5.png index 2bc15ba74e29..f9379a97d6b4 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout5.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout5.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout6.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout6.png index ed300a445c56..2f6c313573aa 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout6.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout6.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png index 78c49877ffd7..fd12eadbfb93 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout9.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout9.png index c262adc048fb..252003b1a9ec 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout9.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout9.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png index 4cc1ee216fd2..6eaa1c6206a8 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_colorbar_location.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_overlapping_gridspecs2.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_overlapping_gridspecs2.png new file mode 100644 index 000000000000..9361ea136bd5 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_overlapping_gridspecs2.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_overlapping_gridspecs3.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_overlapping_gridspecs3.png new file mode 100644 index 000000000000..e24b60d5949e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_overlapping_gridspecs3.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_text/large_subscript_title.png b/lib/matplotlib/tests/baseline_images/test_text/large_subscript_title.png index 36c0eeb5f88e..162169f7c72a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_text/large_subscript_title.png and b/lib/matplotlib/tests/baseline_images/test_text/large_subscript_title.png differ diff --git a/lib/matplotlib/tests/test_arrow_patches.py b/lib/matplotlib/tests/test_arrow_patches.py index 2218a2e7f429..8404a9e091b7 100644 --- a/lib/matplotlib/tests/test_arrow_patches.py +++ b/lib/matplotlib/tests/test_arrow_patches.py @@ -44,7 +44,7 @@ def test_boxarrow(): fig.text(0.5, ((n - i) * spacing - 0.5)/figheight, stylename, ha="center", size=fontsize, - transform=fig.transFigure, + transform=fig.transPanel, bbox=dict(boxstyle=stylename, fc="w", ec="k")) diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 46e6b9663ef3..8b3b5d6f5195 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -1,5 +1,4 @@ import numpy as np -import pytest from matplotlib.testing.decorators import image_comparison import matplotlib.pyplot as plt @@ -61,7 +60,7 @@ def test_constrained_layout3(): fig.colorbar(pcm, ax=ax, pad=pad) -@image_comparison(['constrained_layout4']) +@image_comparison(['constrained_layout4.png']) def test_constrained_layout4(): """Test constrained_layout for a single colorbar with subplots""" fig, axs = plt.subplots(2, 2, constrained_layout=True) @@ -108,21 +107,6 @@ def test_constrained_layout6(): ticks=ticker.MaxNLocator(nbins=5)) -def test_constrained_layout7(): - """Test for proper warning if fig not set in GridSpec""" - with pytest.warns( - UserWarning, match=('Calling figure.constrained_layout, but figure ' - 'not setup to do constrained layout')): - fig = plt.figure(constrained_layout=True) - gs = gridspec.GridSpec(1, 2) - gsl = gridspec.GridSpecFromSubplotSpec(2, 2, gs[0]) - gsr = gridspec.GridSpecFromSubplotSpec(1, 2, gs[1]) - for gs in gsl: - fig.add_subplot(gs) - # need to trigger a draw to get warning - fig.draw(fig.canvas.get_renderer()) - - @image_comparison(['constrained_layout8.png']) def test_constrained_layout8(): """Test for gridspecs that are not completely full""" @@ -179,9 +163,10 @@ def test_constrained_layout11(): fig = plt.figure(constrained_layout=True, figsize=(13, 3)) gs0 = gridspec.GridSpec(1, 2, figure=fig) gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0]) - gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1]) ax = fig.add_subplot(gs0[1]) example_plot(ax, fontsize=9) + + gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1]) axs = [] for gs in gsl0: ax = fig.add_subplot(gs) @@ -192,6 +177,30 @@ def test_constrained_layout11(): example_plot(ax, fontsize=9) +@image_comparison(['constrained_layout11subpanel.png'], + style='default') +def test_constrained_layout11subpanel(): + """Test for nested mixed layout with subpanels""" + fig = plt.figure(constrained_layout=True, figsize=(13, 3)) + gs0 = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[1.75, 1]) + + spl = fig.add_subpanel(gs0[0], facecolor='0.8') + ax = fig.add_subplot(gs0[1]) + example_plot(ax, fontsize=9) + + # split left side in two: + gsl = spl.add_gridspec(1, 2) + + ax = spl.add_subplot(gsl[0]) + example_plot(ax, fontsize=9) + #split right side of left side into 2x2 + spl0 = spl.add_subpanel(gsl[1], facecolor='0.7') + axs = spl0.subplots(2, 2) + for ax in axs.flat: + pcm = example_pcolor(ax, fontsize=9) + spl0.colorbar(pcm, ax=axs, shrink=0.6, aspect=20.) + + @image_comparison(['constrained_layout11rat.png']) def test_constrained_layout11rat(): """Test for multiple nested gridspecs with width_ratios""" @@ -214,7 +223,7 @@ def test_constrained_layout11rat(): @image_comparison(['constrained_layout12.png']) def test_constrained_layout12(): """Test that very unbalanced labeling still works.""" - fig = plt.figure(constrained_layout=True) + fig = plt.figure(constrained_layout=True, figsize=(5, 7)) gs0 = gridspec.GridSpec(6, 2, figure=fig) @@ -356,17 +365,11 @@ def test_constrained_layout23(): Comment in #11035: suptitle used to cause an exception when reusing a figure w/ CL with ``clear=True``. """ - for i in range(2): fig, ax = plt.subplots(num="123", constrained_layout=True, clear=True) fig.suptitle("Suptitle{}".format(i)) -# This test occasionally fails the image comparison tests, so we mark as -# flaky. Apparently the constraint solver occasionally doesn't fully -# optimize. Would be nice if this were more deterministic... -@pytest.mark.timeout(30) -@pytest.mark.flaky(reruns=3) @image_comparison(['test_colorbar_location.png'], remove_text=True, style='mpl20') def test_colorbar_location(): @@ -398,4 +401,40 @@ def test_hidden_axes(): extents1 = np.copy(axs[0, 0].get_position().extents) np.testing.assert_allclose( - extents1, [0.045552, 0.548288, 0.47319, 0.982638], rtol=1e-5) + extents1, [0.045552, 0.541343, 0.478398, 0.982638], rtol=1e-5) + + +@image_comparison(['test_overlapping_gridspecs2.png']) +def test_overlapping_gridspecs2(): + # This type of layout is challenging because the inner + # margins are not constrained. Tested above in 12 for vertical + # here we do horizontally... + fig = plt.figure(constrained_layout=True, figsize=(8, 5)) + gs0 = gridspec.GridSpec(2, 6, figure=fig) + + ax1 = fig.add_subplot(gs0[1, :3]) + ax2 = fig.add_subplot(gs0[1, 3:]) + + example_plot(ax1, fontsize=24) + example_plot(ax2, fontsize=24) + + ax = fig.add_subplot(gs0[0, 0:2]) + example_plot(ax) + ax = fig.add_subplot(gs0[0, 2:4]) + example_plot(ax) + ax = fig.add_subplot(gs0[0, 4:]) + example_plot(ax) + ax.set_xlabel('x-label') + + +@image_comparison(['test_overlapping_gridspecs3.png']) +def test_overlapping_gridspecs3(): + # this is like grid_strategy. + fig = plt.figure(constrained_layout=True, figsize=(5, 5)) + gs = fig.add_gridspec(2, 4) + for i in range(2): + aa = fig.add_subplot(gs[0, i * 2:(i + 1) * 2]) + aa.set_ylabel('YY', fontsize=22 * (i + 1)) + for i in range(1): + a3 = fig.add_subplot(gs[1, (i * 2 + 1):((i + 1) * 2 + 1)]) + a3.set_ylabel('YY', fontsize=28) diff --git a/lib/matplotlib/tight_layout.py b/lib/matplotlib/tight_layout.py index eddd0d678736..5450624177a3 100644 --- a/lib/matplotlib/tight_layout.py +++ b/lib/matplotlib/tight_layout.py @@ -87,7 +87,7 @@ def auto_adjust_subplotpars( tight_bbox_raw = Bbox.union(bb) tight_bbox = TransformedBbox(tight_bbox_raw, - fig.transFigure.inverted()) + fig.transPanel.inverted()) row1, col1 = divmod(num1, cols) if num2 is None: @@ -116,7 +116,7 @@ def auto_adjust_subplotpars( + pad_inches / fig_height_inch) suptitle = fig._suptitle if suptitle and suptitle.get_in_layout(): - rel_suptitle_height = fig.transFigure.inverted().transform_bbox( + rel_suptitle_height = fig.transPanel.inverted().transform_bbox( suptitle.get_window_extent(renderer)).height margin_top += rel_suptitle_height + pad_inches / fig_height_inch if not margin_bottom: diff --git a/tutorials/intermediate/artists.py b/tutorials/intermediate/artists.py index ed13c91a5e09..a282941b0f72 100644 --- a/tutorials/intermediate/artists.py +++ b/tutorials/intermediate/artists.py @@ -323,8 +323,8 @@ class in the matplotlib API, and the one you will be working with most fig = plt.figure() -l1 = lines.Line2D([0, 1], [0, 1], transform=fig.transFigure, figure=fig) -l2 = lines.Line2D([0, 1], [1, 0], transform=fig.transFigure, figure=fig) +l1 = lines.Line2D([0, 1], [0, 1], transform=fig.transPanel, figure=fig) +l2 = lines.Line2D([0, 1], [1, 0], transform=fig.transPanel, figure=fig) fig.lines.extend([l1, l2]) plt.show() diff --git a/tutorials/intermediate/constrainedlayout_guide.py b/tutorials/intermediate/constrainedlayout_guide.py index 1975153db181..8f233bc8d405 100644 --- a/tutorials/intermediate/constrainedlayout_guide.py +++ b/tutorials/intermediate/constrainedlayout_guide.py @@ -147,31 +147,6 @@ def example_plot(ax, fontsize=12, hide_labels=False): fig.colorbar(im, ax=axs[1:, ][:, 1], shrink=0.8) fig.colorbar(im, ax=axs[:, -1], shrink=0.6) -############################################################################ -# Note that there is a bit of a subtlety when specifying a single axes -# as the parent. In the following, it might be desirable and expected -# for the colorbars to line up, but they don't because the colorbar paired -# with the bottom axes is tied to the subplotspec of the axes, and hence -# shrinks when the gridspec-level colorbar is added. - -fig, axs = plt.subplots(3, 1, figsize=(4, 4), constrained_layout=True) -for ax in axs[:2]: - im = ax.pcolormesh(arr, **pc_kwargs) -fig.colorbar(im, ax=axs[:2], shrink=0.6) -im = axs[2].pcolormesh(arr, **pc_kwargs) -fig.colorbar(im, ax=axs[2], shrink=0.6) - -############################################################################ -# The API to make a single-axes behave like a list of axes is to specify -# it as a list (or other iterable container), as below: - -fig, axs = plt.subplots(3, 1, figsize=(4, 4), constrained_layout=True) -for ax in axs[:2]: - im = ax.pcolormesh(arr, **pc_kwargs) -fig.colorbar(im, ax=axs[:2], shrink=0.6) -im = axs[2].pcolormesh(arr, **pc_kwargs) -fig.colorbar(im, ax=[axs[2]], shrink=0.6) - #################################################### # Suptitle # ========= @@ -260,8 +235,8 @@ def example_plot(ax, fontsize=12, hide_labels=False): # # For constrained_layout, we have implemented a padding around the edge of # each axes. This padding sets the distance from the edge of the plot, -# and the minimum distance between adjacent plots. It is specified in -# inches by the keyword arguments ``w_pad`` and ``h_pad`` to the function +# and the distance between adjacent plots. It is specified in inches by +# the keyword arguments ``w_pad`` and ``h_pad`` to the function # `~.figure.Figure.set_constrained_layout_pads`: fig, axs = plt.subplots(2, 2, constrained_layout=True) @@ -278,15 +253,14 @@ def example_plot(ax, fontsize=12, hide_labels=False): # Spacing between subplots is set by ``wspace`` and ``hspace``. There are # specified as a fraction of the size of the subplot group as a whole. # If the size of the figure is changed, then these spaces change in -# proportion. Note in the below how the space at the edges doesn't change from -# the above, but the space between subplots does. +# proportion. Note in the below how the space at the edges doesn't change +# from the above, but the space between subplots does. fig, axs = plt.subplots(2, 2, constrained_layout=True) for ax in axs.flat: example_plot(ax, hide_labels=True) fig.set_constrained_layout_pads(w_pad=2/72, h_pad=2/72, hspace=0.2, wspace=0.2) - ########################################## # Spacing with colorbars # ----------------------- @@ -382,14 +356,15 @@ def example_plot(ax, fontsize=12, hide_labels=False): ax.set_title("") ax.set_xlabel("") -ax.set_xlabel("x-label", fontsize=12) +ax.set_xlabel("x-label", fontsize=12); ############################################################################ -# Note that in the above the left and columns don't have the same vertical -# extent. If we want the top and bottom of the two grids to line up then -# they need to be in the same gridspec: +# Note that in the above the left and right columns don't have the same +# vertical extent. If we want the top and bottom of the two grids to line up +# then they need to be in the same gridspec. We need to make this figure +# larger as well in order for the axes not to collapse to zero height: -fig = plt.figure() +fig = plt.figure(figsize=(4, 6)) gs0 = fig.add_gridspec(6, 2) @@ -400,11 +375,12 @@ def example_plot(ax, fontsize=12, hide_labels=False): example_plot(ax2) ax = fig.add_subplot(gs0[0:2, 1]) -example_plot(ax) +example_plot(ax, hide_labels=True) ax = fig.add_subplot(gs0[2:4, 1]) -example_plot(ax) +example_plot(ax, hide_labels=True) ax = fig.add_subplot(gs0[4:, 1]) -example_plot(ax) +example_plot(ax, hide_labels=True) +fig.suptitle('Overlapping Gridspecs') ############################################################################ # This example uses two gridspecs to have the colorbar only pertain to @@ -450,26 +426,6 @@ def docomplicated(suptitle=None): example_plot(axs[0], fontsize=12) axs[1].set_position([0.2, 0.2, 0.4, 0.4]) -############################################################################### -# If you want an inset axes in data-space, you need to manually execute the -# layout using ``fig.execute_constrained_layout()`` call. The inset figure -# will then be properly positioned. However, it will not be properly -# positioned if the size of the figure is subsequently changed. Similarly, -# if the figure is printed to another backend, there may be slight changes -# of location due to small differences in how the backends render fonts. - -from matplotlib.transforms import Bbox - -fig, axs = plt.subplots(1, 2) -example_plot(axs[0], fontsize=12) -fig.execute_constrained_layout() -# put into data-space: -bb_data_ax2 = Bbox.from_bounds(0.5, 1., 0.2, 0.4) -disp_coords = axs[0].transData.transform(bb_data_ax2) -fig_coords_ax2 = fig.transFigure.inverted().transform(disp_coords) -bb_ax2 = Bbox(fig_coords_ax2) -ax2 = fig.add_axes(bb_ax2) - ############################################################################### # Manually turning off ``constrained_layout`` # =========================================== @@ -492,7 +448,7 @@ def docomplicated(suptitle=None): # Incompatible functions # ---------------------- # -# ``constrained_layout`` will not work on subplots created via +# ``constrained_layout`` currently will not work on subplots created via # `.pyplot.subplot`. The reason is that each call to `.pyplot.subplot` creates # a separate `.GridSpec` instance and ``constrained_layout`` uses (nested) # gridspecs to carry out the layout. So the following fails to yield a nice @@ -524,8 +480,8 @@ def docomplicated(suptitle=None): ############################################################################### # Similarly, -# :func:`~matplotlib.pyplot.subplot2grid` doesn't work for the same reason: -# each call creates a different parent gridspec. +# :func:`~matplotlib.pyplot.subplot2grid` doesn't (yet) work for the same +# reason: each call creates a different parent gridspec. fig = plt.figure() @@ -601,144 +557,83 @@ def docomplicated(suptitle=None): # The algorithm for the constraint is relatively straightforward, but # has some complexity due to the complex ways we can layout a figure. # -# Figure layout -# ------------- -# -# Figures are laid out in a hierarchy: -# -# 1. Figure: ``fig = plt.figure()`` -# -# a. Gridspec ``gs0 = gridspec.GridSpec(1, 2, figure=fig)`` -# -# i. Subplotspec: ``ss = gs[0, 0]`` +# Layout in Matplotlib is carried out with gridspecs +# via the `~.GridSpec` class. A gridspec is a logical division of the figure +# into rows and columns, with the reltive width of the axes in those +# rows and columns set by width_ratios and height ratios. # -# 1. Axes: ``ax0 = fig.add_subplot(ss)`` -# -# ii. Subplotspec: ``ss = gs[0, 1]`` -# -# 1. Gridspec: ``gsR = gridspec.GridSpecFromSubplotSpec(2, 1, ss)`` -# -# - Subplotspec: ``ss = gsR[0, 0]`` -# -# - Axes: ``axR0 = fig.add_subplot(ss)`` -# -# - Subplotspec: ``ss = gsR[1, 0]`` -# -# - Axes: ``axR1 = fig.add_subplot(ss)`` -# -# Each item has a layoutbox associated with it. The nesting of gridspecs -# created with `.GridSpecFromSubplotSpec` can be arbitrarily deep. -# -# Each `~matplotlib.axes.Axes` has *two* layoutboxes. The first one, -# ``ax._layoutbox`` represents the outside of the Axes and all its -# decorations (i.e. ticklabels, axis labels, etc.). -# The second layoutbox corresponds to the Axes' ``ax.position``, which sets -# where in the figure the spines are placed. -# -# Why so many stacked containers? Ideally, all that would be needed are the -# Axes layout boxes. For the Gridspec case, a container is -# needed if the Gridspec is nested via `.GridSpecFromSubplotSpec`. At the -# top level, it is desirable for symmetry, but it also makes room for -# `~.Figure.suptitle`. -# -# For the Subplotspec/Axes case, Axes often have colorbars or other -# annotations that need to be packaged inside the Subplotspec, hence the -# need for the outer layer. +# In constrained_layout, each gridspec gets a *layoutgrid* associated with +# it. The *layoutgrid* has a series of ``left`` and ``right`` variables +# for each column, and ``bottom`` and ``top`` variables for each row, and +# further it has a margin for each of left, right, bottom and top. +# Constrained_layout the inner part of each cell the same width and height +# and adjusts the overall width and height to accommodate the margins. # # # Simple case: one Axes # --------------------- # -# For a single Axes the layout is straight forward. The Figure and -# outer Gridspec layoutboxes coincide. The Subplotspec and Axes -# boxes also coincide because the Axes has no colorbar. Note -# the difference between the red ``pos`` box and the green ``ax`` box -# is set by the size of the decorations around the Axes. +# For a single Axes the layout is straight forward. There is one parent +# layoutgrid for the figure consisting of one column and row, and +# a child layoutgrid for the gridspec that contains the axes, again +# consisting of one row and column. # +# The Figure and # In the code, this is accomplished by the entries in # ``do_constrained_layout()`` like:: # -# ax._poslayoutbox.edit_left_margin_min(-bbox.x0 + pos.x0 + w_padt) +# gridspec._layoutgrid[0, 0].edit_margin_min('left', +# -bbox.x0 + pos.x0 + w_pad) # +# where ``bbox`` is the tight bounding box of the axes, and ``pos`` its +# position. Note how the four margins encompass the axes decorations. -from matplotlib._layoutbox import plot_children +from matplotlib._layoutgrid import plot_children fig, ax = plt.subplots(constrained_layout=True) example_plot(ax, fontsize=24) -plot_children(fig, fig._layoutbox, printit=False) +plot_children(fig, fig._layoutgrid) ####################################################################### # Simple case: two Axes # --------------------- -# For this case, the Axes layoutboxes and the Subplotspec boxes still -# co-incide. However, because the decorations in the right-hand plot are so -# much smaller than the left-hand, so the right-hand layoutboxes are smaller. -# -# The Subplotspec boxes are laid out in the code in the subroutine -# ``arange_subplotspecs()``, which simply checks the subplotspecs in the code -# against one another and stacks them appropriately. -# -# The two ``pos`` axes are lined up. Because they have the same -# minimum row, they are lined up at the top. Because -# they have the same maximum row they are lined up at the bottom. In the -# code this is accomplished via the calls to ``layoutbox.align``. If -# there was more than one row, then the same horizontal alignment would -# occur between the rows. -# -# The two ``pos`` axes are given the same width because the subplotspecs -# occupy the same number of columns. This is accomplished in the code where -# ``dcols0`` is compared to ``dcolsC``. If they are equal, then their widths -# are constrained to be equal. -# -# While it is a bit subtle in this case, note that the division between the -# Subplotspecs is *not* centered, but has been moved to the right to make -# space for the larger labels on the left-hand plot. +# When there are multiple axes they have their layouts bound in +# simple ways. In this example the left axes has much larger decorations +# than the right, but they share a bottom margin, which is made large +# enough to accommodate the larger xlabel. Same with the shared top +# margin. The left and right margins are not shared, and hence are +# different. fig, ax = plt.subplots(1, 2, constrained_layout=True) example_plot(ax[0], fontsize=32) example_plot(ax[1], fontsize=8) -plot_children(fig, fig._layoutbox, printit=False) +plot_children(fig, fig._layoutgrid, printit=False) ####################################################################### # Two Axes and colorbar # --------------------- # -# Adding a colorbar makes it clear why the Subplotspec layoutboxes must -# be different from the axes layoutboxes. Here we see the left-hand -# subplotspec has more room to accommodate the `~.Figure.colorbar`, and -# that there are two green ``ax`` boxes inside the ``ss`` box. -# -# Note that the width of the ``pos`` boxes is still the same because of the -# constraint on their widths because their subplotspecs occupy the same -# number of columns (one in this example). -# -# The colorbar layout logic is contained in `~matplotlib.colorbar.make_axes` -# which calls ``_constrained_layout.layoutcolorbarsingle()`` -# for cbars attached to a single axes, and -# ``_constrained_layout.layoutcolorbargridspec()`` if the colorbar is -# associated with a gridspec. +# A colorbar is simply another item that expands the margin of the parent +# layoutgrid cell: fig, ax = plt.subplots(1, 2, constrained_layout=True) im = ax[0].pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=ax[0], shrink=0.6) im = ax[1].pcolormesh(arr, **pc_kwargs) -plot_children(fig, fig._layoutbox, printit=False) +plot_children(fig, fig._layoutgrid) ####################################################################### # Colorbar associated with a Gridspec # ----------------------------------- # -# This example shows the Subplotspec layoutboxes being made smaller by -# a colorbar layoutbox. The size of the colorbar layoutbox is -# set to be ``shrink`` smaller than the vertical extent of the ``pos`` -# layoutboxes in the gridspec, and it is made to be centered between -# those two points. +# If a colorbar belongs to more than one cell of the grid, then +# it makes a larger margin for each: fig, axs = plt.subplots(2, 2, constrained_layout=True) for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs, shrink=0.6) -plot_children(fig, fig._layoutbox, printit=False) +plot_children(fig, fig._layoutgrid, printit=False) ####################################################################### # Uneven sized Axes @@ -768,7 +663,7 @@ def docomplicated(suptitle=None): im = ax.pcolormesh(arr, **pc_kwargs) ax = fig.add_subplot(gs[1, 1]) im = ax.pcolormesh(arr, **pc_kwargs) -plot_children(fig, fig._layoutbox, printit=False) +plot_children(fig, fig._layoutgrid, printit=False) ####################################################################### # Height and width ratios are accommodated with the same part of @@ -776,45 +671,27 @@ def docomplicated(suptitle=None): # than the larger. fig = plt.figure(constrained_layout=True) -gs = gridspec.GridSpec(3, 2, figure=fig, - height_ratios=[1., 0.5, 1.5], width_ratios=[1.2, 0.8]) -ax = fig.add_subplot(gs[:2, 0]) -im = ax.pcolormesh(arr, **pc_kwargs) -ax = fig.add_subplot(gs[2, 0]) +gs = gridspec.GridSpec(2, 2, figure=fig, width_ratios=[1, 2], + height_ratios=[2, 1]) +ax = fig.add_subplot(gs[:, 0]) im = ax.pcolormesh(arr, **pc_kwargs) ax = fig.add_subplot(gs[0, 1]) im = ax.pcolormesh(arr, **pc_kwargs) -ax = fig.add_subplot(gs[1:, 1]) +ax = fig.add_subplot(gs[1, 1]) im = ax.pcolormesh(arr, **pc_kwargs) -plot_children(fig, fig._layoutbox, printit=False) +plot_children(fig, fig._layoutgrid, printit=False) -######################################################################## -# Empty gridspec slots -# -------------------- -# -# The final piece of the code that has not been explained is what happens if -# there is an empty gridspec opening. In that case a fake invisible axes is -# added and we proceed as before. The empty gridspec has no decorations, but -# the axes position in made the same size as the occupied Axes positions. -# -# This is done at the start of -# ``_constrained_layout.do_constrained_layout()`` (``hassubplotspec``). +####################################################################### +# One case that requires finessing if margins do not have any artists +# constraining their width. In the case below, the right margin for column 0 +# and the left margin for column 3 have no margin artists to set their width, +# so we take the maximum width of the margin widths that have artists. +# This makes all the axes have the same size: fig = plt.figure(constrained_layout=True) -gs = gridspec.GridSpec(1, 3, figure=fig) -ax = fig.add_subplot(gs[0]) -im = ax.pcolormesh(arr, **pc_kwargs) -ax = fig.add_subplot(gs[-1]) -im = ax.pcolormesh(arr, **pc_kwargs) -plot_children(fig, fig._layoutbox, printit=False) -plt.show() - -######################################################################## -# Other notes -# ----------- -# -# The layout is called only once. This is OK if the original layout was -# pretty close (which it should be in most cases). However, if the layout -# changes a lot from the default layout then the decorators can change size. -# In particular the x and ytick labels can change. If this happens, then -# we should probably call the whole routine twice. +gs = fig.add_gridspec(2, 4) +ax00 = fig.add_subplot(gs[0, 0:2]) +ax01 = fig.add_subplot(gs[0, 2:]) +ax10 = fig.add_subplot(gs[1, 1:3]) +example_plot(ax10, fontsize=14) +plot_children(fig, fig._layoutgrid) diff --git a/tutorials/intermediate/legend_guide.py b/tutorials/intermediate/legend_guide.py index 82b98f0ec053..57dd1b1d8273 100644 --- a/tutorials/intermediate/legend_guide.py +++ b/tutorials/intermediate/legend_guide.py @@ -111,7 +111,7 @@ # the corner's location and the coordinate system of that location:: # # plt.legend(bbox_to_anchor=(1, 1), -# bbox_transform=plt.gcf().transFigure) +# bbox_transform=plt.gcf().transPanel) # # More examples of custom legend placement: