Skip to content

Navigation Menu

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 9c577ae

Browse filesBrowse files
committed
ENH: add outside kwarg to figure legend
1 parent c2946de commit 9c577ae
Copy full SHA for 9c577ae

File tree

7 files changed

+139
-9
lines changed
Filter options

7 files changed

+139
-9
lines changed
+8Lines 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

Copy file name to clipboardExpand all lines: examples/text_labels_and_annotations/figlegend_demo.py
+23Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/_constrained_layout.py
+21Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/figure.py
+1Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/legend.py
+8Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_legend.py
+46Lines 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

Copy file name to clipboardExpand all lines: tutorials/intermediate/legend_guide.py
+32-9Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.