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

Skip to content

ENH: add figure.legend; outside kwarg for better layout outside subplots #13072

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions examples/text_labels_and_annotations/figlegendoutside_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""
==========================
Figure legend outside axes
==========================

Instead of plotting a legend on each axis, a legend for all the artists on all
the sub-axes of a figure can be plotted instead. If constrained layout is
used (:doc:`/tutorials/intermediate/constrainedlayout_guide`) then room
can be made automatically for the legend by using `~.Figure.legend` with the
``outside=True`` kwarg.

"""

import numpy as np
import matplotlib.pyplot as plt

fig, axs = plt.subplots(1, 2, sharey=True, constrained_layout=True)

x = np.arange(0.0, 2.0, 0.02)
y1 = np.sin(2 * np.pi * x)
y2 = np.exp(-x)
axs[0].plot(x, y1, 'rs-', label='Line1')
h2, = axs[0].plot(x, y2, 'go', label='Line2')

axs[0].set_ylabel('DATA')

y3 = np.sin(4 * np.pi * x)
y4 = np.exp(-2 * x)
axs[1].plot(x, y3, 'yd-', label='Line3')
h4, = axs[1].plot(x, y4, 'k^', label='Line4')

fig.legend(loc='upper center', outside=True, ncol=2)
fig.legend(ax=[axs[1]], outside=True, loc='lower right')
fig.legend(handles=[h2, h4], labels=['curve2', 'curve4'],
outside=True, loc='center left', borderaxespad=6)
plt.show()

###############################################################################
# The usual codes for the *loc* kwarg are allowed, however, the corner
# codes have an ambiguity as to whether the legend is stacked
# horizontally (the default) or vertically. To specify the vertical stacking
# the *outside* kwarg can be specified with ``"vertical"`` instead of just
# the boolean *True*:

fig, axs = plt.subplots(1, 2, sharey=True, constrained_layout=True)
axs[0].plot(x, y1, 'rs-', label='Line1')
h2, = axs[0].plot(x, y2, 'go', label='Line2')

axs[0].set_ylabel('DATA')
axs[1].plot(x, y3, 'yd-', label='Line3')
h4, = axs[1].plot(x, y4, 'k^', label='Line4')

fig.legend(loc='upper right', outside='vertical', ncol=2)
plt.show()

###############################################################################
# Significantly more complicated layouts are possible using the gridspec
# organization of subplots:

fig = plt.figure(constrained_layout=True)
gs0 = fig.add_gridspec(1, 2)

gs = gs0[0].subgridspec(1, 1)
for i in range(1):
ax = fig.add_subplot(gs[i, 0])
ax.plot(range(10), label=f'Boo{i}')
lg = fig.legend(ax=[ax], loc='lower right', outside=True, borderaxespad=4)

gs2 = gs0[1].subgridspec(3, 1)
axx = []
for i in range(3):
ax = fig.add_subplot(gs2[i, 0])
ax.plot(range(10), label=f'Who{i}', color=f'C{i+1}')
if i < 2:
ax.set_xticklabels('')
axx += [ax]
lg2 = fig.legend(ax=axx[:-1], loc='upper right', outside=True, borderaxespad=4)
plt.show()
41 changes: 41 additions & 0 deletions lib/matplotlib/_constrained_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

import numpy as np

import matplotlib.gridspec as gridspec
import matplotlib.cbook as cbook
import matplotlib._layoutbox as layoutbox

Expand Down Expand Up @@ -182,6 +183,10 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
# reserve at top of figure include an h_pad above and below
suptitle._layoutbox.edit_height(height + h_pad * 2)

# do layout for any legend_offsets
for gs in gss:
_do_offset_legend_layout(gs._layoutbox)

# OK, the above lines up ax._poslayoutbox with ax._layoutbox
# now we need to
# 1) arrange the subplotspecs. We do it at this level because
Expand Down Expand Up @@ -227,11 +232,46 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad,
else:
if suptitle is not None and suptitle._layoutbox is not None:
suptitle._layoutbox.edit_height(0)
# now set the position of any offset legends...
for gs in gss:
_do_offset_legend_position(gs._layoutbox)
else:
cbook._warn_external('constrained_layout not applied. At least '
'one axes collapsed to zero width or height.')


def _do_offset_legend_layout(gslayoutbox):
"""
Helper to get the right width and height for an offset legend.
"""
for child in gslayoutbox.children:
if child._is_subplotspec_layoutbox():
# check for nested gridspecs...
for child2 in child.children:
# check for gridspec children...
if child2._is_gridspec_layoutbox():
_do_offset_legend_layout(child2)
elif isinstance(child.artist, gridspec.LegendLayout):
child.artist._update_width_height()


def _do_offset_legend_position(gslayoutbox):
"""
Helper to properly set the offset box for the offset legends...
"""
for child in gslayoutbox.children:
if child._is_subplotspec_layoutbox():
# check for nested gridspecs...
for child2 in child.children:
# check for gridspec children...
if child2._is_gridspec_layoutbox():
_do_offset_legend_position(child2)
elif isinstance(child.artist, gridspec.LegendLayout):
# update position...
child.artist.set_bbox_to_anchor(gslayoutbox.get_rect())
child.artist._update_width_height()


def _make_ghost_gridspec_slots(fig, gs):
"""
Check for unoccupied gridspec slots and make ghost axes for these
Expand Down Expand Up @@ -477,6 +517,7 @@ def _arrange_subplotspecs(gs, hspace=0, wspace=0):
if child2._is_gridspec_layoutbox():
_arrange_subplotspecs(child2, hspace=hspace, wspace=wspace)
sschildren += [child]

# now arrange the subplots...
for child0 in sschildren:
ss0 = child0.artist
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/_layoutbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ def layout_from_subplotspec(self, subspec,
self.width == parent.width * width,
self.height == parent.height * height]
for c in cs:
self.solver.addConstraint(c | 'required')
self.solver.addConstraint((c | 'strong'))

return lb

Expand Down
1 change: 0 additions & 1 deletion lib/matplotlib/axes/_subplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ def __init__(self, fig, *args, **kwargs):
raise ValueError(f'Illegal argument(s) to subplot: {args}')

self.update_params()

# _axes_class is set in the subplot_class_factory
self._axes_class.__init__(self, fig, self.figbox, **kwargs)
# add a layout box to this, for both the full axis, and the poss
Expand Down
63 changes: 42 additions & 21 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

from matplotlib.axes import Axes, SubplotBase, subplot_class_factory
from matplotlib.blocking_input import BlockingMouseInput, BlockingKeyMouseInput
from matplotlib.gridspec import GridSpec
from matplotlib.gridspec import GridSpec, GridSpecBase
import matplotlib.legend as mlegend
from matplotlib.patches import Rectangle
from matplotlib.projections import (get_projection_names,
Expand Down Expand Up @@ -663,6 +663,10 @@ def get_children(self):
*self.images,
*self.legends]

def get_gridspecs(self):
"""Get a list of gridspecs associated with the figure."""
return self._gridspecs

def contains(self, mouseevent):
"""
Test whether the mouse event occurred on the figure.
Expand Down Expand Up @@ -1556,11 +1560,7 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False,
subplot_kw = subplot_kw.copy()
gridspec_kw = gridspec_kw.copy()

if self.get_constrained_layout():
gs = GridSpec(nrows, ncols, figure=self, **gridspec_kw)
else:
# this should turn constrained_layout off if we don't want it
gs = GridSpec(nrows, ncols, figure=None, **gridspec_kw)
gs = GridSpec(nrows, ncols, figure=self, **gridspec_kw)
self._gridspecs.append(gs)

# Create array to hold all axes.
Expand Down Expand Up @@ -1755,7 +1755,7 @@ def get_axes(self):
# docstring of pyplot.figlegend.

@docstring.dedent_interpd
def legend(self, *args, **kwargs):
def legend(self, *args, outside=False, ax=None, **kwargs):
"""
Place a legend on the figure.

Expand All @@ -1779,6 +1779,19 @@ def legend(self, *args, **kwargs):

Parameters
----------

outside: bool or string
If ``constrained_layout=True``, then try and place legend outside
axes listed in *axs*, or highest-level gridspec if axs is empty.
Note, "center" and "best" options to *loc* do not work with
``outside=True``. The corner values of *loc* (i.e. "upper right")
will default to a horizontal layout of the legend, but this can
be changed by specifying a string
``outside="vertical", loc="upper right"``.

ax : sequence of `~.axes.Axes`
axes to gather handles from (if *handles* is empty).

handles : list of `.Artist`, optional
A list of Artists (lines, patches) to be added to the legend.
Use this together with *labels*, if you need full control on what
Expand Down Expand Up @@ -1807,24 +1820,32 @@ def legend(self, *args, **kwargs):
Not all kinds of artist are supported by the legend command. See
:doc:`/tutorials/intermediate/legend_guide` for details.
"""
if ax is None:
ax = self.axes

handles, labels, extra_args, kwargs = mlegend._parse_legend_args(
self.axes,
ax,
*args,
**kwargs)
# check for third arg
if len(extra_args):
# cbook.warn_deprecated(
# "2.1",
# message="Figure.legend will accept no more than two "
# "positional arguments in the future. Use "
# "'fig.legend(handles, labels, loc=location)' "
# "instead.")
# kwargs['loc'] = extra_args[0]
# extra_args = extra_args[1:]
pass
l = mlegend.Legend(self, handles, labels, *extra_args, **kwargs)
self.legends.append(l)
if outside and not self.get_constrained_layout():
cbook._warn_external('legend outside=True method needs '
'constrained_layout=True. Setting False')
outside = False
if outside and kwargs.get('bbox_to_anchor') is not None:
cbook._warn_external('legend outside=True ignores bbox_to_anchor '
'kwarg')

if not outside:
l = mlegend.Legend(self, handles, labels, *extra_args, **kwargs)
self.legends.append(l)
else:
loc = kwargs.pop('loc')
if isinstance(ax, GridSpecBase):
gs = ax
else:
gs = ax[0].get_gridspec()
l = gs.legend_outside(loc=loc, align=outside, handles=handles,
labels=labels, **kwargs)
l._remove_method = self.legends.remove
self.stale = True
return l
Expand Down
Loading