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 1ff75d5

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

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
@@ -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.edit_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

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
@@ -1079,6 +1079,7 @@ def legend(self, *args, **kwargs):
10791079
# explicitly set the bbox transform if the user hasn't.
10801080
l = mlegend.Legend(self, handles, labels, *extra_args,
10811081
bbox_transform=transform, **kwargs)
1082+
l._outside = kwargs.pop('outside', False)
10821083
self.legends.append(l)
10831084
l._remove_method = self.legends.remove
10841085
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
@@ -274,6 +274,13 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
274274
The custom dictionary mapping instances or types to a legend
275275
handler. This *handler_map* updates the default handler map
276276
found at `matplotlib.legend.Legend.get_legend_handler_map`.
277+
278+
outside : bool or string
279+
For `.Figure.legend` when used with `.Figure.set_constrained_layout`.
280+
If True, place the legend outside all the axes in the figure.
281+
loc='upper right/left' wil usually put beside the figure, but if
282+
outside='upper', then place above the axes; if loc='lower right/left'
283+
and outside='lower' then place below the axes.
277284
""")
278285

279286

@@ -338,6 +345,7 @@ def __init__(
338345
frameon=None, # draw frame
339346
handler_map=None,
340347
title_fontproperties=None, # properties for the legend title
348+
outside=False,
341349
):
342350
"""
343351
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
@@ -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

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.