1"""
2The legend module defines the Legend class, which is responsible for
3drawing legends associated with Axes and/or figures.
4
5.. important::
6
7 It is unlikely that you would ever create a Legend instance manually.
8 Most users would normally create a legend via the `~.Axes.legend`
9 function. For more details on legends there is also a :ref:`legend guide
10 <legend_guide>`.
11
12The `Legend` class is a container of legend handles and legend texts.
13
14The legend handler map specifies how to create legend handles from artists
15(lines, patches, etc.) in the Axes or figures. Default legend handlers are
16defined in the :mod:`~matplotlib.legend_handler` module. While not all artist
17types are covered by the default legend handlers, custom legend handlers can be
18defined to support arbitrary objects.
19
20See the :ref`<legend_guide>` for more
21information.
22"""
23
24import itertools
25import logging
26import numbers
27import time
28
29import numpy as np
30
31import matplotlib as mpl
32from matplotlib import _api, _docstring, cbook, colors, offsetbox
33from matplotlib.artist import Artist, allow_rasterization
34from matplotlib.cbook import silent_list
35from matplotlib.font_manager import FontProperties
36from matplotlib.lines import Line2D
37from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch,
38 StepPatch)
39from matplotlib.collections import (
40 Collection, CircleCollection, LineCollection, PathCollection,
41 PolyCollection, RegularPolyCollection)
42from matplotlib.text import Text
43from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
44from matplotlib.transforms import BboxTransformTo, BboxTransformFrom
45from matplotlib.offsetbox import (
46 AnchoredOffsetbox, DraggableOffsetBox,
47 HPacker, VPacker,
48 DrawingArea, TextArea,
49)
50from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer
51from . import legend_handler
52
53
54class DraggableLegend(DraggableOffsetBox):
55 def __init__(self, legend, use_blit=False, update="loc"):
56 """
57 Wrapper around a `.Legend` to support mouse dragging.
58
59 Parameters
60 ----------
61 legend : `.Legend`
62 The `.Legend` instance to wrap.
63 use_blit : bool, optional
64 Use blitting for faster image composition. For details see
65 :ref:`func-animation`.
66 update : {'loc', 'bbox'}, optional
67 If "loc", update the *loc* parameter of the legend upon finalizing.
68 If "bbox", update the *bbox_to_anchor* parameter.
69 """
70 self.legend = legend
71
72 _api.check_in_list(["loc", "bbox"], update=update)
73 self._update = update
74
75 super().__init__(legend, legend._legend_box, use_blit=use_blit)
76
77 def finalize_offset(self):
78 if self._update == "loc":
79 self._update_loc(self.get_loc_in_canvas())
80 elif self._update == "bbox":
81 self._update_bbox_to_anchor(self.get_loc_in_canvas())
82
83 def _update_loc(self, loc_in_canvas):
84 bbox = self.legend.get_bbox_to_anchor()
85 # if bbox has zero width or height, the transformation is
86 # ill-defined. Fall back to the default bbox_to_anchor.
87 if bbox.width == 0 or bbox.height == 0:
88 self.legend.set_bbox_to_anchor(None)
89 bbox = self.legend.get_bbox_to_anchor()
90 _bbox_transform = BboxTransformFrom(bbox)
91 self.legend._loc = tuple(_bbox_transform.transform(loc_in_canvas))
92
93 def _update_bbox_to_anchor(self, loc_in_canvas):
94 loc_in_bbox = self.legend.axes.transAxes.transform(loc_in_canvas)
95 self.legend.set_bbox_to_anchor(loc_in_bbox)
96
97
98_legend_kw_doc_base = """
99bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
100 Box that is used to position the legend in conjunction with *loc*.
101 Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or
102 `figure.bbox` (if `.Figure.legend`). This argument allows arbitrary
103 placement of the legend.
104
105 Bbox coordinates are interpreted in the coordinate system given by
106 *bbox_transform*, with the default transform
107 Axes or Figure coordinates, depending on which ``legend`` is called.
108
109 If a 4-tuple or `.BboxBase` is given, then it specifies the bbox
110 ``(x, y, width, height)`` that the legend is placed in.
111 To put the legend in the best location in the bottom right
112 quadrant of the Axes (or figure)::
113
114 loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.5)
115
116 A 2-tuple ``(x, y)`` places the corner of the legend specified by *loc* at
117 x, y. For example, to put the legend's upper right-hand corner in the
118 center of the Axes (or figure) the following keywords can be used::
119
120 loc='upper right', bbox_to_anchor=(0.5, 0.5)
121
122ncols : int, default: 1
123 The number of columns that the legend has.
124
125 For backward compatibility, the spelling *ncol* is also supported
126 but it is discouraged. If both are given, *ncols* takes precedence.
127
128prop : None or `~matplotlib.font_manager.FontProperties` or dict
129 The font properties of the legend. If None (default), the current
130 :data:`matplotlib.rcParams` will be used.
131
132fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \
133'x-large', 'xx-large'}
134 The font size of the legend. If the value is numeric the size will be the
135 absolute font size in points. String values are relative to the current
136 default font size. This argument is only used if *prop* is not specified.
137
138labelcolor : str or list, default: :rc:`legend.labelcolor`
139 The color of the text in the legend. Either a valid color string
140 (for example, 'red'), or a list of color strings. The labelcolor can
141 also be made to match the color of the line or marker using 'linecolor',
142 'markerfacecolor' (or 'mfc'), or 'markeredgecolor' (or 'mec').
143
144 Labelcolor can be set globally using :rc:`legend.labelcolor`. If None,
145 use :rc:`text.color`.
146
147numpoints : int, default: :rc:`legend.numpoints`
148 The number of marker points in the legend when creating a legend
149 entry for a `.Line2D` (line).
150
151scatterpoints : int, default: :rc:`legend.scatterpoints`
152 The number of marker points in the legend when creating
153 a legend entry for a `.PathCollection` (scatter plot).
154
155scatteryoffsets : iterable of floats, default: ``[0.375, 0.5, 0.3125]``
156 The vertical offset (relative to the font size) for the markers
157 created for a scatter plot legend entry. 0.0 is at the base the
158 legend text, and 1.0 is at the top. To draw all markers at the
159 same height, set to ``[0.5]``.
160
161markerscale : float, default: :rc:`legend.markerscale`
162 The relative size of legend markers compared to the originally drawn ones.
163
164markerfirst : bool, default: True
165 If *True*, legend marker is placed to the left of the legend label.
166 If *False*, legend marker is placed to the right of the legend label.
167
168reverse : bool, default: False
169 If *True*, the legend labels are displayed in reverse order from the input.
170 If *False*, the legend labels are displayed in the same order as the input.
171
172 .. versionadded:: 3.7
173
174frameon : bool, default: :rc:`legend.frameon`
175 Whether the legend should be drawn on a patch (frame).
176
177fancybox : bool, default: :rc:`legend.fancybox`
178 Whether round edges should be enabled around the `.FancyBboxPatch` which
179 makes up the legend's background.
180
181shadow : None, bool or dict, default: :rc:`legend.shadow`
182 Whether to draw a shadow behind the legend.
183 The shadow can be configured using `.Patch` keywords.
184 Customization via :rc:`legend.shadow` is currently not supported.
185
186framealpha : float, default: :rc:`legend.framealpha`
187 The alpha transparency of the legend's background.
188 If *shadow* is activated and *framealpha* is ``None``, the default value is
189 ignored.
190
191facecolor : "inherit" or color, default: :rc:`legend.facecolor`
192 The legend's background color.
193 If ``"inherit"``, use :rc:`axes.facecolor`.
194
195edgecolor : "inherit" or color, default: :rc:`legend.edgecolor`
196 The legend's background patch edge color.
197 If ``"inherit"``, use :rc:`axes.edgecolor`.
198
199mode : {"expand", None}
200 If *mode* is set to ``"expand"`` the legend will be horizontally
201 expanded to fill the Axes area (or *bbox_to_anchor* if defines
202 the legend's size).
203
204bbox_transform : None or `~matplotlib.transforms.Transform`
205 The transform for the bounding box (*bbox_to_anchor*). For a value
206 of ``None`` (default) the Axes'
207 :data:`~matplotlib.axes.Axes.transAxes` transform will be used.
208
209title : str or None
210 The legend's title. Default is no title (``None``).
211
212title_fontproperties : None or `~matplotlib.font_manager.FontProperties` or dict
213 The font properties of the legend's title. If None (default), the
214 *title_fontsize* argument will be used if present; if *title_fontsize* is
215 also None, the current :rc:`legend.title_fontsize` will be used.
216
217title_fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \
218'x-large', 'xx-large'}, default: :rc:`legend.title_fontsize`
219 The font size of the legend's title.
220 Note: This cannot be combined with *title_fontproperties*. If you want
221 to set the fontsize alongside other font properties, use the *size*
222 parameter in *title_fontproperties*.
223
224alignment : {'center', 'left', 'right'}, default: 'center'
225 The alignment of the legend title and the box of entries. The entries
226 are aligned as a single block, so that markers always lined up.
227
228borderpad : float, default: :rc:`legend.borderpad`
229 The fractional whitespace inside the legend border, in font-size units.
230
231labelspacing : float, default: :rc:`legend.labelspacing`
232 The vertical space between the legend entries, in font-size units.
233
234handlelength : float, default: :rc:`legend.handlelength`
235 The length of the legend handles, in font-size units.
236
237handleheight : float, default: :rc:`legend.handleheight`
238 The height of the legend handles, in font-size units.
239
240handletextpad : float, default: :rc:`legend.handletextpad`
241 The pad between the legend handle and text, in font-size units.
242
243borderaxespad : float, default: :rc:`legend.borderaxespad`
244 The pad between the Axes and legend border, in font-size units.
245
246columnspacing : float, default: :rc:`legend.columnspacing`
247 The spacing between columns, in font-size units.
248
249handler_map : dict or None
250 The custom dictionary mapping instances or types to a legend
251 handler. This *handler_map* updates the default handler map
252 found at `matplotlib.legend.Legend.get_legend_handler_map`.
253
254draggable : bool, default: False
255 Whether the legend can be dragged with the mouse.
256"""
257
258_loc_doc_base = """
259loc : str or pair of floats, default: {default}
260 The location of the legend.
261
262 The strings ``'upper left'``, ``'upper right'``, ``'lower left'``,
263 ``'lower right'`` place the legend at the corresponding corner of the
264 {parent}.
265
266 The strings ``'upper center'``, ``'lower center'``, ``'center left'``,
267 ``'center right'`` place the legend at the center of the corresponding edge
268 of the {parent}.
269
270 The string ``'center'`` places the legend at the center of the {parent}.
271{best}
272 The location can also be a 2-tuple giving the coordinates of the lower-left
273 corner of the legend in {parent} coordinates (in which case *bbox_to_anchor*
274 will be ignored).
275
276 For back-compatibility, ``'center right'`` (but no other location) can also
277 be spelled ``'right'``, and each "string" location can also be given as a
278 numeric value:
279
280 ================== =============
281 Location String Location Code
282 ================== =============
283 'best' (Axes only) 0
284 'upper right' 1
285 'upper left' 2
286 'lower left' 3
287 'lower right' 4
288 'right' 5
289 'center left' 6
290 'center right' 7
291 'lower center' 8
292 'upper center' 9
293 'center' 10
294 ================== =============
295 {outside}"""
296
297_loc_doc_best = """
298 The string ``'best'`` places the legend at the location, among the nine
299 locations defined so far, with the minimum overlap with other drawn
300 artists. This option can be quite slow for plots with large amounts of
301 data; your plotting speed may benefit from providing a specific location.
302"""
303
304_legend_kw_axes_st = (
305 _loc_doc_base.format(parent='axes', default=':rc:`legend.loc`',
306 best=_loc_doc_best, outside='') +
307 _legend_kw_doc_base)
308_docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st)
309
310_outside_doc = """
311 If a figure is using the constrained layout manager, the string codes
312 of the *loc* keyword argument can get better layout behaviour using the
313 prefix 'outside'. There is ambiguity at the corners, so 'outside
314 upper right' will make space for the legend above the rest of the
315 axes in the layout, and 'outside right upper' will make space on the
316 right side of the layout. In addition to the values of *loc*
317 listed above, we have 'outside right upper', 'outside right lower',
318 'outside left upper', and 'outside left lower'. See
319 :ref:`legend_guide` for more details.
320"""
321
322_legend_kw_figure_st = (
323 _loc_doc_base.format(parent='figure', default="'upper right'",
324 best='', outside=_outside_doc) +
325 _legend_kw_doc_base)
326_docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st)
327
328_legend_kw_both_st = (
329 _loc_doc_base.format(parent='axes/figure',
330 default=":rc:`legend.loc` for Axes, 'upper right' for Figure",
331 best=_loc_doc_best, outside=_outside_doc) +
332 _legend_kw_doc_base)
333_docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st)
334
335_legend_kw_set_loc_st = (
336 _loc_doc_base.format(parent='axes/figure',
337 default=":rc:`legend.loc` for Axes, 'upper right' for Figure",
338 best=_loc_doc_best, outside=_outside_doc))
339_docstring.interpd.update(_legend_kw_set_loc_doc=_legend_kw_set_loc_st)
340
341
342class Legend(Artist):
343 """
344 Place a legend on the figure/axes.
345 """
346
347 # 'best' is only implemented for Axes legends
348 codes = {'best': 0, **AnchoredOffsetbox.codes}
349 zorder = 5
350
351 def __str__(self):
352 return "Legend"
353
354 @_docstring.dedent_interpd
355 def __init__(
356 self, parent, handles, labels,
357 *,
358 loc=None,
359 numpoints=None, # number of points in the legend line
360 markerscale=None, # relative size of legend markers vs. original
361 markerfirst=True, # left/right ordering of legend marker and label
362 reverse=False, # reverse ordering of legend marker and label
363 scatterpoints=None, # number of scatter points
364 scatteryoffsets=None,
365 prop=None, # properties for the legend texts
366 fontsize=None, # keyword to set font size directly
367 labelcolor=None, # keyword to set the text color
368
369 # spacing & pad defined as a fraction of the font-size
370 borderpad=None, # whitespace inside the legend border
371 labelspacing=None, # vertical space between the legend entries
372 handlelength=None, # length of the legend handles
373 handleheight=None, # height of the legend handles
374 handletextpad=None, # pad between the legend handle and text
375 borderaxespad=None, # pad between the Axes and legend border
376 columnspacing=None, # spacing between columns
377
378 ncols=1, # number of columns
379 mode=None, # horizontal distribution of columns: None or "expand"
380
381 fancybox=None, # True: fancy box, False: rounded box, None: rcParam
382 shadow=None,
383 title=None, # legend title
384 title_fontsize=None, # legend title font size
385 framealpha=None, # set frame alpha
386 edgecolor=None, # frame patch edgecolor
387 facecolor=None, # frame patch facecolor
388
389 bbox_to_anchor=None, # bbox to which the legend will be anchored
390 bbox_transform=None, # transform for the bbox
391 frameon=None, # draw frame
392 handler_map=None,
393 title_fontproperties=None, # properties for the legend title
394 alignment="center", # control the alignment within the legend box
395 ncol=1, # synonym for ncols (backward compatibility)
396 draggable=False # whether the legend can be dragged with the mouse
397 ):
398 """
399 Parameters
400 ----------
401 parent : `~matplotlib.axes.Axes` or `.Figure`
402 The artist that contains the legend.
403
404 handles : list of (`.Artist` or tuple of `.Artist`)
405 A list of Artists (lines, patches) to be added to the legend.
406
407 labels : list of str
408 A list of labels to show next to the artists. The length of handles
409 and labels should be the same. If they are not, they are truncated
410 to the length of the shorter list.
411
412 Other Parameters
413 ----------------
414 %(_legend_kw_doc)s
415
416 Attributes
417 ----------
418 legend_handles
419 List of `.Artist` objects added as legend entries.
420
421 .. versionadded:: 3.7
422 """
423 # local import only to avoid circularity
424 from matplotlib.axes import Axes
425 from matplotlib.figure import FigureBase
426
427 super().__init__()
428
429 if prop is None:
430 self.prop = FontProperties(size=mpl._val_or_rc(fontsize, "legend.fontsize"))
431 else:
432 self.prop = FontProperties._from_any(prop)
433 if isinstance(prop, dict) and "size" not in prop:
434 self.prop.set_size(mpl.rcParams["legend.fontsize"])
435
436 self._fontsize = self.prop.get_size_in_points()
437
438 self.texts = []
439 self.legend_handles = []
440 self._legend_title_box = None
441
442 #: A dictionary with the extra handler mappings for this Legend
443 #: instance.
444 self._custom_handler_map = handler_map
445
446 self.numpoints = mpl._val_or_rc(numpoints, 'legend.numpoints')
447 self.markerscale = mpl._val_or_rc(markerscale, 'legend.markerscale')
448 self.scatterpoints = mpl._val_or_rc(scatterpoints, 'legend.scatterpoints')
449 self.borderpad = mpl._val_or_rc(borderpad, 'legend.borderpad')
450 self.labelspacing = mpl._val_or_rc(labelspacing, 'legend.labelspacing')
451 self.handlelength = mpl._val_or_rc(handlelength, 'legend.handlelength')
452 self.handleheight = mpl._val_or_rc(handleheight, 'legend.handleheight')
453 self.handletextpad = mpl._val_or_rc(handletextpad, 'legend.handletextpad')
454 self.borderaxespad = mpl._val_or_rc(borderaxespad, 'legend.borderaxespad')
455 self.columnspacing = mpl._val_or_rc(columnspacing, 'legend.columnspacing')
456 self.shadow = mpl._val_or_rc(shadow, 'legend.shadow')
457 # trim handles and labels if illegal label...
458 _lab, _hand = [], []
459 for label, handle in zip(labels, handles):
460 if isinstance(label, str) and label.startswith('_'):
461 _api.warn_deprecated("3.8", message=(
462 "An artist whose label starts with an underscore was passed to "
463 "legend(); such artists will no longer be ignored in the future. "
464 "To suppress this warning, explicitly filter out such artists, "
465 "e.g. with `[art for art in artists if not "
466 "art.get_label().startswith('_')]`."))
467 else:
468 _lab.append(label)
469 _hand.append(handle)
470 labels, handles = _lab, _hand
471
472 if reverse:
473 labels.reverse()
474 handles.reverse()
475
476 if len(handles) < 2:
477 ncols = 1
478 self._ncols = ncols if ncols != 1 else ncol
479
480 if self.numpoints <= 0:
481 raise ValueError("numpoints must be > 0; it was %d" % numpoints)
482
483 # introduce y-offset for handles of the scatter plot
484 if scatteryoffsets is None:
485 self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.])
486 else:
487 self._scatteryoffsets = np.asarray(scatteryoffsets)
488 reps = self.scatterpoints // len(self._scatteryoffsets) + 1
489 self._scatteryoffsets = np.tile(self._scatteryoffsets,
490 reps)[:self.scatterpoints]
491
492 # _legend_box is a VPacker instance that contains all
493 # legend items and will be initialized from _init_legend_box()
494 # method.
495 self._legend_box = None
496
497 if isinstance(parent, Axes):
498 self.isaxes = True
499 self.axes = parent
500 self.set_figure(parent.figure)
501 elif isinstance(parent, FigureBase):
502 self.isaxes = False
503 self.set_figure(parent)
504 else:
505 raise TypeError(
506 "Legend needs either Axes or FigureBase as parent"
507 )
508 self.parent = parent
509
510 self._mode = mode
511 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
512
513 # Figure out if self.shadow is valid
514 # If shadow was None, rcParams loads False
515 # So it shouldn't be None here
516
517 self._shadow_props = {'ox': 2, 'oy': -2} # default location offsets
518 if isinstance(self.shadow, dict):
519 self._shadow_props.update(self.shadow)
520 self.shadow = True
521 elif self.shadow in (0, 1, True, False):
522 self.shadow = bool(self.shadow)
523 else:
524 raise ValueError(
525 'Legend shadow must be a dict or bool, not '
526 f'{self.shadow!r} of type {type(self.shadow)}.'
527 )
528
529 # We use FancyBboxPatch to draw a legend frame. The location
530 # and size of the box will be updated during the drawing time.
531
532 facecolor = mpl._val_or_rc(facecolor, "legend.facecolor")
533 if facecolor == 'inherit':
534 facecolor = mpl.rcParams["axes.facecolor"]
535
536 edgecolor = mpl._val_or_rc(edgecolor, "legend.edgecolor")
537 if edgecolor == 'inherit':
538 edgecolor = mpl.rcParams["axes.edgecolor"]
539
540 fancybox = mpl._val_or_rc(fancybox, "legend.fancybox")
541
542 self.legendPatch = FancyBboxPatch(
543 xy=(0, 0), width=1, height=1,
544 facecolor=facecolor, edgecolor=edgecolor,
545 # If shadow is used, default to alpha=1 (#8943).
546 alpha=(framealpha if framealpha is not None
547 else 1 if shadow
548 else mpl.rcParams["legend.framealpha"]),
549 # The width and height of the legendPatch will be set (in draw())
550 # to the length that includes the padding. Thus we set pad=0 here.
551 boxstyle=("round,pad=0,rounding_size=0.2" if fancybox
552 else "square,pad=0"),
553 mutation_scale=self._fontsize,
554 snap=True,
555 visible=mpl._val_or_rc(frameon, "legend.frameon")
556 )
557 self._set_artist_props(self.legendPatch)
558
559 _api.check_in_list(["center", "left", "right"], alignment=alignment)
560 self._alignment = alignment
561
562 # init with null renderer
563 self._init_legend_box(handles, labels, markerfirst)
564
565 # Set legend location
566 self.set_loc(loc)
567
568 # figure out title font properties:
569 if title_fontsize is not None and title_fontproperties is not None:
570 raise ValueError(
571 "title_fontsize and title_fontproperties can't be specified "
572 "at the same time. Only use one of them. ")
573 title_prop_fp = FontProperties._from_any(title_fontproperties)
574 if isinstance(title_fontproperties, dict):
575 if "size" not in title_fontproperties:
576 title_fontsize = mpl.rcParams["legend.title_fontsize"]
577 title_prop_fp.set_size(title_fontsize)
578 elif title_fontsize is not None:
579 title_prop_fp.set_size(title_fontsize)
580 elif not isinstance(title_fontproperties, FontProperties):
581 title_fontsize = mpl.rcParams["legend.title_fontsize"]
582 title_prop_fp.set_size(title_fontsize)
583
584 self.set_title(title, prop=title_prop_fp)
585
586 self._draggable = None
587 self.set_draggable(state=draggable)
588
589 # set the text color
590
591 color_getters = { # getter function depends on line or patch
592 'linecolor': ['get_color', 'get_facecolor'],
593 'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'],
594 'mfc': ['get_markerfacecolor', 'get_facecolor'],
595 'markeredgecolor': ['get_markeredgecolor', 'get_edgecolor'],
596 'mec': ['get_markeredgecolor', 'get_edgecolor'],
597 }
598 labelcolor = mpl._val_or_rc(labelcolor, 'legend.labelcolor')
599 if labelcolor is None:
600 labelcolor = mpl.rcParams['text.color']
601 if isinstance(labelcolor, str) and labelcolor in color_getters:
602 getter_names = color_getters[labelcolor]
603 for handle, text in zip(self.legend_handles, self.texts):
604 try:
605 if handle.get_array() is not None:
606 continue
607 except AttributeError:
608 pass
609 for getter_name in getter_names:
610 try:
611 color = getattr(handle, getter_name)()
612 if isinstance(color, np.ndarray):
613 if (
614 color.shape[0] == 1
615 or np.isclose(color, color[0]).all()
616 ):
617 text.set_color(color[0])
618 else:
619 pass
620 else:
621 text.set_color(color)
622 break
623 except AttributeError:
624 pass
625 elif cbook._str_equal(labelcolor, 'none'):
626 for text in self.texts:
627 text.set_color(labelcolor)
628 elif np.iterable(labelcolor):
629 for text, color in zip(self.texts,
630 itertools.cycle(
631 colors.to_rgba_array(labelcolor))):
632 text.set_color(color)
633 else:
634 raise ValueError(f"Invalid labelcolor: {labelcolor!r}")
635
636 def _set_artist_props(self, a):
637 """
638 Set the boilerplate props for artists added to Axes.
639 """
640 a.set_figure(self.figure)
641 if self.isaxes:
642 a.axes = self.axes
643
644 a.set_transform(self.get_transform())
645
646 @_docstring.dedent_interpd
647 def set_loc(self, loc=None):
648 """
649 Set the location of the legend.
650
651 .. versionadded:: 3.8
652
653 Parameters
654 ----------
655 %(_legend_kw_set_loc_doc)s
656 """
657 loc0 = loc
658 self._loc_used_default = loc is None
659 if loc is None:
660 loc = mpl.rcParams["legend.loc"]
661 if not self.isaxes and loc in [0, 'best']:
662 loc = 'upper right'
663
664 type_err_message = ("loc must be string, coordinate tuple, or"
665 f" an integer 0-10, not {loc!r}")
666
667 # handle outside legends:
668 self._outside_loc = None
669 if isinstance(loc, str):
670 if loc.split()[0] == 'outside':
671 # strip outside:
672 loc = loc.split('outside ')[1]
673 # strip "center" at the beginning
674 self._outside_loc = loc.replace('center ', '')
675 # strip first
676 self._outside_loc = self._outside_loc.split()[0]
677 locs = loc.split()
678 if len(locs) > 1 and locs[0] in ('right', 'left'):
679 # locs doesn't accept "left upper", etc, so swap
680 if locs[0] != 'center':
681 locs = locs[::-1]
682 loc = locs[0] + ' ' + locs[1]
683 # check that loc is in acceptable strings
684 loc = _api.check_getitem(self.codes, loc=loc)
685 elif np.iterable(loc):
686 # coerce iterable into tuple
687 loc = tuple(loc)
688 # validate the tuple represents Real coordinates
689 if len(loc) != 2 or not all(isinstance(e, numbers.Real) for e in loc):
690 raise ValueError(type_err_message)
691 elif isinstance(loc, int):
692 # validate the integer represents a string numeric value
693 if loc < 0 or loc > 10:
694 raise ValueError(type_err_message)
695 else:
696 # all other cases are invalid values of loc
697 raise ValueError(type_err_message)
698
699 if self.isaxes and self._outside_loc:
700 raise ValueError(
701 f"'outside' option for loc='{loc0}' keyword argument only "
702 "works for figure legends")
703
704 if not self.isaxes and loc == 0:
705 raise ValueError(
706 "Automatic legend placement (loc='best') not implemented for "
707 "figure legend")
708
709 tmp = self._loc_used_default
710 self._set_loc(loc)
711 self._loc_used_default = tmp # ignore changes done by _set_loc
712
713 def _set_loc(self, loc):
714 # find_offset function will be provided to _legend_box and
715 # _legend_box will draw itself at the location of the return
716 # value of the find_offset.
717 self._loc_used_default = False
718 self._loc_real = loc
719 self.stale = True
720 self._legend_box.set_offset(self._findoffset)
721
722 def set_ncols(self, ncols):
723 """Set the number of columns."""
724 self._ncols = ncols
725
726 def _get_loc(self):
727 return self._loc_real
728
729 _loc = property(_get_loc, _set_loc)
730
731 def _findoffset(self, width, height, xdescent, ydescent, renderer):
732 """Helper function to locate the legend."""
733
734 if self._loc == 0: # "best".
735 x, y = self._find_best_position(width, height, renderer)
736 elif self._loc in Legend.codes.values(): # Fixed location.
737 bbox = Bbox.from_bounds(0, 0, width, height)
738 x, y = self._get_anchored_bbox(self._loc, bbox,
739 self.get_bbox_to_anchor(),
740 renderer)
741 else: # Axes or figure coordinates.
742 fx, fy = self._loc
743 bbox = self.get_bbox_to_anchor()
744 x, y = bbox.x0 + bbox.width * fx, bbox.y0 + bbox.height * fy
745
746 return x + xdescent, y + ydescent
747
748 @allow_rasterization
749 def draw(self, renderer):
750 # docstring inherited
751 if not self.get_visible():
752 return
753
754 renderer.open_group('legend', gid=self.get_gid())
755
756 fontsize = renderer.points_to_pixels(self._fontsize)
757
758 # if mode == fill, set the width of the legend_box to the
759 # width of the parent (minus pads)
760 if self._mode in ["expand"]:
761 pad = 2 * (self.borderaxespad + self.borderpad) * fontsize
762 self._legend_box.set_width(self.get_bbox_to_anchor().width - pad)
763
764 # update the location and size of the legend. This needs to
765 # be done in any case to clip the figure right.
766 bbox = self._legend_box.get_window_extent(renderer)
767 self.legendPatch.set_bounds(bbox.bounds)
768 self.legendPatch.set_mutation_scale(fontsize)
769
770 # self.shadow is validated in __init__
771 # So by here it is a bool and self._shadow_props contains any configs
772
773 if self.shadow:
774 Shadow(self.legendPatch, **self._shadow_props).draw(renderer)
775
776 self.legendPatch.draw(renderer)
777 self._legend_box.draw(renderer)
778
779 renderer.close_group('legend')
780 self.stale = False
781
782 # _default_handler_map defines the default mapping between plot
783 # elements and the legend handlers.
784
785 _default_handler_map = {
786 StemContainer: legend_handler.HandlerStem(),
787 ErrorbarContainer: legend_handler.HandlerErrorbar(),
788 Line2D: legend_handler.HandlerLine2D(),
789 Patch: legend_handler.HandlerPatch(),
790 StepPatch: legend_handler.HandlerStepPatch(),
791 LineCollection: legend_handler.HandlerLineCollection(),
792 RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
793 CircleCollection: legend_handler.HandlerCircleCollection(),
794 BarContainer: legend_handler.HandlerPatch(
795 update_func=legend_handler.update_from_first_child),
796 tuple: legend_handler.HandlerTuple(),
797 PathCollection: legend_handler.HandlerPathCollection(),
798 PolyCollection: legend_handler.HandlerPolyCollection()
799 }
800
801 # (get|set|update)_default_handler_maps are public interfaces to
802 # modify the default handler map.
803
804 @classmethod
805 def get_default_handler_map(cls):
806 """Return the global default handler map, shared by all legends."""
807 return cls._default_handler_map
808
809 @classmethod
810 def set_default_handler_map(cls, handler_map):
811 """Set the global default handler map, shared by all legends."""
812 cls._default_handler_map = handler_map
813
814 @classmethod
815 def update_default_handler_map(cls, handler_map):
816 """Update the global default handler map, shared by all legends."""
817 cls._default_handler_map.update(handler_map)
818
819 def get_legend_handler_map(self):
820 """Return this legend instance's handler map."""
821 default_handler_map = self.get_default_handler_map()
822 return ({**default_handler_map, **self._custom_handler_map}
823 if self._custom_handler_map else default_handler_map)
824
825 @staticmethod
826 def get_legend_handler(legend_handler_map, orig_handle):
827 """
828 Return a legend handler from *legend_handler_map* that
829 corresponds to *orig_handler*.
830
831 *legend_handler_map* should be a dictionary object (that is
832 returned by the get_legend_handler_map method).
833
834 It first checks if the *orig_handle* itself is a key in the
835 *legend_handler_map* and return the associated value.
836 Otherwise, it checks for each of the classes in its
837 method-resolution-order. If no matching key is found, it
838 returns ``None``.
839 """
840 try:
841 return legend_handler_map[orig_handle]
842 except (TypeError, KeyError): # TypeError if unhashable.
843 pass
844 for handle_type in type(orig_handle).mro():
845 try:
846 return legend_handler_map[handle_type]
847 except KeyError:
848 pass
849 return None
850
851 def _init_legend_box(self, handles, labels, markerfirst=True):
852 """
853 Initialize the legend_box. The legend_box is an instance of
854 the OffsetBox, which is packed with legend handles and
855 texts. Once packed, their location is calculated during the
856 drawing time.
857 """
858
859 fontsize = self._fontsize
860
861 # legend_box is a HPacker, horizontally packed with columns.
862 # Each column is a VPacker, vertically packed with legend items.
863 # Each legend item is a HPacker packed with:
864 # - handlebox: a DrawingArea which contains the legend handle.
865 # - labelbox: a TextArea which contains the legend text.
866
867 text_list = [] # the list of text instances
868 handle_list = [] # the list of handle instances
869 handles_and_labels = []
870
871 # The approximate height and descent of text. These values are
872 # only used for plotting the legend handle.
873 descent = 0.35 * fontsize * (self.handleheight - 0.7) # heuristic.
874 height = fontsize * self.handleheight - descent
875 # each handle needs to be drawn inside a box of (x, y, w, h) =
876 # (0, -descent, width, height). And their coordinates should
877 # be given in the display coordinates.
878
879 # The transformation of each handle will be automatically set
880 # to self.get_transform(). If the artist does not use its
881 # default transform (e.g., Collections), you need to
882 # manually set their transform to the self.get_transform().
883 legend_handler_map = self.get_legend_handler_map()
884
885 for orig_handle, label in zip(handles, labels):
886 handler = self.get_legend_handler(legend_handler_map, orig_handle)
887 if handler is None:
888 _api.warn_external(
889 "Legend does not support handles for "
890 f"{type(orig_handle).__name__} "
891 "instances.\nA proxy artist may be used "
892 "instead.\nSee: https://matplotlib.org/"
893 "stable/users/explain/axes/legend_guide.html"
894 "#controlling-the-legend-entries")
895 # No handle for this artist, so we just defer to None.
896 handle_list.append(None)
897 else:
898 textbox = TextArea(label, multilinebaseline=True,
899 textprops=dict(
900 verticalalignment='baseline',
901 horizontalalignment='left',
902 fontproperties=self.prop))
903 handlebox = DrawingArea(width=self.handlelength * fontsize,
904 height=height,
905 xdescent=0., ydescent=descent)
906
907 text_list.append(textbox._text)
908 # Create the artist for the legend which represents the
909 # original artist/handle.
910 handle_list.append(handler.legend_artist(self, orig_handle,
911 fontsize, handlebox))
912 handles_and_labels.append((handlebox, textbox))
913
914 columnbox = []
915 # array_split splits n handles_and_labels into ncols columns, with the
916 # first n%ncols columns having an extra entry. filter(len, ...)
917 # handles the case where n < ncols: the last ncols-n columns are empty
918 # and get filtered out.
919 for handles_and_labels_column in filter(
920 len, np.array_split(handles_and_labels, self._ncols)):
921 # pack handlebox and labelbox into itembox
922 itemboxes = [HPacker(pad=0,
923 sep=self.handletextpad * fontsize,
924 children=[h, t] if markerfirst else [t, h],
925 align="baseline")
926 for h, t in handles_and_labels_column]
927 # pack columnbox
928 alignment = "baseline" if markerfirst else "right"
929 columnbox.append(VPacker(pad=0,
930 sep=self.labelspacing * fontsize,
931 align=alignment,
932 children=itemboxes))
933
934 mode = "expand" if self._mode == "expand" else "fixed"
935 sep = self.columnspacing * fontsize
936 self._legend_handle_box = HPacker(pad=0,
937 sep=sep, align="baseline",
938 mode=mode,
939 children=columnbox)
940 self._legend_title_box = TextArea("")
941 self._legend_box = VPacker(pad=self.borderpad * fontsize,
942 sep=self.labelspacing * fontsize,
943 align=self._alignment,
944 children=[self._legend_title_box,
945 self._legend_handle_box])
946 self._legend_box.set_figure(self.figure)
947 self._legend_box.axes = self.axes
948 self.texts = text_list
949 self.legend_handles = handle_list
950
951 def _auto_legend_data(self):
952 """
953 Return display coordinates for hit testing for "best" positioning.
954
955 Returns
956 -------
957 bboxes
958 List of bounding boxes of all patches.
959 lines
960 List of `.Path` corresponding to each line.
961 offsets
962 List of (x, y) offsets of all collection.
963 """
964 assert self.isaxes # always holds, as this is only called internally
965 bboxes = []
966 lines = []
967 offsets = []
968 for artist in self.parent._children:
969 if isinstance(artist, Line2D):
970 lines.append(
971 artist.get_transform().transform_path(artist.get_path()))
972 elif isinstance(artist, Rectangle):
973 bboxes.append(
974 artist.get_bbox().transformed(artist.get_data_transform()))
975 elif isinstance(artist, Patch):
976 lines.append(
977 artist.get_transform().transform_path(artist.get_path()))
978 elif isinstance(artist, PolyCollection):
979 lines.extend(artist.get_transform().transform_path(path)
980 for path in artist.get_paths())
981 elif isinstance(artist, Collection):
982 transform, transOffset, hoffsets, _ = artist._prepare_points()
983 if len(hoffsets):
984 offsets.extend(transOffset.transform(hoffsets))
985 elif isinstance(artist, Text):
986 bboxes.append(artist.get_window_extent())
987
988 return bboxes, lines, offsets
989
990 def get_children(self):
991 # docstring inherited
992 return [self._legend_box, self.get_frame()]
993
994 def get_frame(self):
995 """Return the `~.patches.Rectangle` used to frame the legend."""
996 return self.legendPatch
997
998 def get_lines(self):
999 r"""Return the list of `~.lines.Line2D`\s in the legend."""
1000 return [h for h in self.legend_handles if isinstance(h, Line2D)]
1001
1002 def get_patches(self):
1003 r"""Return the list of `~.patches.Patch`\s in the legend."""
1004 return silent_list('Patch',
1005 [h for h in self.legend_handles
1006 if isinstance(h, Patch)])
1007
1008 def get_texts(self):
1009 r"""Return the list of `~.text.Text`\s in the legend."""
1010 return silent_list('Text', self.texts)
1011
1012 def set_alignment(self, alignment):
1013 """
1014 Set the alignment of the legend title and the box of entries.
1015
1016 The entries are aligned as a single block, so that markers always
1017 lined up.
1018
1019 Parameters
1020 ----------
1021 alignment : {'center', 'left', 'right'}.
1022
1023 """
1024 _api.check_in_list(["center", "left", "right"], alignment=alignment)
1025 self._alignment = alignment
1026 self._legend_box.align = alignment
1027
1028 def get_alignment(self):
1029 """Get the alignment value of the legend box"""
1030 return self._legend_box.align
1031
1032 def set_title(self, title, prop=None):
1033 """
1034 Set legend title and title style.
1035
1036 Parameters
1037 ----------
1038 title : str
1039 The legend title.
1040
1041 prop : `.font_manager.FontProperties` or `str` or `pathlib.Path`
1042 The font properties of the legend title.
1043 If a `str`, it is interpreted as a fontconfig pattern parsed by
1044 `.FontProperties`. If a `pathlib.Path`, it is interpreted as the
1045 absolute path to a font file.
1046
1047 """
1048 self._legend_title_box._text.set_text(title)
1049 if title:
1050 self._legend_title_box._text.set_visible(True)
1051 self._legend_title_box.set_visible(True)
1052 else:
1053 self._legend_title_box._text.set_visible(False)
1054 self._legend_title_box.set_visible(False)
1055
1056 if prop is not None:
1057 self._legend_title_box._text.set_fontproperties(prop)
1058
1059 self.stale = True
1060
1061 def get_title(self):
1062 """Return the `.Text` instance for the legend title."""
1063 return self._legend_title_box._text
1064
1065 def get_window_extent(self, renderer=None):
1066 # docstring inherited
1067 if renderer is None:
1068 renderer = self.figure._get_renderer()
1069 return self._legend_box.get_window_extent(renderer=renderer)
1070
1071 def get_tightbbox(self, renderer=None):
1072 # docstring inherited
1073 return self._legend_box.get_window_extent(renderer)
1074
1075 def get_frame_on(self):
1076 """Get whether the legend box patch is drawn."""
1077 return self.legendPatch.get_visible()
1078
1079 def set_frame_on(self, b):
1080 """
1081 Set whether the legend box patch is drawn.
1082
1083 Parameters
1084 ----------
1085 b : bool
1086 """
1087 self.legendPatch.set_visible(b)
1088 self.stale = True
1089
1090 draw_frame = set_frame_on # Backcompat alias.
1091
1092 def get_bbox_to_anchor(self):
1093 """Return the bbox that the legend will be anchored to."""
1094 if self._bbox_to_anchor is None:
1095 return self.parent.bbox
1096 else:
1097 return self._bbox_to_anchor
1098
1099 def set_bbox_to_anchor(self, bbox, transform=None):
1100 """
1101 Set the bbox that the legend will be anchored to.
1102
1103 Parameters
1104 ----------
1105 bbox : `~matplotlib.transforms.BboxBase` or tuple
1106 The bounding box can be specified in the following ways:
1107
1108 - A `.BboxBase` instance
1109 - A tuple of ``(left, bottom, width, height)`` in the given
1110 transform (normalized axes coordinate if None)
1111 - A tuple of ``(left, bottom)`` where the width and height will be
1112 assumed to be zero.
1113 - *None*, to remove the bbox anchoring, and use the parent bbox.
1114
1115 transform : `~matplotlib.transforms.Transform`, optional
1116 A transform to apply to the bounding box. If not specified, this
1117 will use a transform to the bounding box of the parent.
1118 """
1119 if bbox is None:
1120 self._bbox_to_anchor = None
1121 return
1122 elif isinstance(bbox, BboxBase):
1123 self._bbox_to_anchor = bbox
1124 else:
1125 try:
1126 l = len(bbox)
1127 except TypeError as err:
1128 raise ValueError(f"Invalid bbox: {bbox}") from err
1129
1130 if l == 2:
1131 bbox = [bbox[0], bbox[1], 0, 0]
1132
1133 self._bbox_to_anchor = Bbox.from_bounds(*bbox)
1134
1135 if transform is None:
1136 transform = BboxTransformTo(self.parent.bbox)
1137
1138 self._bbox_to_anchor = TransformedBbox(self._bbox_to_anchor,
1139 transform)
1140 self.stale = True
1141
1142 def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
1143 """
1144 Place the *bbox* inside the *parentbbox* according to a given
1145 location code. Return the (x, y) coordinate of the bbox.
1146
1147 Parameters
1148 ----------
1149 loc : int
1150 A location code in range(1, 11). This corresponds to the possible
1151 values for ``self._loc``, excluding "best".
1152 bbox : `~matplotlib.transforms.Bbox`
1153 bbox to be placed, in display coordinates.
1154 parentbbox : `~matplotlib.transforms.Bbox`
1155 A parent box which will contain the bbox, in display coordinates.
1156 """
1157 return offsetbox._get_anchored_bbox(
1158 loc, bbox, parentbbox,
1159 self.borderaxespad * renderer.points_to_pixels(self._fontsize))
1160
1161 def _find_best_position(self, width, height, renderer):
1162 """Determine the best location to place the legend."""
1163 assert self.isaxes # always holds, as this is only called internally
1164
1165 start_time = time.perf_counter()
1166
1167 bboxes, lines, offsets = self._auto_legend_data()
1168
1169 bbox = Bbox.from_bounds(0, 0, width, height)
1170
1171 candidates = []
1172 for idx in range(1, len(self.codes)):
1173 l, b = self._get_anchored_bbox(idx, bbox,
1174 self.get_bbox_to_anchor(),
1175 renderer)
1176 legendBox = Bbox.from_bounds(l, b, width, height)
1177 # XXX TODO: If markers are present, it would be good to take them
1178 # into account when checking vertex overlaps in the next line.
1179 badness = (sum(legendBox.count_contains(line.vertices)
1180 for line in lines)
1181 + legendBox.count_contains(offsets)
1182 + legendBox.count_overlaps(bboxes)
1183 + sum(line.intersects_bbox(legendBox, filled=False)
1184 for line in lines))
1185 # Include the index to favor lower codes in case of a tie.
1186 candidates.append((badness, idx, (l, b)))
1187 if badness == 0:
1188 break
1189
1190 _, _, (l, b) = min(candidates)
1191
1192 if self._loc_used_default and time.perf_counter() - start_time > 1:
1193 _api.warn_external(
1194 'Creating legend with loc="best" can be slow with large '
1195 'amounts of data.')
1196
1197 return l, b
1198
1199 @_api.rename_parameter("3.8", "event", "mouseevent")
1200 def contains(self, mouseevent):
1201 return self.legendPatch.contains(mouseevent)
1202
1203 def set_draggable(self, state, use_blit=False, update='loc'):
1204 """
1205 Enable or disable mouse dragging support of the legend.
1206
1207 Parameters
1208 ----------
1209 state : bool
1210 Whether mouse dragging is enabled.
1211 use_blit : bool, optional
1212 Use blitting for faster image composition. For details see
1213 :ref:`func-animation`.
1214 update : {'loc', 'bbox'}, optional
1215 The legend parameter to be changed when dragged:
1216
1217 - 'loc': update the *loc* parameter of the legend
1218 - 'bbox': update the *bbox_to_anchor* parameter of the legend
1219
1220 Returns
1221 -------
1222 `.DraggableLegend` or *None*
1223 If *state* is ``True`` this returns the `.DraggableLegend` helper
1224 instance. Otherwise this returns *None*.
1225 """
1226 if state:
1227 if self._draggable is None:
1228 self._draggable = DraggableLegend(self,
1229 use_blit,
1230 update=update)
1231 else:
1232 if self._draggable is not None:
1233 self._draggable.disconnect()
1234 self._draggable = None
1235 return self._draggable
1236
1237 def get_draggable(self):
1238 """Return ``True`` if the legend is draggable, ``False`` otherwise."""
1239 return self._draggable is not None
1240
1241
1242# Helper functions to parse legend arguments for both `figure.legend` and
1243# `axes.legend`:
1244def _get_legend_handles(axs, legend_handler_map=None):
1245 """Yield artists that can be used as handles in a legend."""
1246 handles_original = []
1247 for ax in axs:
1248 handles_original += [
1249 *(a for a in ax._children
1250 if isinstance(a, (Line2D, Patch, Collection, Text))),
1251 *ax.containers]
1252 # support parasite Axes:
1253 if hasattr(ax, 'parasites'):
1254 for axx in ax.parasites:
1255 handles_original += [
1256 *(a for a in axx._children
1257 if isinstance(a, (Line2D, Patch, Collection, Text))),
1258 *axx.containers]
1259
1260 handler_map = {**Legend.get_default_handler_map(),
1261 **(legend_handler_map or {})}
1262 has_handler = Legend.get_legend_handler
1263 for handle in handles_original:
1264 label = handle.get_label()
1265 if label != '_nolegend_' and has_handler(handler_map, handle):
1266 yield handle
1267 elif (label and not label.startswith('_') and
1268 not has_handler(handler_map, handle)):
1269 _api.warn_external(
1270 "Legend does not support handles for "
1271 f"{type(handle).__name__} "
1272 "instances.\nSee: https://matplotlib.org/stable/"
1273 "tutorials/intermediate/legend_guide.html"
1274 "#implementing-a-custom-legend-handler")
1275 continue
1276
1277
1278def _get_legend_handles_labels(axs, legend_handler_map=None):
1279 """Return handles and labels for legend."""
1280 handles = []
1281 labels = []
1282 for handle in _get_legend_handles(axs, legend_handler_map):
1283 label = handle.get_label()
1284 if label and not label.startswith('_'):
1285 handles.append(handle)
1286 labels.append(label)
1287 return handles, labels
1288
1289
1290def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs):
1291 """
1292 Get the handles and labels from the calls to either ``figure.legend``
1293 or ``axes.legend``.
1294
1295 The parser is a bit involved because we support::
1296
1297 legend()
1298 legend(labels)
1299 legend(handles, labels)
1300 legend(labels=labels)
1301 legend(handles=handles)
1302 legend(handles=handles, labels=labels)
1303
1304 The behavior for a mixture of positional and keyword handles and labels
1305 is undefined and issues a warning; it will be an error in the future.
1306
1307 Parameters
1308 ----------
1309 axs : list of `.Axes`
1310 If handles are not given explicitly, the artists in these Axes are
1311 used as handles.
1312 *args : tuple
1313 Positional parameters passed to ``legend()``.
1314 handles
1315 The value of the keyword argument ``legend(handles=...)``, or *None*
1316 if that keyword argument was not used.
1317 labels
1318 The value of the keyword argument ``legend(labels=...)``, or *None*
1319 if that keyword argument was not used.
1320 **kwargs
1321 All other keyword arguments passed to ``legend()``.
1322
1323 Returns
1324 -------
1325 handles : list of (`.Artist` or tuple of `.Artist`)
1326 The legend handles.
1327 labels : list of str
1328 The legend labels.
1329 kwargs : dict
1330 *kwargs* with keywords handles and labels removed.
1331
1332 """
1333 log = logging.getLogger(__name__)
1334
1335 handlers = kwargs.get('handler_map')
1336
1337 if (handles is not None or labels is not None) and args:
1338 _api.warn_deprecated("3.9", message=(
1339 "You have mixed positional and keyword arguments, some input may "
1340 "be discarded. This is deprecated since %(since)s and will "
1341 "become an error %(removal)s."))
1342
1343 if (hasattr(handles, "__len__") and
1344 hasattr(labels, "__len__") and
1345 len(handles) != len(labels)):
1346 _api.warn_external(f"Mismatched number of handles and labels: "
1347 f"len(handles) = {len(handles)} "
1348 f"len(labels) = {len(labels)}")
1349 # if got both handles and labels as kwargs, make same length
1350 if handles and labels:
1351 handles, labels = zip(*zip(handles, labels))
1352
1353 elif handles is not None and labels is None:
1354 labels = [handle.get_label() for handle in handles]
1355
1356 elif labels is not None and handles is None:
1357 # Get as many handles as there are labels.
1358 handles = [handle for handle, label
1359 in zip(_get_legend_handles(axs, handlers), labels)]
1360
1361 elif len(args) == 0: # 0 args: automatically detect labels and handles.
1362 handles, labels = _get_legend_handles_labels(axs, handlers)
1363 if not handles:
1364 _api.warn_external(
1365 "No artists with labels found to put in legend. Note that "
1366 "artists whose label start with an underscore are ignored "
1367 "when legend() is called with no argument.")
1368
1369 elif len(args) == 1: # 1 arg: user defined labels, automatic handle detection.
1370 labels, = args
1371 if any(isinstance(l, Artist) for l in labels):
1372 raise TypeError("A single argument passed to legend() must be a "
1373 "list of labels, but found an Artist in there.")
1374
1375 # Get as many handles as there are labels.
1376 handles = [handle for handle, label
1377 in zip(_get_legend_handles(axs, handlers), labels)]
1378
1379 elif len(args) == 2: # 2 args: user defined handles and labels.
1380 handles, labels = args[:2]
1381
1382 else:
1383 raise _api.nargs_error('legend', '0-2', len(args))
1384
1385 return handles, labels, kwargs