Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8de355d

Browse files
committed
ENH: add outside kwarg to figure legend
1 parent 00ec4ec commit 8de355d

File tree

7 files changed

+126
-1
lines changed

7 files changed

+126
-1
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
@@ -418,6 +418,27 @@ 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+
margin = 'right'
431+
if ((leg._loc in (3, 4) and leg._outside == 'lower') or
432+
(leg._loc == 8)):
433+
layoutgrids[gs].edit_margin_min('bottom', h)
434+
elif ((leg._loc in (1, 2) and leg._outside == 'upper') or
435+
(leg._loc == 9)):
436+
layoutgrids[gs].edit_margin_min('top', h)
437+
elif leg._loc in (1, 4, 5, 7):
438+
layoutgrids[gs].edit_margin_min('right', w)
439+
elif leg._loc in (2, 3, 6):
440+
layoutgrids[gs].edit_margin_min('left', w)
441+
421442

422443
def make_margin_suptitles(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0):
423444
# 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
@@ -1116,6 +1116,7 @@ def legend(self, *args, **kwargs):
11161116
# explicitly set the bbox transform if the user hasn't.
11171117
l = mlegend.Legend(self, handles, labels, *extra_args,
11181118
bbox_transform=transform, **kwargs)
1119+
l._outside = kwargs.pop('outside', False)
11191120
self.legends.append(l)
11201121
l._remove_method = self.legends.remove
11211122
self.stale = True

lib/matplotlib/legend.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,13 @@ def _update_bbox_to_anchor(self, loc_in_canvas):
289289
290290
draggable : bool, default: False
291291
Whether the legend can be dragged with the mouse.
292+
293+
outside : bool or string
294+
For `.Figure.legend` when used with `.Figure.set_constrained_layout`.
295+
If True, place the legend outside all the axes in the figure.
296+
loc='upper right/left' wil usually put beside the figure, but if
297+
outside='upper', then place above the axes; if loc='lower right/left'
298+
and outside='lower' then place below the axes.
292299
""")
293300

294301

@@ -346,7 +353,8 @@ def __init__(
346353
alignment="center", # control the alignment within the legend box
347354
*,
348355
ncol=1, # synonym for ncols (backward compatibility)
349-
draggable=False # whether the legend can be dragged with the mouse
356+
draggable=False, # whether the legend can be dragged with the mouse
357+
outside=False,
350358
):
351359
"""
352360
Parameters

lib/matplotlib/tests/test_legend.py

Lines changed: 46 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
@@ -398,6 +399,51 @@ def test_warn_args_kwargs(self):
398399
"be discarded.")
399400

400401

402+
def test_figure_legend_outside():
403+
outside = [True]*9 + ['upper', 'upper', 'lower', 'lower']
404+
print(outside)
405+
todos = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4]
406+
axbb = [[20.347556, 27.722556, 659.249, 588.833], # upper right
407+
[151.681556, 27.722556, 790.583, 588.833], # upper left
408+
[151.681556, 27.722556, 790.583, 588.833], # lower left
409+
[20.347556, 27.722556, 659.249, 588.833], # lower right
410+
[20.347556, 27.722556, 659.249, 588.833], # right
411+
[151.681556, 27.722556, 790.583, 588.833], # center left
412+
[20.347556, 27.722556, 659.249, 588.833], # center right
413+
[20.347556, 71.056556, 790.583, 588.833], # lower center
414+
[20.347556, 27.722556, 790.583, 545.499], # upper center
415+
[20.347556, 27.722556, 790.583, 545.499], # up-right,'upper'
416+
[20.347556, 27.722556, 790.583, 545.499], # up-left,'upper'
417+
[20.347556, 71.056556, 790.583, 588.833], # low-left,'lower'
418+
[20.347556, 71.056556, 790.583, 588.833], # low-right,'lower'
419+
]
420+
legbb = [[667., 555., 790., 590.], # upper right
421+
[10., 555., 133., 590.], # upper left
422+
[10., 10., 133., 45.], # lower left
423+
[667, 10., 790., 45.], # lower right
424+
[667., 282.5, 790., 317.5],
425+
[10., 282.5, 133., 317.5],
426+
[667., 282.5, 790., 317.5],
427+
[338.5, 10., 461.5, 45.],
428+
[338.5, 555., 461.5, 590.],
429+
[667., 555., 790., 590.], # upper right
430+
[10., 555., 133., 590.], # upper left
431+
[10., 10., 133., 45.], # lower left
432+
[667, 10., 790., 45.], # lower right
433+
]
434+
for nn, todo in enumerate(todos):
435+
print(todo)
436+
fig, axs = plt.subplots(constrained_layout=True, dpi=100)
437+
axs.plot(range(10), label='Boo1')
438+
leg = fig.legend(loc=todo, outside=outside[nn])
439+
renderer = fig.canvas.get_renderer()
440+
fig.canvas.draw()
441+
assert_allclose(axs.get_window_extent(renderer=renderer).extents,
442+
axbb[nn])
443+
assert_allclose(leg.get_window_extent(renderer=renderer).extents,
444+
legbb[nn])
445+
446+
401447
@image_comparison(['legend_stackplot.png'])
402448
def test_legend_stackplot():
403449
"""Test legend for PolyCollection using stackplot."""

tutorials/intermediate/legend_guide.py

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

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")
150+
151+
axs['right'].plot([1, 2, 3], 'C2', label="test3")
152+
axs['right'].plot([3, 2, 1], 'C3', label="test4")
153+
# Place a legend to the right of this smaller subplot.
154+
fig.legend(loc='upper right', outside=True)
155+
138156
plt.show()
139157

140158
###############################################################################

0 commit comments

Comments
 (0)