Skip to content

Allow figure.legend to be called without arguments #7811

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

Merged
merged 3 commits into from
Jan 20, 2017
Merged
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
6 changes: 6 additions & 0 deletions doc/users/whats_new/figure_legend_no_args.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
figure.legend() can be called without arguments
-----------------------------------------------

Calling :func:`figure.legend` can now be done with no arguments. In this case a
legend will be created that contains all the artists on all the axes contained
within the figure.
19 changes: 14 additions & 5 deletions examples/pylab_examples/figlegend_demo.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
"""
==================
Figure legend demo
==================

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.
"""

import numpy as np
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you are at it, do you mind adding a docstring to give this example a title and explain what it does? See http://matplotlib.org/devdocs/devel/MEP/MEP12.html for examples.

import matplotlib.pyplot as plt

fig = plt.figure()
ax1 = fig.add_axes([0.1, 0.1, 0.4, 0.7])
ax2 = fig.add_axes([0.55, 0.1, 0.4, 0.7])
fig, axs = plt.subplots(1, 2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


x = np.arange(0.0, 2.0, 0.02)
y1 = np.sin(2*np.pi*x)
y2 = np.exp(-x)
l1, l2 = ax1.plot(x, y1, 'rs-', x, y2, 'go')
l1, l2 = axs[0].plot(x, y1, 'rs-', x, y2, 'go')

y3 = np.sin(4*np.pi*x)
y4 = np.exp(-2*x)
l3, l4 = ax2.plot(x, y3, 'yd-', x, y4, 'k^')
l3, l4 = axs[1].plot(x, y3, 'yd-', x, y4, 'k^')

fig.legend((l1, l2), ('Line 1', 'Line 2'), 'upper left')
fig.legend((l3, l4), ('Line 3', 'Line 4'), 'upper right')

plt.tight_layout()
plt.show()
234 changes: 162 additions & 72 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,127 +1262,217 @@ def draw_artist(self, a):
def get_axes(self):
return self.axes

def legend(self, handles, labels, *args, **kwargs):
def legend(self, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you are at it, do you mind migrating this to a numpydoc compatible docstring?
(I can also do it if you don't mind me pushing to your branch).

"""
Place a legend in the figure. Labels are a sequence of
strings, handles is a sequence of
:class:`~matplotlib.lines.Line2D` or
:class:`~matplotlib.patches.Patch` instances, and loc can be a
string or an integer specifying the legend location
Place a legend on the figure.

USAGE::
To make a legend from existing artists on every axes::

legend()

To make a legend for a list of lines and labels::

legend( (line1, line2, line3),
('label1', 'label2', 'label3'),
'upper right')

The *loc* location codes are::

'best' : 0, (currently not supported for figure legends)
'upper right' : 1,
'upper left' : 2,
'lower left' : 3,
'lower right' : 4,
'right' : 5,
'center left' : 6,
'center right' : 7,
'lower center' : 8,
'upper center' : 9,
'center' : 10,

*loc* can also be an (x,y) tuple in figure coords, which
specifies the lower left of the legend box. figure coords are
(0,0) is the left, bottom of the figure and 1,1 is the right,
top.

Keyword arguments:

prop: [ *None* | FontProperties | dict ]
A :class:`matplotlib.font_manager.FontProperties`
instance. If *prop* is a dictionary, a new instance will be
created with *prop*. If *None*, use rc settings.

numpoints: integer
Parameters
----------
loc : string or integer
The location of the legend. Possible codes are:

=============== =============
Location String Location Code
=============== =============
'upper right' 1
'upper left' 2
'lower left' 3
'lower right' 4
'right' 5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why there's this seemingly extra location.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's the same in axes.legend() too.

'center left' 6
'center right' 7
'lower center' 8
'upper center' 9
'center' 10
=============== =============

*loc* can also be an (x,y) tuple in figure coords, which specifies
the lower left of the legend box. In figure coords (0,0) is the
bottom left of the figure, and (1,1) is the top right.

prop : None or FontProperties or dict
A :class:`matplotlib.font_manager.FontProperties` instance. If
*prop* is a dictionary, a new instance will be created with *prop*.
If *None*, use rc settings.

numpoints : integer
The number of points in the legend line, default is 4

scatterpoints: integer
scatterpoints : integer
The number of points in the legend line, default is 4

scatteryoffsets: list of floats
a list of yoffsets for scatter symbols in legend
scatteryoffsets : list of floats
A list of yoffsets for scatter symbols in legend

markerscale: [ *None* | scalar ]
markerscale : None or scalar
The relative size of legend markers vs. original. If *None*, use rc
settings.

markerfirst: [ *True* | *False* ]
if *True*, legend marker is placed to the left of the legend label
if *False*, legend marker is placed to the right of the legend
label
markerfirst : bool
If *True*, legend marker is placed to the left of the legend label.
If *False*, legend marker is placed to the right of the legend
label.

frameon: [ *None* | bool ]
frameon : None or bool
Control whether the legend should be drawn on a patch (frame).
Default is *None* which will take the value from the
``legend.frameon`` :data:`rcParam<matplotlib.rcParams>`.

fancybox: [ *None* | *False* | *True* ]
if *True*, draw a frame with a round fancybox. If *None*, use rc
fancybox : None or bool
If *True*, draw a frame with a round fancybox. If *None*, use rc
settings.

shadow: [ *None* | *False* | *True* ]
shadow : None or bool
If *True*, draw a shadow behind legend. If *None*, use rc settings.

framealpha: [ *None* | float ]
framealpha : None or float
Control the alpha transparency of the legend's background.
Default is *None* which will take the value from the
``legend.framealpha`` :data:`rcParam<matplotlib.rcParams>`.

facecolor: [ *None* | "inherit" | a color spec ]
facecolor : None or "inherit" or a color spec
Control the legend's background color.
Default is *None* which will take the value from the
``legend.facecolor`` :data:`rcParam<matplotlib.rcParams>`.
If ``"inherit"``, it will take the ``axes.facecolor``
:data:`rcParam<matplotlib.rcParams>`.

edgecolor: [ *None* | "inherit" | a color spec ]
edgecolor : None or "inherit" or a color spec
Control the legend's background patch edge color.
Default is *None* which will take the value from the
``legend.edgecolor`` :data:`rcParam<matplotlib.rcParams>`.
If ``"inherit"``, it will take the ``axes.edgecolor``
:data:`rcParam<matplotlib.rcParams>`.

ncol : integer
number of columns. default is 1
ncol : integer
Number of columns. Default is 1.

mode : [ "expand" | *None* ]
if mode is "expand", the legend will be horizontally expanded
mode : "expand" or None
If mode is "expand", the legend will be horizontally expanded
to fill the axes area (or *bbox_to_anchor*)

title : string
the legend title
title : string
The legend title

borderpad : float or None
The fractional whitespace inside the legend border, measured in
font-size units.
Default is *None* which will take the value from the
``legend.borderpad`` :data:`rcParam<matplotlib.rcParams>`.

labelspacing : float or None
The vertical space between the legend entries, measured in
font-size units.
Default is *None* which will take the value from the
``legend.labelspacing`` :data:`rcParam<matplotlib.rcParams>`.

handlelength : float or None
The length of the legend handles, measured in font-size units.
Default is *None* which will take the value from the
``legend.handlelength`` :data:`rcParam<matplotlib.rcParams>`.

handletextpad : float or None
The padding between the legend handle and text, measured in
font-size units.
Default is *None* which will take the value from the
``legend.handletextpad`` :data:`rcParam<matplotlib.rcParams>`.

Padding and spacing between various elements use following keywords
parameters. The dimensions of these values are given as a fraction
of the fontsize. Values from rcParams will be used if None.
borderaxespad : float or None
The padding between the axes and legend border, measured in
font-size units.
Default is *None* which will take the value from the
``legend.borderaxespad`` :data:`rcParam<matplotlib.rcParams>`.

================ ====================================================
Keyword Description
================ ====================================================
borderpad the fractional whitespace inside the legend border
labelspacing the vertical space between the legend entries
handlelength the length of the legend handles
handletextpad the pad between the legend handle and text
borderaxespad the pad between the axes and legend border
columnspacing the spacing between columns
================ ====================================================
columnspacing : float or None
The spacing between columns, measured in font-size units.
Default is *None* which will take the value from the
``legend.columnspacing`` :data:`rcParam<matplotlib.rcParams>`.

.. Note:: Not all kinds of artist are supported by the legend.
See LINK (FIXME) for details.
Returns
-------
:class:`matplotlib.legend.Legend` instance

**Example:**
Notes
-----
Not all kinds of artist are supported by the legend command.
See :ref:`plotting-guide-legend` for details.

Examples
--------
.. plot:: mpl_examples/pylab_examples/figlegend_demo.py
"""
l = Legend(self, handles, labels, *args, **kwargs)

# If no arguments given, collect up all the artists on the figure
if len(args) == 0:
handles = []
labels = []

def in_handles(h, l):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to leave a comment as to what's happening here (at least it's not obvious to me at all...).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How's that? If you can think of better wording feel free to push to my branch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Typo: "consdier".
I would make it a comment rather than a docstring to save some whitespace, and get rid of the whitespace between the try... catch blocks (right now it's very spread out).

# Method to check if we already have a given handle and label.
# Consider two handles to be the same if they share a label,
# color, facecolor, and edgecolor.

# Loop through each handle and label already collected
for f_h, f_l in zip(handles, labels):
if f_l != l:
continue
if type(f_h) != type(h):
continue
try:
if f_h.get_color() != h.get_color():
continue
except AttributeError:
pass
try:
if f_h.get_facecolor() != h.get_facecolor():
continue
except AttributeError:
pass
try:
if f_h.get_edgecolor() != h.get_edgecolor():
continue
except AttributeError:
pass
return True
return False

for ax in self.axes:
ax_handles, ax_labels = ax.get_legend_handles_labels()
for h, l in zip(ax_handles, ax_labels):
if not in_handles(h, l):
handles.append(h)
labels.append(l)
if len(handles) == 0:
warnings.warn("No labeled objects found. "
"Use label='...' kwarg on individual plots.")
return None

elif len(args) == 2:
# LINES, LABELS
handles, labels = args

elif len(args) == 3:
# LINES, LABELS, LOC
handles, labels, loc = args
kwargs['loc'] = loc

else:
raise TypeError('Invalid number of arguments passed to legend. '
'Please specify either 0 args, 2 args '
'(artist handles, figure labels) or 3 args '
'(artist handles, figure labels, legend location)')

l = Legend(self, handles, labels, **kwargs)
self.legends.append(l)
l._remove_method = lambda h: self.legends.remove(h)
self.stale = True
Expand Down
15 changes: 11 additions & 4 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,7 @@ def figimage(*args, **kwargs):
return gcf().figimage(*args, **kwargs)


def figlegend(handles, labels, loc, **kwargs):
def figlegend(*args, **kwargs):
"""
Place a legend in the figure.

Expand All @@ -757,7 +757,14 @@ def figlegend(handles, labels, loc, **kwargs):

A :class:`matplotlib.legend.Legend` instance is returned.

Example::
Examples
--------

To make a legend from existing artists on every axes::

figlegend()

To make a legend for a list of lines and labels::

figlegend( (line1, line2, line3),
('label1', 'label2', 'label3'),
Expand All @@ -768,7 +775,7 @@ def figlegend(handles, labels, loc, **kwargs):
:func:`~matplotlib.pyplot.legend`

"""
return gcf().legend(handles, labels, loc, **kwargs)
return gcf().legend(*args, **kwargs)


## Figure and Axes hybrid ##
Expand Down Expand Up @@ -976,7 +983,7 @@ def subplot(*args, **kwargs):

.. note::

Creating a subplot will delete any pre-existing subplot that overlaps
Creating a subplot will delete any pre-existing subplot that overlaps
with it beyond sharing a boundary::

import matplotlib.pyplot as plt
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading