8000 ENH: add outside kwarg to figure legend · matplotlib/matplotlib@65f061c · GitHub
[go: up one dir, main page]

Skip to content
Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 65f061c

Browse files
committed
ENH: add outside kwarg to figure legend
1 parent 0e3a261 commit 65f061c

File tree

8 files changed

+243
-49
lines changed

8 files changed

+243
-49
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Figure legends can be placed outside figures using constrained_layout
2+
---------------------------------------------------------------------
3+
Constrained layout will make space for Figure legends if they are specified
4+
by a *loc* keyword argument that starts with the string "outside". The
5+
codes are unique from axes codes, in that "outside upper right" will
6+
make room at the top of the figure for the legend, whereas
7+
"outside right upper" will make room on the right-hand side of the figure.
8+
See :doc:`/tutorials/intermediate/legend_guide` for details.

examples/text_labels_and_annotations/figlegend_demo.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,26 @@
2828

2929
plt.tight_layout()
3030
plt.show()
31+
32+
##############################################################################
33+
# Sometimes we do not want the legend to overlap the axes. If you use
34+
# constrained_layout you can specify "outside right upper", and
35+
# constrained_layout will make room for the legend.
36+
37+
fig, axs = plt.subplots(1, 2, layout='constrained')
38+
39+
x = np.arange(0.0, 2.0, 0.02)
40+
y1 = np.sin(2 * np.pi * x)
41+
y2 = np.exp(-x)
42+
l1, = axs[0].plot(x, y1)
43+
l2, = axs[0].plot(x, y2, marker='o')
44+
45+
y3 = np.sin(4 * np.pi * x)
46+
y4 = np.exp(-2 * x)
47+
l3, = axs[1].plot(x, y3, color='tab:green')
48+
l4, = axs[1].plot(x, y4, color='tab:red', marker='^')
49+
50+
fig.legend((l1, l2), ('Line 1', 'Line 2'), loc='upper left')
51+
fig.legend((l3, l4), ('Line 3', 'Line 4'), loc='outside right upper')
52+
53+
plt.show()

lib/matplotlib/_constrained_layout.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,25 @@ def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0,
418418
# pass the new margins down to the layout grid for the solution...
419419
layoutgrids[gs].edit_outer_margin_mins(margin, ss)
420420

421+
# make margins for figure-level legends:
422+
for leg in fig.legends:
423+
inv_trans_fig = None
424+
if leg._outside and leg._bbox_to_anchor is None:
425+
if inv_trans_fig is None:
426+
inv_trans_fig = fig.transFigure.inverted().transform_bbox
427+
bbox = inv_trans_fig(leg.get_tightbbox(renderer))
428+
w = bbox.width + 2 * w_pad
429+
h = bbox.height + 2 * h_pad
430+
legendloc = leg._outside
431+
if legendloc == 'lower':
432+
layoutgrids[fig].edit_margin_min('bottom', h)
433+
elif legendloc == 'upper':
434+
layoutgrids[fig].edit_margin_min('top', h)
435+
if legendloc == 'right':
436+
layoutgrids[fig].edit_margin_min('right', w)
437+
elif legendloc == 'left':
438+
layoutgrids[fig].edit_margin_min('left', w)
439+
421440

422441
def make_margin_suptitles(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0):
423442
# Figure out how large the suptitle is and make the

lib/matplotlib/axes/_axes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def legend(self, *args, **kwargs):
294294
295295
Other Parameters
296296
----------------
297-
%(_legend_kw_doc)s
297+
%(_legend_kw_axes)s
298298
299299
See Also
300300
--------

lib/matplotlib/figure.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1085,7 +1085,8 @@ def legend(self, *args, **kwargs):
10851085
10861086
Other Parameters
10871087
----------------
1088-
%(_legend_kw_doc)s
1088+
%(_legend_kw_figure)s
1089+
10891090
10901091
See Also
10911092
--------

lib/matplotlib/legend.py

Lines changed: 100 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -94,51 +94,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
9494
self.legend.set_bbox_to_anchor(loc_in_bbox)
9595

9696

97-
_docstring.interpd.update(_legend_kw_doc="""
98-
loc : str or pair of floats, default: :rc:`legend.loc` ('best' for axes, \
99-
'upper right' for figures)
100-
The location of the legend.
101-
102-
The strings
103-
``'upper left', 'upper right', 'lower left', 'lower right'``
104-
place the legend at the corresponding corner of the axes/figure.
105-
106-
The strings
107-
``'upper center', 'lower center', 'center left', 'center right'``
108-
place the legend at the center of the corresponding edge of the
109-
axes/figure.
110-
111-
The string ``'center'`` places the legend at the center of the axes/figure.
112-
113-
The string ``'best'`` places the legend at the location, among the nine
114-
locations defined so far, with the minimum overlap with other drawn
115-
artists. This option can be quite slow for plots with large amounts of
116-
data; your plotting speed may benefit from providing a specific location.
117-
118-
The location can also be a 2-tuple giving the coordinates of the lower-left
119-
corner of the legend in axes coordinates (in which case *bbox_to_anchor*
120-
will be ignored).
121-
122-
For back-compatibility, ``'center right'`` (but no other location) can also
123-
be spelled ``'right'``, and each "string" locations can also be given as a
124-
numeric value:
125-
126-
=============== =============
127-
Location String Location Code
128-
=============== =============
129-
'best' 0
130-
'upper right' 1
131-
'upper left' 2
132-
'lower left' 3
133-
'lower right' 4
134-
'right' 5
135-
'center left' 6
136-
'center right' 7
137-
'lower center' 8
138-
'upper center' 9
139-
'center' 10
140-
=============== =============
141-
97+
_legend_kw_doc_base = """
14298
bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
14399
Box that is used to position the legend in conjunction with *loc*.
144100
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):
295251
296252
draggable : bool, default: False
297253
Whether the legend can be dragged with the mouse.
298-
""")
254+
"""
255+
256+
_loc_doc_base = """
257+
loc : str or pair of floats, {0}
258+
The location of the legend.
259+
260+
The strings
261+
``'upper left', 'upper right', 'lower left', 'lower right'``
262+
place the legend at the corresponding corner of the axes/figure.
263+
264+
The strings
265+
``'upper center', 'lower center', 'center left', 'center right'``
266+
1241 place the legend at the center of the corresponding edge of the
267+
axes/figure.
268+
269+
The string ``'center'`` places the legend at the center of the axes/figure.
270+
271+
The string ``'best'`` places the legend at the location, among the nine
272+
locations defined so far, with the minimum overlap with other drawn
273+
artists. This option can be quite slow for plots with large amounts of
274+
data; your plotting speed may benefit from providing a specific location.
275+
276+
The location can also be a 2-tuple giving the coordinates of the lower-left
277+
corner of the legend in axes coordinates (in which case *bbox_to_anchor*
278+
will be ignored).
279+
280+
For back-compatibility, ``'center right'`` (but no other location) can also
281+
be spelled ``'right'``, and each "string" locations can also be given as a
282+
numeric value:
283+
284+
=============== =============
285+
Location String Location Code
286+
=============== =============
287+
'best' 0
288+
'upper right' 1
289+
'upper left' 2
290+
'lower left' 3
291+
'lower right' 4
292+
'right' 5
293+
'center left' 6
294+
'center right' 7
295+
'lower center' 8
296+
'upper center' 9
297+
'center' 10
298+
=============== =============
299+
{1}"""
300+
301+
_legend_kw_axes_st = (_loc_doc_base.format("default: :rc:`legend.loc`", '') +
302+
_legend_kw_doc_base)
303+
_docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st)
304+
305+
_outside_doc = """
306+
If a figure is using the constrained layout manager, the string codes
307+
of the *loc* keyword argument can get better layout behaviour using the
308+
prefix 'outside'. There is ambiguity at the corners, so 'outside
309+
upper right' will make space for the legend above the rest of the
310+
axes in the layout, and 'outside right upper' will make space on the
311+
right side of the layout. In addition to the values of *loc*
312+
listed above, we have 'outside right upper', 'outside right lower',
313+
'outside left upper', and 'outside left lower'. See
314+
:doc:`/tutorials/intermediate/legend_guide` for more details.
315+
"""
316+
317+
_legend_kw_figure_st = (_loc_doc_base.format("default: 'upper right'",
318+
_outside_doc) +
319+
_legend_kw_doc_base)
320+
_docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st)
321+
322+
_legend_kw_both_st = (
323+
_loc_doc_base.format("default: 'best' for axes, 'upper right' for figures",
324+
_outside_doc) +
325+
_legend_kw_doc_base)
326+
_docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st)
299327

300328

301329
class Legend(Artist):
@@ -482,13 +510,39 @@ def val_or_rc(val, rc_name):
482510
)
483511
self.parent = parent
484512

513+
loc0 = loc
485514
self._loc_used_default = loc is None
486515
if loc is None:
487516
loc = mpl.rcParams["legend.loc"]
488517
if not self.isaxes and loc in [0, 'best']:
489518
loc = 'upper right'
519+
520+
# handle outside legends:
521+
self._outside = None
490522
if isinstance(loc, str):
523+
if loc.split()[0] == 'outside':
524+
# strip outside:
525+
loc = loc.split('outside ')[1]
526+
# strip "center" at the beginning
527+
self._outside = loc.replace('center ', '')
528+
# strip first
529+
self._outside = self._outside.split()[0]
530+
locs = loc.split()
531+
if len(locs) > 1 and locs[0] in ('right', 'left'):
532+
# locs doesn't accept "left upper", etc, so swap
533+
if locs[0] != 'center':
534+
locs = locs[::-1]
535+
loc = locs[0] + ' ' + locs[1]
536+
# check that loc is in acceptable strings
491537
loc = _api.check_getitem(self.codes, loc=loc)
538+
539+
if self.isaxes and self._outside:
540+
# warn if user has done "outside upper right", but don't
541+
# error because at this point we just drop the "outside".
542+
raise ValueError(
543+
f"'outside' option for loc='{loc0}' keyword argument only "
544+
"works for figure legends")
545+
492546
if not self.isaxes and loc == 0:
493547
raise ValueError(
494548
"Automatic legend placement (loc='best') not implemented for "

lib/matplotlib/tests/test_legend.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import warnings
55

66
import numpy as np
7+
from numpy.testing import assert_allclose
78
import pytest
89

910
from matplotlib.testing.decorators import check_figures_equal, image_comparison
@@ -486,6 +487,47 @@ def test_warn_args_kwargs(self):
486487
"be discarded.")
487488

488489

490+
def test_figure_legend_outside():
491+
todos = ['upper ' + pos for pos in ['left', 'center', 'right']]
492+
todos += ['lower ' + pos for pos in ['left', 'center', 'right']]
493+
todos += ['left ' + pos for pos in ['lower', 'center', 'upper']]
494+
todos += ['right ' + pos for pos in ['lower', 'center', 'upper']]
495+
496+
upperext = [20.347556, 27.722556, 790.583, 545.499]
497+
lowerext = [20.347556, 71.056556, 790.583, 588.833]
498+
leftext = [151.681556, 27.722556, 790.583, 588.833]
499+
rightext = [20.347556, 27.722556, 659.249, 588.833]
500+
axbb = [upperext, upperext, upperext,
501+
lowerext, lowerext, lowerext,
502+
leftext, leftext, leftext,
503+
rightext, rightext, rightext]
504+
505+
legbb = [[10., 555., 133., 590.], # upper left
506+
[338.5, 555., 461.5, 590.], # upper center
507+
[667, 555., 790., 590.], # upper right
508+
[10., 10., 133., 45.], # lower left
509+
[338.5, 10., 461.5, 45.], # lower center
510+
[667., 10., 790., 45.], # lower right
511+
[10., 10., 133., 45.], # left lower
512+
[10., 282.5, 133., 317.5], # left center
513+
[10., 555., 133., 590.], # left upper
514+
[667, 10., 790., 45.], # right lower
515+
[667., 282.5, 790., 317.5], # right center
516+
[667., 555., 790., 590.]] # right upper
517+
518+
for nn, todo in enumerate(todos):
519+
print(todo)
520+
fig, axs = plt.subplots(constrained_layout=True, dpi=100)
521+
axs.plot(range(10), label='Boo1')
522+
leg = fig.legend(loc='outside ' + todo)
523+
fig.draw_without_rendering()
524+
525+
assert_allclose(axs.get_window_extent().extents,
526+
axbb[nn])
527+
assert_allclose(leg.get_window_extent().extents,
528+
legbb[nn])
529+
530+
489531
@image_comparison(['legend_stackplot.png'])
490532
def test_legend_stackplot():
491533
"""Test legend for PolyCollection using stackplot."""

tutorials/intermediate/legend_guide.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,54 @@
135135
ax_dict['bottom'].legend(bbox_to_anchor=(1.05, 1),
136136
loc='upper left', borderaxespad=0.)
137137

138-
plt.show()
138+
##############################################################################
139+
# Figure legends
140+
# --------------
141+
#
142+
# Sometimes it makes more sense to place a legend relative to the (sub)figure
143+
# rather than individual Axes. By using ``constrained_layout`` and
144+
# specifying "outside" at the beginning of the *loc* keyword argument,
145+
# the legend is drawn outside the Axes on the (sub)figure.
146+
147+
fig, axs = plt.subplot_mosaic([['left', 'right']], layout='constrained')
148+
149+
axs['left'].plot([1, 2, 3], label="test1")
150+
axs['left'].plot([3, 2, 1], label="test2")
151+
152+
axs['right'].plot([1, 2, 3], 'C2', label="test3")
153+
axs['right'].plot([3, 2, 1], 'C3', label="test4")
154+
# Place a legend to the right of this smaller subplot.
155+
fig.legend(loc='outside upper right')
156+
157+
##############################################################################
158+
# This accepts a slightly different grammar than the normal *loc* keyword,
159+
# where "outside right upper" is different from "outside upper right".
160+
#
161+
ucl = ['upper', 'center', 'lower']
162+
lcr = ['left', 'center', 'right']
163+
fig, ax = plt.subplots(figsize=(6, 4), layout='constrained', facecolor='0.7')
164+
165+
ax.plot([1, 2], [1, 2], label='TEST')
166+
# Place a legend to the right of this smaller subplot.
167+
for loc in [
168+
'outside upper left',
169+
'outside upper center',
170+
'outside upper right',
171+
'outside lower left',
172+
'outside lower center',
173+
'outside lower right']:
174+
fig.legend(loc=loc, title=loc)
175+
176+
fig, ax = plt.subplots(figsize=(6, 4), layout='constrained', facecolor='0.7')
177+
ax.plot([1, 2], [1, 2], label='test')
178+
179+
for loc in [
180+
'outside left upper',
181+
'outside right upper',
182+
'outside left lower',
183+
'outside right lower']:
184+
fig.legend(loc=loc, title=loc)
185+
139186

140187
###############################################################################
141188
# Multiple legends on the same Axes

0 commit comments

Comments
 (0)
0