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

Skip to content

Commit 9c577ae

Browse files
committed
ENH: add outside kwarg to figure legend
1 parent c2946de commit 9c577ae

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
@@ -273,6 +273,27 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0,
273273
# pass the new margins down to the layout grid for the solution...
274274
gs._layoutgrid.edit_outer_margin_mins(margin, ss)
275275

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

277298
def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0):
278299
# 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
@@ -1085,6 +1085,7 @@ def legend(self, *args, **kwargs):
10851085
# explicitly set the bbox transform if the user hasn't.
10861086
l = mlegend.Legend(self, handles, labels, *extra_args,
10871087
bbox_transform=transform, **kwargs)
1088+
l._outside = kwargs.pop('outside', False)
10881089
self.legends.append(l)
10891090
l._remove_method = self.legends.remove
10901091
self.stale = True

lib/matplotlib/legend.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,13 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
263263
The custom dictionary mapping instances or types to a legend
264264
handler. This *handler_map* updates the default handler map
265265
found at `matplotlib.legend.Legend.get_legend_handler_map`.
266+
267+
outside : bool or string
268+
For `.Figure.legend` when used with `.Figure.set_constrained_layout`.
269+
If True, place the legend outside all the axes in the figure.
270+
loc='upper right/left' wil usually put beside the figure, but if
271+
outside='upper', then place above the axes; if loc='lower right/left'
272+
and outside='lower' then place below the axes.
266273
""")
267274

268275

@@ -332,6 +339,7 @@ def __init__(self, parent, handles, labels,
332339
bbox_transform=None, # transform for the bbox
333340
frameon=None, # draw frame
334341
handler_map=None,
342+
outside=False,
335343
):
336344
"""
337345
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
@@ -380,6 +381,51 @@ def test_warn_args_kwargs(self):
380381
"be discarded.")
381382

382383

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