1386 lines
55 KiB
Python
1386 lines
55 KiB
Python
"""
|
|
The legend module defines the Legend class, which is responsible for
|
|
drawing legends associated with Axes and/or figures.
|
|
|
|
.. important::
|
|
|
|
It is unlikely that you would ever create a Legend instance manually.
|
|
Most users would normally create a legend via the `~.Axes.legend`
|
|
function. For more details on legends there is also a :ref:`legend guide
|
|
<legend_guide>`.
|
|
|
|
The `Legend` class is a container of legend handles and legend texts.
|
|
|
|
The legend handler map specifies how to create legend handles from artists
|
|
(lines, patches, etc.) in the Axes or figures. Default legend handlers are
|
|
defined in the :mod:`~matplotlib.legend_handler` module. While not all artist
|
|
types are covered by the default legend handlers, custom legend handlers can be
|
|
defined to support arbitrary objects.
|
|
|
|
See the :ref`<legend_guide>` for more
|
|
information.
|
|
"""
|
|
|
|
import itertools
|
|
import logging
|
|
import numbers
|
|
import time
|
|
|
|
import numpy as np
|
|
|
|
import matplotlib as mpl
|
|
from matplotlib import _api, _docstring, cbook, colors, offsetbox
|
|
from matplotlib.artist import Artist, allow_rasterization
|
|
from matplotlib.cbook import silent_list
|
|
from matplotlib.font_manager import FontProperties
|
|
from matplotlib.lines import Line2D
|
|
from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch,
|
|
StepPatch)
|
|
from matplotlib.collections import (
|
|
Collection, CircleCollection, LineCollection, PathCollection,
|
|
PolyCollection, RegularPolyCollection)
|
|
from matplotlib.text import Text
|
|
from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
|
|
from matplotlib.transforms import BboxTransformTo, BboxTransformFrom
|
|
from matplotlib.offsetbox import (
|
|
AnchoredOffsetbox, DraggableOffsetBox,
|
|
HPacker, VPacker,
|
|
DrawingArea, TextArea,
|
|
)
|
|
from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer
|
|
from . import legend_handler
|
|
|
|
|
|
class DraggableLegend(DraggableOffsetBox):
|
|
def __init__(self, legend, use_blit=False, update="loc"):
|
|
"""
|
|
Wrapper around a `.Legend` to support mouse dragging.
|
|
|
|
Parameters
|
|
----------
|
|
legend : `.Legend`
|
|
The `.Legend` instance to wrap.
|
|
use_blit : bool, optional
|
|
Use blitting for faster image composition. For details see
|
|
:ref:`func-animation`.
|
|
update : {'loc', 'bbox'}, optional
|
|
If "loc", update the *loc* parameter of the legend upon finalizing.
|
|
If "bbox", update the *bbox_to_anchor* parameter.
|
|
"""
|
|
self.legend = legend
|
|
|
|
_api.check_in_list(["loc", "bbox"], update=update)
|
|
self._update = update
|
|
|
|
super().__init__(legend, legend._legend_box, use_blit=use_blit)
|
|
|
|
def finalize_offset(self):
|
|
if self._update == "loc":
|
|
self._update_loc(self.get_loc_in_canvas())
|
|
elif self._update == "bbox":
|
|
self._update_bbox_to_anchor(self.get_loc_in_canvas())
|
|
|
|
def _update_loc(self, loc_in_canvas):
|
|
bbox = self.legend.get_bbox_to_anchor()
|
|
# if bbox has zero width or height, the transformation is
|
|
# ill-defined. Fall back to the default bbox_to_anchor.
|
|
if bbox.width == 0 or bbox.height == 0:
|
|
self.legend.set_bbox_to_anchor(None)
|
|
bbox = self.legend.get_bbox_to_anchor()
|
|
_bbox_transform = BboxTransformFrom(bbox)
|
|
self.legend._loc = tuple(_bbox_transform.transform(loc_in_canvas))
|
|
|
|
def _update_bbox_to_anchor(self, loc_in_canvas):
|
|
loc_in_bbox = self.legend.axes.transAxes.transform(loc_in_canvas)
|
|
self.legend.set_bbox_to_anchor(loc_in_bbox)
|
|
|
|
|
|
_legend_kw_doc_base = """
|
|
bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
|
|
Box that is used to position the legend in conjunction with *loc*.
|
|
Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or
|
|
`figure.bbox` (if `.Figure.legend`). This argument allows arbitrary
|
|
placement of the legend.
|
|
|
|
Bbox coordinates are interpreted in the coordinate system given by
|
|
*bbox_transform*, with the default transform
|
|
Axes or Figure coordinates, depending on which ``legend`` is called.
|
|
|
|
If a 4-tuple or `.BboxBase` is given, then it specifies the bbox
|
|
``(x, y, width, height)`` that the legend is placed in.
|
|
To put the legend in the best location in the bottom right
|
|
quadrant of the Axes (or figure)::
|
|
|
|
loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.5)
|
|
|
|
A 2-tuple ``(x, y)`` places the corner of the legend specified by *loc* at
|
|
x, y. For example, to put the legend's upper right-hand corner in the
|
|
center of the Axes (or figure) the following keywords can be used::
|
|
|
|
loc='upper right', bbox_to_anchor=(0.5, 0.5)
|
|
|
|
ncols : int, default: 1
|
|
The number of columns that the legend has.
|
|
|
|
For backward compatibility, the spelling *ncol* is also supported
|
|
but it is discouraged. If both are given, *ncols* takes precedence.
|
|
|
|
prop : None or `~matplotlib.font_manager.FontProperties` or dict
|
|
The font properties of the legend. If None (default), the current
|
|
:data:`matplotlib.rcParams` will be used.
|
|
|
|
fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \
|
|
'x-large', 'xx-large'}
|
|
The font size of the legend. If the value is numeric the size will be the
|
|
absolute font size in points. String values are relative to the current
|
|
default font size. This argument is only used if *prop* is not specified.
|
|
|
|
labelcolor : str or list, default: :rc:`legend.labelcolor`
|
|
The color of the text in the legend. Either a valid color string
|
|
(for example, 'red'), or a list of color strings. The labelcolor can
|
|
also be made to match the color of the line or marker using 'linecolor',
|
|
'markerfacecolor' (or 'mfc'), or 'markeredgecolor' (or 'mec').
|
|
|
|
Labelcolor can be set globally using :rc:`legend.labelcolor`. If None,
|
|
use :rc:`text.color`.
|
|
|
|
numpoints : int, default: :rc:`legend.numpoints`
|
|
The number of marker points in the legend when creating a legend
|
|
entry for a `.Line2D` (line).
|
|
|
|
scatterpoints : int, default: :rc:`legend.scatterpoints`
|
|
The number of marker points in the legend when creating
|
|
a legend entry for a `.PathCollection` (scatter plot).
|
|
|
|
scatteryoffsets : iterable of floats, default: ``[0.375, 0.5, 0.3125]``
|
|
The vertical offset (relative to the font size) for the markers
|
|
created for a scatter plot legend entry. 0.0 is at the base the
|
|
legend text, and 1.0 is at the top. To draw all markers at the
|
|
same height, set to ``[0.5]``.
|
|
|
|
markerscale : float, default: :rc:`legend.markerscale`
|
|
The relative size of legend markers compared to the originally drawn ones.
|
|
|
|
markerfirst : bool, default: True
|
|
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.
|
|
|
|
reverse : bool, default: False
|
|
If *True*, the legend labels are displayed in reverse order from the input.
|
|
If *False*, the legend labels are displayed in the same order as the input.
|
|
|
|
.. versionadded:: 3.7
|
|
|
|
frameon : bool, default: :rc:`legend.frameon`
|
|
Whether the legend should be drawn on a patch (frame).
|
|
|
|
fancybox : bool, default: :rc:`legend.fancybox`
|
|
Whether round edges should be enabled around the `.FancyBboxPatch` which
|
|
makes up the legend's background.
|
|
|
|
shadow : None, bool or dict, default: :rc:`legend.shadow`
|
|
Whether to draw a shadow behind the legend.
|
|
The shadow can be configured using `.Patch` keywords.
|
|
Customization via :rc:`legend.shadow` is currently not supported.
|
|
|
|
framealpha : float, default: :rc:`legend.framealpha`
|
|
The alpha transparency of the legend's background.
|
|
If *shadow* is activated and *framealpha* is ``None``, the default value is
|
|
ignored.
|
|
|
|
facecolor : "inherit" or color, default: :rc:`legend.facecolor`
|
|
The legend's background color.
|
|
If ``"inherit"``, use :rc:`axes.facecolor`.
|
|
|
|
edgecolor : "inherit" or color, default: :rc:`legend.edgecolor`
|
|
The legend's background patch edge color.
|
|
If ``"inherit"``, use :rc:`axes.edgecolor`.
|
|
|
|
mode : {"expand", None}
|
|
If *mode* is set to ``"expand"`` the legend will be horizontally
|
|
expanded to fill the Axes area (or *bbox_to_anchor* if defines
|
|
the legend's size).
|
|
|
|
bbox_transform : None or `~matplotlib.transforms.Transform`
|
|
The transform for the bounding box (*bbox_to_anchor*). For a value
|
|
of ``None`` (default) the Axes'
|
|
:data:`~matplotlib.axes.Axes.transAxes` transform will be used.
|
|
|
|
title : str or None
|
|
The legend's title. Default is no title (``None``).
|
|
|
|
title_fontproperties : None or `~matplotlib.font_manager.FontProperties` or dict
|
|
The font properties of the legend's title. If None (default), the
|
|
*title_fontsize* argument will be used if present; if *title_fontsize* is
|
|
also None, the current :rc:`legend.title_fontsize` will be used.
|
|
|
|
title_fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \
|
|
'x-large', 'xx-large'}, default: :rc:`legend.title_fontsize`
|
|
The font size of the legend's title.
|
|
Note: This cannot be combined with *title_fontproperties*. If you want
|
|
to set the fontsize alongside other font properties, use the *size*
|
|
parameter in *title_fontproperties*.
|
|
|
|
alignment : {'center', 'left', 'right'}, default: 'center'
|
|
The alignment of the legend title and the box of entries. The entries
|
|
are aligned as a single block, so that markers always lined up.
|
|
|
|
borderpad : float, default: :rc:`legend.borderpad`
|
|
The fractional whitespace inside the legend border, in font-size units.
|
|
|
|
labelspacing : float, default: :rc:`legend.labelspacing`
|
|
The vertical space between the legend entries, in font-size units.
|
|
|
|
handlelength : float, default: :rc:`legend.handlelength`
|
|
The length of the legend handles, in font-size units.
|
|
|
|
handleheight : float, default: :rc:`legend.handleheight`
|
|
The height of the legend handles, in font-size units.
|
|
|
|
handletextpad : float, default: :rc:`legend.handletextpad`
|
|
The pad between the legend handle and text, in font-size units.
|
|
|
|
borderaxespad : float, default: :rc:`legend.borderaxespad`
|
|
The pad between the Axes and legend border, in font-size units.
|
|
|
|
columnspacing : float, default: :rc:`legend.columnspacing`
|
|
The spacing between columns, in font-size units.
|
|
|
|
handler_map : dict or None
|
|
The custom dictionary mapping instances or types to a legend
|
|
handler. This *handler_map* updates the default handler map
|
|
found at `matplotlib.legend.Legend.get_legend_handler_map`.
|
|
|
|
draggable : bool, default: False
|
|
Whether the legend can be dragged with the mouse.
|
|
"""
|
|
|
|
_loc_doc_base = """
|
|
loc : str or pair of floats, default: {default}
|
|
The location of the legend.
|
|
|
|
The strings ``'upper left'``, ``'upper right'``, ``'lower left'``,
|
|
``'lower right'`` place the legend at the corresponding corner of the
|
|
{parent}.
|
|
|
|
The strings ``'upper center'``, ``'lower center'``, ``'center left'``,
|
|
``'center right'`` place the legend at the center of the corresponding edge
|
|
of the {parent}.
|
|
|
|
The string ``'center'`` places the legend at the center of the {parent}.
|
|
{best}
|
|
The location can also be a 2-tuple giving the coordinates of the lower-left
|
|
corner of the legend in {parent} coordinates (in which case *bbox_to_anchor*
|
|
will be ignored).
|
|
|
|
For back-compatibility, ``'center right'`` (but no other location) can also
|
|
be spelled ``'right'``, and each "string" location can also be given as a
|
|
numeric value:
|
|
|
|
================== =============
|
|
Location String Location Code
|
|
================== =============
|
|
'best' (Axes only) 0
|
|
'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
|
|
================== =============
|
|
{outside}"""
|
|
|
|
_loc_doc_best = """
|
|
The string ``'best'`` places the legend at the location, among the nine
|
|
locations defined so far, with the minimum overlap with other drawn
|
|
artists. This option can be quite slow for plots with large amounts of
|
|
data; your plotting speed may benefit from providing a specific location.
|
|
"""
|
|
|
|
_legend_kw_axes_st = (
|
|
_loc_doc_base.format(parent='axes', default=':rc:`legend.loc`',
|
|
best=_loc_doc_best, outside='') +
|
|
_legend_kw_doc_base)
|
|
_docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st)
|
|
|
|
_outside_doc = """
|
|
If a figure is using the constrained layout manager, the string codes
|
|
of the *loc* keyword argument can get better layout behaviour using the
|
|
prefix 'outside'. There is ambiguity at the corners, so 'outside
|
|
upper right' will make space for the legend above the rest of the
|
|
axes in the layout, and 'outside right upper' will make space on the
|
|
right side of the layout. In addition to the values of *loc*
|
|
listed above, we have 'outside right upper', 'outside right lower',
|
|
'outside left upper', and 'outside left lower'. See
|
|
:ref:`legend_guide` for more details.
|
|
"""
|
|
|
|
_legend_kw_figure_st = (
|
|
_loc_doc_base.format(parent='figure', default="'upper right'",
|
|
best='', outside=_outside_doc) +
|
|
_legend_kw_doc_base)
|
|
_docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st)
|
|
|
|
_legend_kw_both_st = (
|
|
_loc_doc_base.format(parent='axes/figure',
|
|
default=":rc:`legend.loc` for Axes, 'upper right' for Figure",
|
|
best=_loc_doc_best, outside=_outside_doc) +
|
|
_legend_kw_doc_base)
|
|
_docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st)
|
|
|
|
_legend_kw_set_loc_st = (
|
|
_loc_doc_base.format(parent='axes/figure',
|
|
default=":rc:`legend.loc` for Axes, 'upper right' for Figure",
|
|
best=_loc_doc_best, outside=_outside_doc))
|
|
_docstring.interpd.update(_legend_kw_set_loc_doc=_legend_kw_set_loc_st)
|
|
|
|
|
|
class Legend(Artist):
|
|
"""
|
|
Place a legend on the figure/axes.
|
|
"""
|
|
|
|
# 'best' is only implemented for Axes legends
|
|
codes = {'best': 0, **AnchoredOffsetbox.codes}
|
|
zorder = 5
|
|
|
|
def __str__(self):
|
|
return "Legend"
|
|
|
|
@_docstring.dedent_interpd
|
|
def __init__(
|
|
self, parent, handles, labels,
|
|
*,
|
|
loc=None,
|
|
numpoints=None, # number of points in the legend line
|
|
markerscale=None, # relative size of legend markers vs. original
|
|
markerfirst=True, # left/right ordering of legend marker and label
|
|
reverse=False, # reverse ordering of legend marker and label
|
|
scatterpoints=None, # number of scatter points
|
|
scatteryoffsets=None,
|
|
prop=None, # properties for the legend texts
|
|
fontsize=None, # keyword to set font size directly
|
|
labelcolor=None, # keyword to set the text color
|
|
|
|
# spacing & pad defined as a fraction of the font-size
|
|
borderpad=None, # whitespace inside the legend border
|
|
labelspacing=None, # vertical space between the legend entries
|
|
handlelength=None, # length of the legend handles
|
|
handleheight=None, # height of the legend handles
|
|
handletextpad=None, # pad between the legend handle and text
|
|
borderaxespad=None, # pad between the Axes and legend border
|
|
columnspacing=None, # spacing between columns
|
|
|
|
ncols=1, # number of columns
|
|
mode=None, # horizontal distribution of columns: None or "expand"
|
|
|
|
fancybox=None, # True: fancy box, False: rounded box, None: rcParam
|
|
shadow=None,
|
|
title=None, # legend title
|
|
title_fontsize=None, # legend title font size
|
|
framealpha=None, # set frame alpha
|
|
edgecolor=None, # frame patch edgecolor
|
|
facecolor=None, # frame patch facecolor
|
|
|
|
bbox_to_anchor=None, # bbox to which the legend will be anchored
|
|
bbox_transform=None, # transform for the bbox
|
|
frameon=None, # draw frame
|
|
handler_map=None,
|
|
title_fontproperties=None, # properties for the legend title
|
|
alignment="center", # control the alignment within the legend box
|
|
ncol=1, # synonym for ncols (backward compatibility)
|
|
draggable=False # whether the legend can be dragged with the mouse
|
|
):
|
|
"""
|
|
Parameters
|
|
----------
|
|
parent : `~matplotlib.axes.Axes` or `.Figure`
|
|
The artist that contains the legend.
|
|
|
|
handles : list of (`.Artist` or tuple of `.Artist`)
|
|
A list of Artists (lines, patches) to be added to the legend.
|
|
|
|
labels : list of str
|
|
A list of labels to show next to the artists. The length of handles
|
|
and labels should be the same. If they are not, they are truncated
|
|
to the length of the shorter list.
|
|
|
|
Other Parameters
|
|
----------------
|
|
%(_legend_kw_doc)s
|
|
|
|
Attributes
|
|
----------
|
|
legend_handles
|
|
List of `.Artist` objects added as legend entries.
|
|
|
|
.. versionadded:: 3.7
|
|
"""
|
|
# local import only to avoid circularity
|
|
from matplotlib.axes import Axes
|
|
from matplotlib.figure import FigureBase
|
|
|
|
super().__init__()
|
|
|
|
if prop is None:
|
|
self.prop = FontProperties(size=mpl._val_or_rc(fontsize, "legend.fontsize"))
|
|
else:
|
|
self.prop = FontProperties._from_any(prop)
|
|
if isinstance(prop, dict) and "size" not in prop:
|
|
self.prop.set_size(mpl.rcParams["legend.fontsize"])
|
|
|
|
self._fontsize = self.prop.get_size_in_points()
|
|
|
|
self.texts = []
|
|
self.legend_handles = []
|
|
self._legend_title_box = None
|
|
|
|
#: A dictionary with the extra handler mappings for this Legend
|
|
#: instance.
|
|
self._custom_handler_map = handler_map
|
|
|
|
self.numpoints = mpl._val_or_rc(numpoints, 'legend.numpoints')
|
|
self.markerscale = mpl._val_or_rc(markerscale, 'legend.markerscale')
|
|
self.scatterpoints = mpl._val_or_rc(scatterpoints, 'legend.scatterpoints')
|
|
self.borderpad = mpl._val_or_rc(borderpad, 'legend.borderpad')
|
|
self.labelspacing = mpl._val_or_rc(labelspacing, 'legend.labelspacing')
|
|
self.handlelength = mpl._val_or_rc(handlelength, 'legend.handlelength')
|
|
self.handleheight = mpl._val_or_rc(handleheight, 'legend.handleheight')
|
|
self.handletextpad = mpl._val_or_rc(handletextpad, 'legend.handletextpad')
|
|
self.borderaxespad = mpl._val_or_rc(borderaxespad, 'legend.borderaxespad')
|
|
self.columnspacing = mpl._val_or_rc(columnspacing, 'legend.columnspacing')
|
|
self.shadow = mpl._val_or_rc(shadow, 'legend.shadow')
|
|
# trim handles and labels if illegal label...
|
|
_lab, _hand = [], []
|
|
for label, handle in zip(labels, handles):
|
|
if isinstance(label, str) and label.startswith('_'):
|
|
_api.warn_deprecated("3.8", message=(
|
|
"An artist whose label starts with an underscore was passed to "
|
|
"legend(); such artists will no longer be ignored in the future. "
|
|
"To suppress this warning, explicitly filter out such artists, "
|
|
"e.g. with `[art for art in artists if not "
|
|
"art.get_label().startswith('_')]`."))
|
|
else:
|
|
_lab.append(label)
|
|
_hand.append(handle)
|
|
labels, handles = _lab, _hand
|
|
|
|
if reverse:
|
|
labels.reverse()
|
|
handles.reverse()
|
|
|
|
if len(handles) < 2:
|
|
ncols = 1
|
|
self._ncols = ncols if ncols != 1 else ncol
|
|
|
|
if self.numpoints <= 0:
|
|
raise ValueError("numpoints must be > 0; it was %d" % numpoints)
|
|
|
|
# introduce y-offset for handles of the scatter plot
|
|
if scatteryoffsets is None:
|
|
self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.])
|
|
else:
|
|
self._scatteryoffsets = np.asarray(scatteryoffsets)
|
|
reps = self.scatterpoints // len(self._scatteryoffsets) + 1
|
|
self._scatteryoffsets = np.tile(self._scatteryoffsets,
|
|
reps)[:self.scatterpoints]
|
|
|
|
# _legend_box is a VPacker instance that contains all
|
|
# legend items and will be initialized from _init_legend_box()
|
|
# method.
|
|
self._legend_box = None
|
|
|
|
if isinstance(parent, Axes):
|
|
self.isaxes = True
|
|
self.axes = parent
|
|
self.set_figure(parent.figure)
|
|
elif isinstance(parent, FigureBase):
|
|
self.isaxes = False
|
|
self.set_figure(parent)
|
|
else:
|
|
raise TypeError(
|
|
"Legend needs either Axes or FigureBase as parent"
|
|
)
|
|
self.parent = parent
|
|
|
|
self._mode = mode
|
|
self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
|
|
|
|
# Figure out if self.shadow is valid
|
|
# If shadow was None, rcParams loads False
|
|
# So it shouldn't be None here
|
|
|
|
self._shadow_props = {'ox': 2, 'oy': -2} # default location offsets
|
|
if isinstance(self.shadow, dict):
|
|
self._shadow_props.update(self.shadow)
|
|
self.shadow = True
|
|
elif self.shadow in (0, 1, True, False):
|
|
self.shadow = bool(self.shadow)
|
|
else:
|
|
raise ValueError(
|
|
'Legend shadow must be a dict or bool, not '
|
|
f'{self.shadow!r} of type {type(self.shadow)}.'
|
|
)
|
|
|
|
# We use FancyBboxPatch to draw a legend frame. The location
|
|
# and size of the box will be updated during the drawing time.
|
|
|
|
facecolor = mpl._val_or_rc(facecolor, "legend.facecolor")
|
|
if facecolor == 'inherit':
|
|
facecolor = mpl.rcParams["axes.facecolor"]
|
|
|
|
edgecolor = mpl._val_or_rc(edgecolor, "legend.edgecolor")
|
|
if edgecolor == 'inherit':
|
|
edgecolor = mpl.rcParams["axes.edgecolor"]
|
|
|
|
fancybox = mpl._val_or_rc(fancybox, "legend.fancybox")
|
|
|
|
self.legendPatch = FancyBboxPatch(
|
|
xy=(0, 0), width=1, height=1,
|
|
facecolor=facecolor, edgecolor=edgecolor,
|
|
# If shadow is used, default to alpha=1 (#8943).
|
|
alpha=(framealpha if framealpha is not None
|
|
else 1 if shadow
|
|
else mpl.rcParams["legend.framealpha"]),
|
|
# The width and height of the legendPatch will be set (in draw())
|
|
# to the length that includes the padding. Thus we set pad=0 here.
|
|
boxstyle=("round,pad=0,rounding_size=0.2" if fancybox
|
|
else "square,pad=0"),
|
|
mutation_scale=self._fontsize,
|
|
snap=True,
|
|
visible=mpl._val_or_rc(frameon, "legend.frameon")
|
|
)
|
|
self._set_artist_props(self.legendPatch)
|
|
|
|
_api.check_in_list(["center", "left", "right"], alignment=alignment)
|
|
self._alignment = alignment
|
|
|
|
# init with null renderer
|
|
self._init_legend_box(handles, labels, markerfirst)
|
|
|
|
# Set legend location
|
|
self.set_loc(loc)
|
|
|
|
# figure out title font properties:
|
|
if title_fontsize is not None and title_fontproperties is not None:
|
|
raise ValueError(
|
|
"title_fontsize and title_fontproperties can't be specified "
|
|
"at the same time. Only use one of them. ")
|
|
title_prop_fp = FontProperties._from_any(title_fontproperties)
|
|
if isinstance(title_fontproperties, dict):
|
|
if "size" not in title_fontproperties:
|
|
title_fontsize = mpl.rcParams["legend.title_fontsize"]
|
|
title_prop_fp.set_size(title_fontsize)
|
|
elif title_fontsize is not None:
|
|
title_prop_fp.set_size(title_fontsize)
|
|
elif not isinstance(title_fontproperties, FontProperties):
|
|
title_fontsize = mpl.rcParams["legend.title_fontsize"]
|
|
title_prop_fp.set_size(title_fontsize)
|
|
|
|
self.set_title(title, prop=title_prop_fp)
|
|
|
|
self._draggable = None
|
|
self.set_draggable(state=draggable)
|
|
|
|
# set the text color
|
|
|
|
color_getters = { # getter function depends on line or patch
|
|
'linecolor': ['get_color', 'get_facecolor'],
|
|
'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'],
|
|
'mfc': ['get_markerfacecolor', 'get_facecolor'],
|
|
'markeredgecolor': ['get_markeredgecolor', 'get_edgecolor'],
|
|
'mec': ['get_markeredgecolor', 'get_edgecolor'],
|
|
}
|
|
labelcolor = mpl._val_or_rc(labelcolor, 'legend.labelcolor')
|
|
if labelcolor is None:
|
|
labelcolor = mpl.rcParams['text.color']
|
|
if isinstance(labelcolor, str) and labelcolor in color_getters:
|
|
getter_names = color_getters[labelcolor]
|
|
for handle, text in zip(self.legend_handles, self.texts):
|
|
try:
|
|
if handle.get_array() is not None:
|
|
continue
|
|
except AttributeError:
|
|
pass
|
|
for getter_name in getter_names:
|
|
try:
|
|
color = getattr(handle, getter_name)()
|
|
if isinstance(color, np.ndarray):
|
|
if (
|
|
color.shape[0] == 1
|
|
or np.isclose(color, color[0]).all()
|
|
):
|
|
text.set_color(color[0])
|
|
else:
|
|
pass
|
|
else:
|
|
text.set_color(color)
|
|
break
|
|
except AttributeError:
|
|
pass
|
|
elif cbook._str_equal(labelcolor, 'none'):
|
|
for text in self.texts:
|
|
text.set_color(labelcolor)
|
|
elif np.iterable(labelcolor):
|
|
for text, color in zip(self.texts,
|
|
itertools.cycle(
|
|
colors.to_rgba_array(labelcolor))):
|
|
text.set_color(color)
|
|
else:
|
|
raise ValueError(f"Invalid labelcolor: {labelcolor!r}")
|
|
|
|
def _set_artist_props(self, a):
|
|
"""
|
|
Set the boilerplate props for artists added to Axes.
|
|
"""
|
|
a.set_figure(self.figure)
|
|
if self.isaxes:
|
|
a.axes = self.axes
|
|
|
|
a.set_transform(self.get_transform())
|
|
|
|
@_docstring.dedent_interpd
|
|
def set_loc(self, loc=None):
|
|
"""
|
|
Set the location of the legend.
|
|
|
|
.. versionadded:: 3.8
|
|
|
|
Parameters
|
|
----------
|
|
%(_legend_kw_set_loc_doc)s
|
|
"""
|
|
loc0 = loc
|
|
self._loc_used_default = loc is None
|
|
if loc is None:
|
|
loc = mpl.rcParams["legend.loc"]
|
|
if not self.isaxes and loc in [0, 'best']:
|
|
loc = 'upper right'
|
|
|
|
type_err_message = ("loc must be string, coordinate tuple, or"
|
|
f" an integer 0-10, not {loc!r}")
|
|
|
|
# handle outside legends:
|
|
self._outside_loc = None
|
|
if isinstance(loc, str):
|
|
if loc.split()[0] == 'outside':
|
|
# strip outside:
|
|
loc = loc.split('outside ')[1]
|
|
# strip "center" at the beginning
|
|
self._outside_loc = loc.replace('center ', '')
|
|
# strip first
|
|
self._outside_loc = self._outside_loc.split()[0]
|
|
locs = loc.split()
|
|
if len(locs) > 1 and locs[0] in ('right', 'left'):
|
|
# locs doesn't accept "left upper", etc, so swap
|
|
if locs[0] != 'center':
|
|
locs = locs[::-1]
|
|
loc = locs[0] + ' ' + locs[1]
|
|
# check that loc is in acceptable strings
|
|
loc = _api.check_getitem(self.codes, loc=loc)
|
|
elif np.iterable(loc):
|
|
# coerce iterable into tuple
|
|
loc = tuple(loc)
|
|
# validate the tuple represents Real coordinates
|
|
if len(loc) != 2 or not all(isinstance(e, numbers.Real) for e in loc):
|
|
raise ValueError(type_err_message)
|
|
elif isinstance(loc, int):
|
|
# validate the integer represents a string numeric value
|
|
if loc < 0 or loc > 10:
|
|
raise ValueError(type_err_message)
|
|
else:
|
|
# all other cases are invalid values of loc
|
|
raise ValueError(type_err_message)
|
|
|
|
if self.isaxes and self._outside_loc:
|
|
raise ValueError(
|
|
f"'outside' option for loc='{loc0}' keyword argument only "
|
|
"works for figure legends")
|
|
|
|
if not self.isaxes and loc == 0:
|
|
raise ValueError(
|
|
"Automatic legend placement (loc='best') not implemented for "
|
|
"figure legend")
|
|
|
|
tmp = self._loc_used_default
|
|
self._set_loc(loc)
|
|
self._loc_used_default = tmp # ignore changes done by _set_loc
|
|
|
|
def _set_loc(self, loc):
|
|
# find_offset function will be provided to _legend_box and
|
|
# _legend_box will draw itself at the location of the return
|
|
# value of the find_offset.
|
|
self._loc_used_default = False
|
|
self._loc_real = loc
|
|
self.stale = True
|
|
self._legend_box.set_offset(self._findoffset)
|
|
|
|
def set_ncols(self, ncols):
|
|
"""Set the number of columns."""
|
|
self._ncols = ncols
|
|
|
|
def _get_loc(self):
|
|
return self._loc_real
|
|
|
|
_loc = property(_get_loc, _set_loc)
|
|
|
|
def _findoffset(self, width, height, xdescent, ydescent, renderer):
|
|
"""Helper function to locate the legend."""
|
|
|
|
if self._loc == 0: # "best".
|
|
x, y = self._find_best_position(width, height, renderer)
|
|
elif self._loc in Legend.codes.values(): # Fixed location.
|
|
bbox = Bbox.from_bounds(0, 0, width, height)
|
|
x, y = self._get_anchored_bbox(self._loc, bbox,
|
|
self.get_bbox_to_anchor(),
|
|
renderer)
|
|
else: # Axes or figure coordinates.
|
|
fx, fy = self._loc
|
|
bbox = self.get_bbox_to_anchor()
|
|
x, y = bbox.x0 + bbox.width * fx, bbox.y0 + bbox.height * fy
|
|
|
|
return x + xdescent, y + ydescent
|
|
|
|
@allow_rasterization
|
|
def draw(self, renderer):
|
|
# docstring inherited
|
|
if not self.get_visible():
|
|
return
|
|
|
|
renderer.open_group('legend', gid=self.get_gid())
|
|
|
|
fontsize = renderer.points_to_pixels(self._fontsize)
|
|
|
|
# if mode == fill, set the width of the legend_box to the
|
|
# width of the parent (minus pads)
|
|
if self._mode in ["expand"]:
|
|
pad = 2 * (self.borderaxespad + self.borderpad) * fontsize
|
|
self._legend_box.set_width(self.get_bbox_to_anchor().width - pad)
|
|
|
|
# update the location and size of the legend. This needs to
|
|
# be done in any case to clip the figure right.
|
|
bbox = self._legend_box.get_window_extent(renderer)
|
|
self.legendPatch.set_bounds(bbox.bounds)
|
|
self.legendPatch.set_mutation_scale(fontsize)
|
|
|
|
# self.shadow is validated in __init__
|
|
# So by here it is a bool and self._shadow_props contains any configs
|
|
|
|
if self.shadow:
|
|
Shadow(self.legendPatch, **self._shadow_props).draw(renderer)
|
|
|
|
self.legendPatch.draw(renderer)
|
|
self._legend_box.draw(renderer)
|
|
|
|
renderer.close_group('legend')
|
|
self.stale = False
|
|
|
|
# _default_handler_map defines the default mapping between plot
|
|
# elements and the legend handlers.
|
|
|
|
_default_handler_map = {
|
|
StemContainer: legend_handler.HandlerStem(),
|
|
ErrorbarContainer: legend_handler.HandlerErrorbar(),
|
|
Line2D: legend_handler.HandlerLine2D(),
|
|
Patch: legend_handler.HandlerPatch(),
|
|
StepPatch: legend_handler.HandlerStepPatch(),
|
|
LineCollection: legend_handler.HandlerLineCollection(),
|
|
RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
|
|
CircleCollection: legend_handler.HandlerCircleCollection(),
|
|
BarContainer: legend_handler.HandlerPatch(
|
|
update_func=legend_handler.update_from_first_child),
|
|
tuple: legend_handler.HandlerTuple(),
|
|
PathCollection: legend_handler.HandlerPathCollection(),
|
|
PolyCollection: legend_handler.HandlerPolyCollection()
|
|
}
|
|
|
|
# (get|set|update)_default_handler_maps are public interfaces to
|
|
# modify the default handler map.
|
|
|
|
@classmethod
|
|
def get_default_handler_map(cls):
|
|
"""Return the global default handler map, shared by all legends."""
|
|
return cls._default_handler_map
|
|
|
|
@classmethod
|
|
def set_default_handler_map(cls, handler_map):
|
|
"""Set the global default handler map, shared by all legends."""
|
|
cls._default_handler_map = handler_map
|
|
|
|
@classmethod
|
|
def update_default_handler_map(cls, handler_map):
|
|
"""Update the global default handler map, shared by all legends."""
|
|
cls._default_handler_map.update(handler_map)
|
|
|
|
def get_legend_handler_map(self):
|
|
"""Return this legend instance's handler map."""
|
|
default_handler_map = self.get_default_handler_map()
|
|
return ({**default_handler_map, **self._custom_handler_map}
|
|
if self._custom_handler_map else default_handler_map)
|
|
|
|
@staticmethod
|
|
def get_legend_handler(legend_handler_map, orig_handle):
|
|
"""
|
|
Return a legend handler from *legend_handler_map* that
|
|
corresponds to *orig_handler*.
|
|
|
|
*legend_handler_map* should be a dictionary object (that is
|
|
returned by the get_legend_handler_map method).
|
|
|
|
It first checks if the *orig_handle* itself is a key in the
|
|
*legend_handler_map* and return the associated value.
|
|
Otherwise, it checks for each of the classes in its
|
|
method-resolution-order. If no matching key is found, it
|
|
returns ``None``.
|
|
"""
|
|
try:
|
|
return legend_handler_map[orig_handle]
|
|
except (TypeError, KeyError): # TypeError if unhashable.
|
|
pass
|
|
for handle_type in type(orig_handle).mro():
|
|
try:
|
|
return legend_handler_map[handle_type]
|
|
except KeyError:
|
|
pass
|
|
return None
|
|
|
|
def _init_legend_box(self, handles, labels, markerfirst=True):
|
|
"""
|
|
Initialize the legend_box. The legend_box is an instance of
|
|
the OffsetBox, which is packed with legend handles and
|
|
texts. Once packed, their location is calculated during the
|
|
drawing time.
|
|
"""
|
|
|
|
fontsize = self._fontsize
|
|
|
|
# legend_box is a HPacker, horizontally packed with columns.
|
|
# Each column is a VPacker, vertically packed with legend items.
|
|
# Each legend item is a HPacker packed with:
|
|
# - handlebox: a DrawingArea which contains the legend handle.
|
|
# - labelbox: a TextArea which contains the legend text.
|
|
|
|
text_list = [] # the list of text instances
|
|
handle_list = [] # the list of handle instances
|
|
handles_and_labels = []
|
|
|
|
# The approximate height and descent of text. These values are
|
|
# only used for plotting the legend handle.
|
|
descent = 0.35 * fontsize * (self.handleheight - 0.7) # heuristic.
|
|
height = fontsize * self.handleheight - descent
|
|
# each handle needs to be drawn inside a box of (x, y, w, h) =
|
|
# (0, -descent, width, height). And their coordinates should
|
|
# be given in the display coordinates.
|
|
|
|
# The transformation of each handle will be automatically set
|
|
# to self.get_transform(). If the artist does not use its
|
|
# default transform (e.g., Collections), you need to
|
|
# manually set their transform to the self.get_transform().
|
|
legend_handler_map = self.get_legend_handler_map()
|
|
|
|
for orig_handle, label in zip(handles, labels):
|
|
handler = self.get_legend_handler(legend_handler_map, orig_handle)
|
|
if handler is None:
|
|
_api.warn_external(
|
|
"Legend does not support handles for "
|
|
f"{type(orig_handle).__name__} "
|
|
"instances.\nA proxy artist may be used "
|
|
"instead.\nSee: https://matplotlib.org/"
|
|
"stable/users/explain/axes/legend_guide.html"
|
|
"#controlling-the-legend-entries")
|
|
# No handle for this artist, so we just defer to None.
|
|
handle_list.append(None)
|
|
else:
|
|
textbox = TextArea(label, multilinebaseline=True,
|
|
textprops=dict(
|
|
verticalalignment='baseline',
|
|
horizontalalignment='left',
|
|
fontproperties=self.prop))
|
|
handlebox = DrawingArea(width=self.handlelength * fontsize,
|
|
height=height,
|
|
xdescent=0., ydescent=descent)
|
|
|
|
text_list.append(textbox._text)
|
|
# Create the artist for the legend which represents the
|
|
# original artist/handle.
|
|
handle_list.append(handler.legend_artist(self, orig_handle,
|
|
fontsize, handlebox))
|
|
handles_and_labels.append((handlebox, textbox))
|
|
|
|
columnbox = []
|
|
# array_split splits n handles_and_labels into ncols columns, with the
|
|
# first n%ncols columns having an extra entry. filter(len, ...)
|
|
# handles the case where n < ncols: the last ncols-n columns are empty
|
|
# and get filtered out.
|
|
for handles_and_labels_column in filter(
|
|
len, np.array_split(handles_and_labels, self._ncols)):
|
|
# pack handlebox and labelbox into itembox
|
|
itemboxes = [HPacker(pad=0,
|
|
sep=self.handletextpad * fontsize,
|
|
children=[h, t] if markerfirst else [t, h],
|
|
align="baseline")
|
|
for h, t in handles_and_labels_column]
|
|
# pack columnbox
|
|
alignment = "baseline" if markerfirst else "right"
|
|
columnbox.append(VPacker(pad=0,
|
|
sep=self.labelspacing * fontsize,
|
|
align=alignment,
|
|
children=itemboxes))
|
|
|
|
mode = "expand" if self._mode == "expand" else "fixed"
|
|
sep = self.columnspacing * fontsize
|
|
self._legend_handle_box = HPacker(pad=0,
|
|
sep=sep, align="baseline",
|
|
mode=mode,
|
|
children=columnbox)
|
|
self._legend_title_box = TextArea("")
|
|
self._legend_box = VPacker(pad=self.borderpad * fontsize,
|
|
sep=self.labelspacing * fontsize,
|
|
align=self._alignment,
|
|
children=[self._legend_title_box,
|
|
self._legend_handle_box])
|
|
self._legend_box.set_figure(self.figure)
|
|
self._legend_box.axes = self.axes
|
|
self.texts = text_list
|
|
self.legend_handles = handle_list
|
|
|
|
def _auto_legend_data(self):
|
|
"""
|
|
Return display coordinates for hit testing for "best" positioning.
|
|
|
|
Returns
|
|
-------
|
|
bboxes
|
|
List of bounding boxes of all patches.
|
|
lines
|
|
List of `.Path` corresponding to each line.
|
|
offsets
|
|
List of (x, y) offsets of all collection.
|
|
"""
|
|
assert self.isaxes # always holds, as this is only called internally
|
|
bboxes = []
|
|
lines = []
|
|
offsets = []
|
|
for artist in self.parent._children:
|
|
if isinstance(artist, Line2D):
|
|
lines.append(
|
|
artist.get_transform().transform_path(artist.get_path()))
|
|
elif isinstance(artist, Rectangle):
|
|
bboxes.append(
|
|
artist.get_bbox().transformed(artist.get_data_transform()))
|
|
elif isinstance(artist, Patch):
|
|
lines.append(
|
|
artist.get_transform().transform_path(artist.get_path()))
|
|
elif isinstance(artist, PolyCollection):
|
|
lines.extend(artist.get_transform().transform_path(path)
|
|
for path in artist.get_paths())
|
|
elif isinstance(artist, Collection):
|
|
transform, transOffset, hoffsets, _ = artist._prepare_points()
|
|
if len(hoffsets):
|
|
offsets.extend(transOffset.transform(hoffsets))
|
|
elif isinstance(artist, Text):
|
|
bboxes.append(artist.get_window_extent())
|
|
|
|
return bboxes, lines, offsets
|
|
|
|
def get_children(self):
|
|
# docstring inherited
|
|
return [self._legend_box, self.get_frame()]
|
|
|
|
def get_frame(self):
|
|
"""Return the `~.patches.Rectangle` used to frame the legend."""
|
|
return self.legendPatch
|
|
|
|
def get_lines(self):
|
|
r"""Return the list of `~.lines.Line2D`\s in the legend."""
|
|
return [h for h in self.legend_handles if isinstance(h, Line2D)]
|
|
|
|
def get_patches(self):
|
|
r"""Return the list of `~.patches.Patch`\s in the legend."""
|
|
return silent_list('Patch',
|
|
[h for h in self.legend_handles
|
|
if isinstance(h, Patch)])
|
|
|
|
def get_texts(self):
|
|
r"""Return the list of `~.text.Text`\s in the legend."""
|
|
return silent_list('Text', self.texts)
|
|
|
|
def set_alignment(self, alignment):
|
|
"""
|
|
Set the alignment of the legend title and the box of entries.
|
|
|
|
The entries are aligned as a single block, so that markers always
|
|
lined up.
|
|
|
|
Parameters
|
|
----------
|
|
alignment : {'center', 'left', 'right'}.
|
|
|
|
"""
|
|
_api.check_in_list(["center", "left", "right"], alignment=alignment)
|
|
self._alignment = alignment
|
|
self._legend_box.align = alignment
|
|
|
|
def get_alignment(self):
|
|
"""Get the alignment value of the legend box"""
|
|
return self._legend_box.align
|
|
|
|
def set_title(self, title, prop=None):
|
|
"""
|
|
Set legend title and title style.
|
|
|
|
Parameters
|
|
----------
|
|
title : str
|
|
The legend title.
|
|
|
|
prop : `.font_manager.FontProperties` or `str` or `pathlib.Path`
|
|
The font properties of the legend title.
|
|
If a `str`, it is interpreted as a fontconfig pattern parsed by
|
|
`.FontProperties`. If a `pathlib.Path`, it is interpreted as the
|
|
absolute path to a font file.
|
|
|
|
"""
|
|
self._legend_title_box._text.set_text(title)
|
|
if title:
|
|
self._legend_title_box._text.set_visible(True)
|
|
self._legend_title_box.set_visible(True)
|
|
else:
|
|
self._legend_title_box._text.set_visible(False)
|
|
self._legend_title_box.set_visible(False)
|
|
|
|
if prop is not None:
|
|
self._legend_title_box._text.set_fontproperties(prop)
|
|
|
|
self.stale = True
|
|
|
|
def get_title(self):
|
|
"""Return the `.Text` instance for the legend title."""
|
|
return self._legend_title_box._text
|
|
|
|
def get_window_extent(self, renderer=None):
|
|
# docstring inherited
|
|
if renderer is None:
|
|
renderer = self.figure._get_renderer()
|
|
return self._legend_box.get_window_extent(renderer=renderer)
|
|
|
|
def get_tightbbox(self, renderer=None):
|
|
# docstring inherited
|
|
return self._legend_box.get_window_extent(renderer)
|
|
|
|
def get_frame_on(self):
|
|
"""Get whether the legend box patch is drawn."""
|
|
return self.legendPatch.get_visible()
|
|
|
|
def set_frame_on(self, b):
|
|
"""
|
|
Set whether the legend box patch is drawn.
|
|
|
|
Parameters
|
|
----------
|
|
b : bool
|
|
"""
|
|
self.legendPatch.set_visible(b)
|
|
self.stale = True
|
|
|
|
draw_frame = set_frame_on # Backcompat alias.
|
|
|
|
def get_bbox_to_anchor(self):
|
|
"""Return the bbox that the legend will be anchored to."""
|
|
if self._bbox_to_anchor is None:
|
|
return self.parent.bbox
|
|
else:
|
|
return self._bbox_to_anchor
|
|
|
|
def set_bbox_to_anchor(self, bbox, transform=None):
|
|
"""
|
|
Set the bbox that the legend will be anchored to.
|
|
|
|
Parameters
|
|
----------
|
|
bbox : `~matplotlib.transforms.BboxBase` or tuple
|
|
The bounding box can be specified in the following ways:
|
|
|
|
- A `.BboxBase` instance
|
|
- A tuple of ``(left, bottom, width, height)`` in the given
|
|
transform (normalized axes coordinate if None)
|
|
- A tuple of ``(left, bottom)`` where the width and height will be
|
|
assumed to be zero.
|
|
- *None*, to remove the bbox anchoring, and use the parent bbox.
|
|
|
|
transform : `~matplotlib.transforms.Transform`, optional
|
|
A transform to apply to the bounding box. If not specified, this
|
|
will use a transform to the bounding box of the parent.
|
|
"""
|
|
if bbox is None:
|
|
self._bbox_to_anchor = None
|
|
return
|
|
elif isinstance(bbox, BboxBase):
|
|
self._bbox_to_anchor = bbox
|
|
else:
|
|
try:
|
|
l = len(bbox)
|
|
except TypeError as err:
|
|
raise ValueError(f"Invalid bbox: {bbox}") from err
|
|
|
|
if l == 2:
|
|
bbox = [bbox[0], bbox[1], 0, 0]
|
|
|
|
self._bbox_to_anchor = Bbox.from_bounds(*bbox)
|
|
|
|
if transform is None:
|
|
transform = BboxTransformTo(self.parent.bbox)
|
|
|
|
self._bbox_to_anchor = TransformedBbox(self._bbox_to_anchor,
|
|
transform)
|
|
self.stale = True
|
|
|
|
def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
|
|
"""
|
|
Place the *bbox* inside the *parentbbox* according to a given
|
|
location code. Return the (x, y) coordinate of the bbox.
|
|
|
|
Parameters
|
|
----------
|
|
loc : int
|
|
A location code in range(1, 11). This corresponds to the possible
|
|
values for ``self._loc``, excluding "best".
|
|
bbox : `~matplotlib.transforms.Bbox`
|
|
bbox to be placed, in display coordinates.
|
|
parentbbox : `~matplotlib.transforms.Bbox`
|
|
A parent box which will contain the bbox, in display coordinates.
|
|
"""
|
|
return offsetbox._get_anchored_bbox(
|
|
loc, bbox, parentbbox,
|
|
self.borderaxespad * renderer.points_to_pixels(self._fontsize))
|
|
|
|
def _find_best_position(self, width, height, renderer):
|
|
"""Determine the best location to place the legend."""
|
|
assert self.isaxes # always holds, as this is only called internally
|
|
|
|
start_time = time.perf_counter()
|
|
|
|
bboxes, lines, offsets = self._auto_legend_data()
|
|
|
|
bbox = Bbox.from_bounds(0, 0, width, height)
|
|
|
|
candidates = []
|
|
for idx in range(1, len(self.codes)):
|
|
l, b = self._get_anchored_bbox(idx, bbox,
|
|
self.get_bbox_to_anchor(),
|
|
renderer)
|
|
legendBox = Bbox.from_bounds(l, b, width, height)
|
|
# XXX TODO: If markers are present, it would be good to take them
|
|
# into account when checking vertex overlaps in the next line.
|
|
badness = (sum(legendBox.count_contains(line.vertices)
|
|
for line in lines)
|
|
+ legendBox.count_contains(offsets)
|
|
+ legendBox.count_overlaps(bboxes)
|
|
+ sum(line.intersects_bbox(legendBox, filled=False)
|
|
for line in lines))
|
|
# Include the index to favor lower codes in case of a tie.
|
|
candidates.append((badness, idx, (l, b)))
|
|
if badness == 0:
|
|
break
|
|
|
|
_, _, (l, b) = min(candidates)
|
|
|
|
if self._loc_used_default and time.perf_counter() - start_time > 1:
|
|
_api.warn_external(
|
|
'Creating legend with loc="best" can be slow with large '
|
|
'amounts of data.')
|
|
|
|
return l, b
|
|
|
|
@_api.rename_parameter("3.8", "event", "mouseevent")
|
|
def contains(self, mouseevent):
|
|
return self.legendPatch.contains(mouseevent)
|
|
|
|
def set_draggable(self, state, use_blit=False, update='loc'):
|
|
"""
|
|
Enable or disable mouse dragging support of the legend.
|
|
|
|
Parameters
|
|
----------
|
|
state : bool
|
|
Whether mouse dragging is enabled.
|
|
use_blit : bool, optional
|
|
Use blitting for faster image composition. For details see
|
|
:ref:`func-animation`.
|
|
update : {'loc', 'bbox'}, optional
|
|
The legend parameter to be changed when dragged:
|
|
|
|
- 'loc': update the *loc* parameter of the legend
|
|
- 'bbox': update the *bbox_to_anchor* parameter of the legend
|
|
|
|
Returns
|
|
-------
|
|
`.DraggableLegend` or *None*
|
|
If *state* is ``True`` this returns the `.DraggableLegend` helper
|
|
instance. Otherwise this returns *None*.
|
|
"""
|
|
if state:
|
|
if self._draggable is None:
|
|
self._draggable = DraggableLegend(self,
|
|
use_blit,
|
|
update=update)
|
|
else:
|
|
if self._draggable is not None:
|
|
self._draggable.disconnect()
|
|
self._draggable = None
|
|
return self._draggable
|
|
|
|
def get_draggable(self):
|
|
"""Return ``True`` if the legend is draggable, ``False`` otherwise."""
|
|
return self._draggable is not None
|
|
|
|
|
|
# Helper functions to parse legend arguments for both `figure.legend` and
|
|
# `axes.legend`:
|
|
def _get_legend_handles(axs, legend_handler_map=None):
|
|
"""Yield artists that can be used as handles in a legend."""
|
|
handles_original = []
|
|
for ax in axs:
|
|
handles_original += [
|
|
*(a for a in ax._children
|
|
if isinstance(a, (Line2D, Patch, Collection, Text))),
|
|
*ax.containers]
|
|
# support parasite Axes:
|
|
if hasattr(ax, 'parasites'):
|
|
for axx in ax.parasites:
|
|
handles_original += [
|
|
*(a for a in axx._children
|
|
if isinstance(a, (Line2D, Patch, Collection, Text))),
|
|
*axx.containers]
|
|
|
|
handler_map = {**Legend.get_default_handler_map(),
|
|
**(legend_handler_map or {})}
|
|
has_handler = Legend.get_legend_handler
|
|
for handle in handles_original:
|
|
label = handle.get_label()
|
|
if label != '_nolegend_' and has_handler(handler_map, handle):
|
|
yield handle
|
|
elif (label and not label.startswith('_') and
|
|
not has_handler(handler_map, handle)):
|
|
_api.warn_external(
|
|
"Legend does not support handles for "
|
|
f"{type(handle).__name__} "
|
|
"instances.\nSee: https://matplotlib.org/stable/"
|
|
"tutorials/intermediate/legend_guide.html"
|
|
"#implementing-a-custom-legend-handler")
|
|
continue
|
|
|
|
|
|
def _get_legend_handles_labels(axs, legend_handler_map=None):
|
|
"""Return handles and labels for legend."""
|
|
handles = []
|
|
labels = []
|
|
for handle in _get_legend_handles(axs, legend_handler_map):
|
|
label = handle.get_label()
|
|
if label and not label.startswith('_'):
|
|
handles.append(handle)
|
|
labels.append(label)
|
|
return handles, labels
|
|
|
|
|
|
def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs):
|
|
"""
|
|
Get the handles and labels from the calls to either ``figure.legend``
|
|
or ``axes.legend``.
|
|
|
|
The parser is a bit involved because we support::
|
|
|
|
legend()
|
|
legend(labels)
|
|
legend(handles, labels)
|
|
legend(labels=labels)
|
|
legend(handles=handles)
|
|
legend(handles=handles, labels=labels)
|
|
|
|
The behavior for a mixture of positional and keyword handles and labels
|
|
is undefined and issues a warning; it will be an error in the future.
|
|
|
|
Parameters
|
|
----------
|
|
axs : list of `.Axes`
|
|
If handles are not given explicitly, the artists in these Axes are
|
|
used as handles.
|
|
*args : tuple
|
|
Positional parameters passed to ``legend()``.
|
|
handles
|
|
The value of the keyword argument ``legend(handles=...)``, or *None*
|
|
if that keyword argument was not used.
|
|
labels
|
|
The value of the keyword argument ``legend(labels=...)``, or *None*
|
|
if that keyword argument was not used.
|
|
**kwargs
|
|
All other keyword arguments passed to ``legend()``.
|
|
|
|
Returns
|
|
-------
|
|
handles : list of (`.Artist` or tuple of `.Artist`)
|
|
The legend handles.
|
|
labels : list of str
|
|
The legend labels.
|
|
kwargs : dict
|
|
*kwargs* with keywords handles and labels removed.
|
|
|
|
"""
|
|
log = logging.getLogger(__name__)
|
|
|
|
handlers = kwargs.get('handler_map')
|
|
|
|
if (handles is not None or labels is not None) and args:
|
|
_api.warn_deprecated("3.9", message=(
|
|
"You have mixed positional and keyword arguments, some input may "
|
|
"be discarded. This is deprecated since %(since)s and will "
|
|
"become an error %(removal)s."))
|
|
|
|
if (hasattr(handles, "__len__") and
|
|
hasattr(labels, "__len__") and
|
|
len(handles) != len(labels)):
|
|
_api.warn_external(f"Mismatched number of handles and labels: "
|
|
f"len(handles) = {len(handles)} "
|
|
f"len(labels) = {len(labels)}")
|
|
# if got both handles and labels as kwargs, make same length
|
|
if handles and labels:
|
|
handles, labels = zip(*zip(handles, labels))
|
|
|
|
elif handles is not None and labels is None:
|
|
labels = [handle.get_label() for handle in handles]
|
|
|
|
elif labels is not None and handles is None:
|
|
# Get as many handles as there are labels.
|
|
handles = [handle for handle, label
|
|
in zip(_get_legend_handles(axs, handlers), labels)]
|
|
|
|
elif len(args) == 0: # 0 args: automatically detect labels and handles.
|
|
handles, labels = _get_legend_handles_labels(axs, handlers)
|
|
if not handles:
|
|
_api.warn_external(
|
|
"No artists with labels found to put in legend. Note that "
|
|
"artists whose label start with an underscore are ignored "
|
|
"when legend() is called with no argument.")
|
|
|
|
elif len(args) == 1: # 1 arg: user defined labels, automatic handle detection.
|
|
labels, = args
|
|
if any(isinstance(l, Artist) for l in labels):
|
|
raise TypeError("A single argument passed to legend() must be a "
|
|
"list of labels, but found an Artist in there.")
|
|
|
|
# Get as many handles as there are labels.
|
|
handles = [handle for handle, label
|
|
in zip(_get_legend_handles(axs, handlers), labels)]
|
|
|
|
elif len(args) == 2: # 2 args: user defined handles and labels.
|
|
handles, labels = args[:2]
|
|
|
|
else:
|
|
raise _api.nargs_error('legend', '0-2', len(args))
|
|
|
|
return handles, labels, kwargs
|