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

Skip to content

Commit ffb8578

Browse files
committed
ENH: add outside kwarg to figure legend
1 parent a9c5224 commit ffb8578

File tree

7 files changed

+139
-9
lines changed

7 files changed

+139
-9
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
:orphan:
2+
3+
Figure legends now have *outside* keyword argument
4+
--------------------------------------------------
5+
If a legend is made on a figure (or subfigure), and constrained_layout is being used,
6+
then setting the *outside* kwarg to *True* on `.Figure.legend` will move axes to
7+
make room for the legend. See :doc:`/tutorials/intermediate/legend_guide` for an
8+
example.

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=True``, the legend will
35+
# not overlap.
36+
37+
fig, axs = plt.subplots(1, 2, constrained_layout=True)
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'), 'upper left')
51+
fig.legend((l3, l4), ('Line 3', 'Line 4'), 'upper right', outside=True)
52+
53+
plt.show()

lib/matplotlib/_constrained_layout.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,27 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0,
271271
# pass the new margins down to the layout grid for the solution...
272272
gs._layoutgrid.edit_outer_margin_mins(margin, ss)
273273

274+
# make margins for figure-level legends:
275+
for leg in fig.legends:
276+
inv_trans_fig = None
277+
if leg._outside and leg._bbox_to_anchor is None:
278+
if inv_trans_fig is None:
279+
inv_trans_fig = fig.transFigure.inverted().transform_bbox
280+
bbox = inv_trans_fig(leg.get_tightbbox(renderer))
281+
w = bbox.width + 2 * w_pad
282+
h = bbox.height + 2 * h_pad
283+
margin = 'right'
284+
if ((leg._loc in (3, 4) and leg._outside == 'lower') or
285+
(leg._loc == 8)):
286+
fig._layoutgrid.e 57AE dit_margin_min('bottom', h)
287+
elif ((leg._loc in (1, 2) and leg._outside == 'upper') or
288+
(leg._loc == 9)):
289+
fig._layoutgrid.edit_margin_min('top', h)
290+
elif leg._loc in (1, 4, 5, 7):
291+
fig._layoutgrid.edit_margin_min('right', w)
292+
elif leg._loc in (2, 3, 6):
293+
fig._layoutgrid.edit_margin_min('left', w)
294+
274295

275296
def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0):
276297
# Figure out how large the suptitle is and make the

lib/matplotlib/figure.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,7 @@ def legend(self, *args, **kwargs):
10631063
# explicitly set the bbox transform if the user hasn't.
10641064
l = mlegend.Legend(self, handles, labels, *extra_args,
10651065
bbox_transform=transform, **kwargs)
1066+
l._outside = kwargs.pop('outside', False)
10661067
self.legends.append(l)
10671068
l._remove_method = self.legends.remove
10681069
self.stale = True

lib/matplotlib/legend.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,13 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
271271
The custom dictionary mapping instances or types to a legend
272272
handler. This *handler_map* updates the default handler map
273273
found at `matplotlib.legend.Legend.get_legend_handler_map`.
274+
275+
outside : bool or string
276+
For `.Figure.legend` when used with `.Figure.set_constrained_layout`.
277+
If True, place the legend outside all the axes in the figure.
278+
loc='upper right/left' wil usually put beside the figure, but if
279+
outside='upper', then place above the axes; if loc='lower right/left'
280+
and outside='lower' then place below the axes.
274281
""")
275282

276283

@@ -341,6 +348,7 @@ def __init__(self, parent, handles, labels,
341348
frameon=None, # draw frame
342349
handler_map=None,
343350
title_fontproperties=None, # properties for the legend title
351+
outside=False,
344352
):
345353
"""
346354
Parameters

lib/matplotlib/tests/test_legend.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from unittest import mock
44

55
import numpy as np
6+
from numpy.testing import assert_allclose
67
import pytest
78

89
from matplotlib.testing.decorators import image_comparison
@@ -381,6 +382,51 @@ def test_warn_args_kwargs(self):
381382
"be discarded.")
382383

383384

385+
def test_figure_legend_outside():
386+
outside = [True]*9 + ['upper', 'upper', 'lower', 'lower']
387+
print(outside)
388+
todos = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4]
389+
axbb = [[20.347556, 27.722556, 659.249, 588.833], # upper right
390+
[151.681556, 27.722556, 790.583, 588.833], # upper left
391+
[151.681556, 27.722556, 790.583, 588.833], # lower left
392+
[20.347556, 27.722556, 659.249, 588.833], # lower right
393+
[20.347556, 27.722556, 659.249, 588.833], # right
394+
[151.681556, 27.722556, 790.583, 588.833], # center left
395+
[20.347556, 27.722556, 659.249, 588.833], # center right
396+
[20.347556, 71.056556, 790.583, 588.833], # lower center
397+
[20.347556, 27.722556, 790.583, 545.499], # upper center
398+
[20.347556, 27.722556, 790.583, 545.499], # up-right,'upper'
399+
[20.347556, 27.722556, 790.583, 545.499], # up-left,'upper'
400+
[20.347556, 71.056556, 790.583, 588.833], # low-left,'lower'
401+
[20.347556, 71.056556, 790.583, 588.833], # low-right,'lower'
402+
]
403+
legbb = [[667., 555., 790., 590.], # upper right
404+
[10., 555., 133., 590.], # upper left
405+
[10., 10., 133., 45.], # lower left
406+
[667, 10., 790., 45.], # lower right
407+
[667., 282.5, 790., 317.5],
408+
[10., 282.5, 133., 317.5],
409+
[667., 282.5, 790., 317.5],
410+
[338.5, 10., 461.5, 45.],
411+
[338.5, 555., 461.5, 590.],
412+
[667., 555., 790., 590.], # upper right
413+
[10., 555., 133., 590.], # upper left
414+
[10., 10., 133., 45.], # lower left
415+
[667, 10., 790., 45.], # lower right
416+
]
417+
for nn, todo in enumerate(todos):
418+
print(todo)
419+
fig, axs = plt.subplots(constrained_layout=True, dpi=100)
420+
axs.plot(range(10), label='Boo1')
421+
leg = fig.legend(loc=todo, outside=outside[nn])
422+
renderer = fig.canvas.get_renderer()
423+
fig.canvas.draw()
424+
assert_allclose(axs.get_window_extent(renderer=renderer).extents,
425+
axbb[nn])
426+
assert_allclose(leg.get_window_extent(renderer=renderer).extents,
427+
legbb[nn])
428+
429+
384430
@image_comparison(['legend_stackplot.png'])
385431
def test_legend_stackplot():
386432
"""Test legend for PolyCollection using stackplot."""

tutorials/intermediate/legend_guide.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -115,20 +115,43 @@
115115
#
116116
# More examples of custom legend placement:
117117

118-
plt.subplot(211)
119-
plt.plot([1, 2, 3], label="test1")
120-
plt.plot([3, 2, 1], label="test2")
118+
fig, axs = plt.subplot_mosaic([['top', 'top'], ['left', 'right']])
119+
120+
axs['right'].remove()
121+
122+
axs['top'].plot([1, 2, 3], label="test1")
123+
axs['top'].plot([3, 2, 1], label="test2")
121124

122125
# Place a legend above this subplot, expanding itself to
123126
# fully use the given bounding box.
124-
plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc='lower left',
125-
ncol=2, mode="expand", borderaxespad=0.)
127+
axs['top'].legend(bbox_to_anchor=(0., 1.02, 1., .102), loc='lower left',
128+
ncol=2, mode="expand", borderaxespad=0.)
129+
130+
axs['left'].plot([1, 2, 3], label="test1")
131+
axs['left'].plot([3, 2, 1], label="test2")
132+
# Place a legend to the right of this smaller subplot.
133+
axs['left'].legend(bbox_to_anchor=(1.05, 1), loc='upper left',
134+
borderaxespad=0.)
135+
136+
plt.show()
137+
138+
##############################################################################
139+
# Figure legends
140+
# --------------
141+
#
142+
# Sometimes it makes more sense to place the axes relative to the (sub)figure
143+
# rather than individual axes. By using ``constrained_layout`` and
144+
# ``outside=True`` the legend is drawn outside the axes on the (sub)figure.
145+
146+
fig, axs = plt.subplot_mosaic([['left', 'right']], constrained_layout=True)
147+
148+
axs['left'].plot([1, 2, 3], label="test1")
149+
axs['left'].plot([3, 2, 1], label="test2")
126150

127-
plt.subplot(223)
128-
plt.plot([1, 2, 3], label="test1")
129-
plt.plot([3, 2, 1], label="test2")
151+
axs['right'].plot([1, 2, 3], 'C2', label="test3")
152+
axs['right'].plot([3, 2, 1], 'C3', label="test4")
130153
# Place a legend to the right of this smaller subplot.
131-
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
154+
fig.legend(loc='upper right', outside=True)
132155

133156
plt.show()
134157

0 commit comments

Comments
 (0)
0