diff --git a/doc/users/next_whats_new/legend-figure-outside.rst b/doc/users/next_whats_new/legend-figure-outside.rst new file mode 100644 index 000000000000..a78725b4e2b6 --- /dev/null +++ b/doc/users/next_whats_new/legend-figure-outside.rst @@ -0,0 +1,8 @@ +Figure legends can be placed outside figures using constrained_layout +--------------------------------------------------------------------- +Constrained layout will make space for Figure legends if they are specified +by a *loc* keyword argument that starts with the string "outside". The +codes are unique from axes codes, in that "outside upper right" will +make room at the top of the figure for the legend, whereas +"outside right upper" will make room on the right-hand side of the figure. +See :doc:`/tutorials/intermediate/legend_guide` for details. diff --git a/examples/text_labels_and_annotations/figlegend_demo.py b/examples/text_labels_and_annotations/figlegend_demo.py index 6fc31235f634..c749ae795cd5 100644 --- a/examples/text_labels_and_annotations/figlegend_demo.py +++ b/examples/text_labels_and_annotations/figlegend_demo.py @@ -28,3 +28,26 @@ plt.tight_layout() plt.show() + +############################################################################## +# Sometimes we do not want the legend to overlap the axes. If you use +# constrained_layout you can specify "outside right upper", and +# constrained_layout will make room for the legend. + +fig, axs = plt.subplots(1, 2, layout='constrained') + +x = np.arange(0.0, 2.0, 0.02) +y1 = np.sin(2 * np.pi * x) +y2 = np.exp(-x) +l1, = axs[0].plot(x, y1) +l2, = axs[0].plot(x, y2, marker='o') + +y3 = np.sin(4 * np.pi * x) +y4 = np.exp(-2 * x) +l3, = axs[1].plot(x, y3, color='tab:green') +l4, = axs[1].plot(x, y4, color='tab:red', marker='^') + +fig.legend((l1, l2), ('Line 1', 'Line 2'), loc='upper left') +fig.legend((l3, l4), ('Line 3', 'Line 4'), loc='outside right upper') + +plt.show() diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 996603300620..9554a156f1ec 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -418,6 +418,25 @@ def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0, # pass the new margins down to the layout grid for the solution... layoutgrids[gs].edit_outer_margin_mins(margin, ss) + # make margins for figure-level legends: + for leg in fig.legends: + inv_trans_fig = None + if leg._outside_loc and leg._bbox_to_anchor is None: + if inv_trans_fig is None: + inv_trans_fig = fig.transFigure.inverted().transform_bbox + bbox = inv_trans_fig(leg.get_tightbbox(renderer)) + w = bbox.width + 2 * w_pad + h = bbox.height + 2 * h_pad + legendloc = leg._outside_loc + if legendloc == 'lower': + layoutgrids[fig].edit_margin_min('bottom', h) + elif legendloc == 'upper': + layoutgrids[fig].edit_margin_min('top', h) + if legendloc == 'right': + layoutgrids[fig].edit_margin_min('right', w) + elif legendloc == 'left': + layoutgrids[fig].edit_margin_min('left', w) + def make_margin_suptitles(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0): # Figure out how large the suptitle is and make the diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 58d95912665b..399a79e05cfe 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -294,7 +294,7 @@ def legend(self, *args, **kwargs): Other Parameters ---------------- - %(_legend_kw_doc)s + %(_legend_kw_axes)s See Also -------- diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index b006ca467be1..30e117183176 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1085,7 +1085,8 @@ def legend(self, *args, **kwargs): Other Parameters ---------------- - %(_legend_kw_doc)s + %(_legend_kw_figure)s + See Also -------- diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 66182c68cb60..e9e0651066b3 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -94,51 +94,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): self.legend.set_bbox_to_anchor(loc_in_bbox) -_docstring.interpd.update(_legend_kw_doc=""" -loc : str or pair of floats, default: :rc:`legend.loc` ('best' for axes, \ -'upper right' for figures) - The location of the legend. - - The strings - ``'upper left', 'upper right', 'lower left', 'lower right'`` - place the legend at the corresponding corner of the axes/figure. - - The strings - ``'upper center', 'lower center', 'center left', 'center right'`` - place the legend at the center of the corresponding edge of the - axes/figure. - - The string ``'center'`` places the legend at the center of the axes/figure. - - The string ``'best'`` places the legend at the location, among the nine - locations defined so far, with the minimum overlap with other drawn - artists. This option can be quite slow for plots with large amounts of - data; your plotting speed may benefit from providing a specific location. - - The location can also be a 2-tuple giving the coordinates of the lower-left - corner of the legend in axes coordinates (in which case *bbox_to_anchor* - will be ignored). - - For back-compatibility, ``'center right'`` (but no other location) can also - be spelled ``'right'``, and each "string" locations can also be given as a - numeric value: - - =============== ============= - Location String Location Code - =============== ============= - 'best' 0 - 'upper right' 1 - 'upper left' 2 - 'lower left' 3 - 'lower right' 4 - 'right' 5 - 'center left' 6 - 'center right' 7 - 'lower center' 8 - 'upper center' 9 - 'center' 10 - =============== ============= - +_legend_kw_doc_base = """ bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats Box that is used to position the legend in conjunction with *loc*. Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or @@ -295,7 +251,79 @@ def _update_bbox_to_anchor(self, loc_in_canvas): draggable : bool, default: False Whether the legend can be dragged with the mouse. -""") +""" + +_loc_doc_base = """ +loc : str or pair of floats, {0} + The location of the legend. + + The strings + ``'upper left', 'upper right', 'lower left', 'lower right'`` + place the legend at the corresponding corner of the axes/figure. + + The strings + ``'upper center', 'lower center', 'center left', 'center right'`` + place the legend at the center of the corresponding edge of the + axes/figure. + + The string ``'center'`` places the legend at the center of the axes/figure. + + The string ``'best'`` places the legend at the location, among the nine + locations defined so far, with the minimum overlap with other drawn + artists. This option can be quite slow for plots with large amounts of + data; your plotting speed may benefit from providing a specific location. + + The location can also be a 2-tuple giving the coordinates of the lower-left + corner of the legend in axes coordinates (in which case *bbox_to_anchor* + will be ignored). + + For back-compatibility, ``'center right'`` (but no other location) can also + be spelled ``'right'``, and each "string" locations can also be given as a + numeric value: + + =============== ============= + Location String Location Code + =============== ============= + 'best' 0 + 'upper right' 1 + 'upper left' 2 + 'lower left' 3 + 'lower right' 4 + 'right' 5 + 'center left' 6 + 'center right' 7 + 'lower center' 8 + 'upper center' 9 + 'center' 10 + =============== ============= + {1}""" + +_legend_kw_axes_st = (_loc_doc_base.format("default: :rc:`legend.loc`", '') + + _legend_kw_doc_base) +_docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st) + +_outside_doc = """ + If a figure is using the constrained layout manager, the string codes + of the *loc* keyword argument can get better layout behaviour using the + prefix 'outside'. There is ambiguity at the corners, so 'outside + upper right' will make space for the legend above the rest of the + axes in the layout, and 'outside right upper' will make space on the + right side of the layout. In addition to the values of *loc* + listed above, we have 'outside right upper', 'outside right lower', + 'outside left upper', and 'outside left lower'. See + :doc:`/tutorials/intermediate/legend_guide` for more details. +""" + +_legend_kw_figure_st = (_loc_doc_base.format("default: 'upper right'", + _outside_doc) + + _legend_kw_doc_base) +_docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st) + +_legend_kw_both_st = ( + _loc_doc_base.format("default: 'best' for axes, 'upper right' for figures", + _outside_doc) + + _legend_kw_doc_base) +_docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st) class Legend(Artist): @@ -482,13 +510,37 @@ def val_or_rc(val, rc_name): ) self.parent = parent + loc0 = loc self._loc_used_default = loc is None if loc is None: loc = mpl.rcParams["legend.loc"] if not self.isaxes and loc in [0, 'best']: loc = 'upper right' + + # handle outside legends: + self._outside_loc = None if isinstance(loc, str): + if loc.split()[0] == 'outside': + # strip outside: + loc = loc.split('outside ')[1] + # strip "center" at the beginning + self._outside_loc = loc.replace('center ', '') + # strip first + self._outside_loc = self._outside_loc.split()[0] + locs = loc.split() + if len(locs) > 1 and locs[0] in ('right', 'left'): + # locs doesn't accept "left upper", etc, so swap + if locs[0] != 'center': + locs = locs[::-1] + loc = locs[0] + ' ' + locs[1] + # check that loc is in acceptable strings loc = _api.check_getitem(self.codes, loc=loc) + + if self.isaxes and self._outside_loc: + raise ValueError( + f"'outside' option for loc='{loc0}' keyword argument only " + "works for figure legends") + if not self.isaxes and loc == 0: raise ValueError( "Automatic legend placement (loc='best') not implemented for " diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 56794a3428b8..a8d7fd107d8b 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -4,6 +4,7 @@ import warnings import numpy as np +from numpy.testing import assert_allclose import pytest from matplotlib.testing.decorators import check_figures_equal, image_comparison @@ -18,7 +19,6 @@ import matplotlib.legend as mlegend from matplotlib import rc_context from matplotlib.font_manager import FontProperties -from numpy.testing import assert_allclose def test_legend_ordereddict(): @@ -486,6 +486,47 @@ def test_warn_args_kwargs(self): "be discarded.") +def test_figure_legend_outside(): + todos = ['upper ' + pos for pos in ['left', 'center', 'right']] + todos += ['lower ' + pos for pos in ['left', 'center', 'right']] + todos += ['left ' + pos for pos in ['lower', 'center', 'upper']] + todos += ['right ' + pos for pos in ['lower', 'center', 'upper']] + + upperext = [20.347556, 27.722556, 790.583, 545.499] + lowerext = [20.347556, 71.056556, 790.583, 588.833] + leftext = [151.681556, 27.722556, 790.583, 588.833] + rightext = [20.347556, 27.722556, 659.249, 588.833] + axbb = [upperext, upperext, upperext, + lowerext, lowerext, lowerext, + leftext, leftext, leftext, + rightext, rightext, rightext] + + legbb = [[10., 555., 133., 590.], # upper left + [338.5, 555., 461.5, 590.], # upper center + [667, 555., 790., 590.], # upper right + [10., 10., 133., 45.], # lower left + [338.5, 10., 461.5, 45.], # lower center + [667., 10., 790., 45.], # lower right + [10., 10., 133., 45.], # left lower + [10., 282.5, 133., 317.5], # left center + [10., 555., 133., 590.], # left upper + [667, 10., 790., 45.], # right lower + [667., 282.5, 790., 317.5], # right center + [667., 555., 790., 590.]] # right upper + + for nn, todo in enumerate(todos): + print(todo) + fig, axs = plt.subplots(constrained_layout=True, dpi=100) + axs.plot(range(10), label='Boo1') + leg = fig.legend(loc='outside ' + todo) + fig.draw_without_rendering() + + assert_allclose(axs.get_window_extent().extents, + axbb[nn]) + assert_allclose(leg.get_window_extent().extents, + legbb[nn]) + + @image_comparison(['legend_stackplot.png']) def test_legend_stackplot(): """Test legend for PolyCollection using stackplot.""" diff --git a/tutorials/intermediate/legend_guide.py b/tutorials/intermediate/legend_guide.py index 596d6df93b55..338fca72fbf1 100644 --- a/tutorials/intermediate/legend_guide.py +++ b/tutorials/intermediate/legend_guide.py @@ -135,7 +135,54 @@ ax_dict['bottom'].legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.) -plt.show() +############################################################################## +# Figure legends +# -------------- +# +# Sometimes it makes more sense to place a legend relative to the (sub)figure +# rather than individual Axes. By using ``constrained_layout`` and +# specifying "outside" at the beginning of the *loc* keyword argument, +# the legend is drawn outside the Axes on the (sub)figure. + +fig, axs = plt.subplot_mosaic([['left', 'right']], layout='constrained') + +axs['left'].plot([1, 2, 3], label="test1") +axs['left'].plot([3, 2, 1], label="test2") + +axs['right'].plot([1, 2, 3], 'C2', label="test3") +axs['right'].plot([3, 2, 1], 'C3', label="test4") +# Place a legend to the right of this smaller subplot. +fig.legend(loc='outside upper right') + +############################################################################## +# This accepts a slightly different grammar than the normal *loc* keyword, +# where "outside right upper" is different from "outside upper right". +# +ucl = ['upper', 'center', 'lower'] +lcr = ['left', 'center', 'right'] +fig, ax = plt.subplots(figsize=(6, 4), layout='constrained', facecolor='0.7') + +ax.plot([1, 2], [1, 2], label='TEST') +# Place a legend to the right of this smaller subplot. +for loc in [ + 'outside upper left', + 'outside upper center', + 'outside upper right', + 'outside lower left', + 'outside lower center', + 'outside lower right']: + fig.legend(loc=loc, title=loc) + +fig, ax = plt.subplots(figsize=(6, 4), layout='constrained', facecolor='0.7') +ax.plot([1, 2], [1, 2], label='test') + +for loc in [ + 'outside left upper', + 'outside right upper', + 'outside left lower', + 'outside right lower']: + fig.legend(loc=loc, title=loc) + ############################################################################### # Multiple legends on the same Axes pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy