Coverage for /usr/lib/python3/dist-packages/matplotlib/legend.py: 17%
434 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1"""
2The legend module defines the Legend class, which is responsible for
3drawing legends associated with axes and/or figures.
5.. important::
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 :doc:`legend guide
10 </tutorials/intermediate/legend_guide>`.
12The `Legend` class is a container of legend handles and legend texts.
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.
20See the :doc:`legend guide </tutorials/intermediate/legend_guide>` for more
21information.
22"""
24import itertools
25import logging
26import time
28import numpy as np
30import matplotlib as mpl
31from matplotlib import _api, _docstring, colors, offsetbox
32from matplotlib.artist import Artist, allow_rasterization
33from matplotlib.cbook import silent_list
34from matplotlib.font_manager import FontProperties
35from matplotlib.lines import Line2D
36from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch,
37 StepPatch)
38from matplotlib.collections import (
39 Collection, CircleCollection, LineCollection, PathCollection,
40 PolyCollection, RegularPolyCollection)
41from matplotlib.text import Text
42from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
43from matplotlib.transforms import BboxTransformTo, BboxTransformFrom
44from matplotlib.offsetbox import (
45 AnchoredOffsetbox, DraggableOffsetBox,
46 HPacker, VPacker,
47 DrawingArea, TextArea,
48)
49from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer
50from . import legend_handler
53class DraggableLegend(DraggableOffsetBox):
54 def __init__(self, legend, use_blit=False, update="loc"):
55 """
56 Wrapper around a `.Legend` to support mouse dragging.
58 Parameters
59 ----------
60 legend : `.Legend`
61 The `.Legend` instance to wrap.
62 use_blit : bool, optional
63 Use blitting for faster image composition. For details see
64 :ref:`func-animation`.
65 update : {'loc', 'bbox'}, optional
66 If "loc", update the *loc* parameter of the legend upon finalizing.
67 If "bbox", update the *bbox_to_anchor* parameter.
68 """
69 self.legend = legend
71 _api.check_in_list(["loc", "bbox"], update=update)
72 self._update = update
74 super().__init__(legend, legend._legend_box, use_blit=use_blit)
76 def finalize_offset(self):
77 if self._update == "loc":
78 self._update_loc(self.get_loc_in_canvas())
79 elif self._update == "bbox":
80 self._bbox_to_anchor(self.get_loc_in_canvas())
82 def _update_loc(self, loc_in_canvas):
83 bbox = self.legend.get_bbox_to_anchor()
84 # if bbox has zero width or height, the transformation is
85 # ill-defined. Fall back to the default bbox_to_anchor.
86 if bbox.width == 0 or bbox.height == 0:
87 self.legend.set_bbox_to_anchor(None)
88 bbox = self.legend.get_bbox_to_anchor()
89 _bbox_transform = BboxTransformFrom(bbox)
90 self.legend._loc = tuple(_bbox_transform.transform(loc_in_canvas))
92 def _update_bbox_to_anchor(self, loc_in_canvas):
93 loc_in_bbox = self.legend.axes.transAxes.transform(loc_in_canvas)
94 self.legend.set_bbox_to_anchor(loc_in_bbox)
97_docstring.interpd.update(_legend_kw_doc="""
98loc : str or pair of floats, default: :rc:`legend.loc` ('best' for axes, \
99'upper right' for figures)
100 The location of the legend.
102 The strings
103 ``'upper left', 'upper right', 'lower left', 'lower right'``
104 place the legend at the corresponding corner of the axes/figure.
106 The strings
107 ``'upper center', 'lower center', 'center left', 'center right'``
108 place the legend at the center of the corresponding edge of the
109 axes/figure.
111 The string ``'center'`` places the legend at the center of the axes/figure.
113 The string ``'best'`` places the legend at the location, among the nine
114 locations defined so far, with the minimum overlap with other drawn
115 artists. This option can be quite slow for plots with large amounts of
116 data; your plotting speed may benefit from providing a specific location.
118 The location can also be a 2-tuple giving the coordinates of the lower-left
119 corner of the legend in axes coordinates (in which case *bbox_to_anchor*
120 will be ignored).
122 For back-compatibility, ``'center right'`` (but no other location) can also
123 be spelled ``'right'``, and each "string" locations can also be given as a
124 numeric value:
126 =============== =============
127 Location String Location Code
128 =============== =============
129 'best' 0
130 'upper right' 1
131 'upper left' 2
132 'lower left' 3
133 'lower right' 4
134 'right' 5
135 'center left' 6
136 'center right' 7
137 'lower center' 8
138 'upper center' 9
139 'center' 10
140 =============== =============
142bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
143 Box that is used to position the legend in conjunction with *loc*.
144 Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or
145 `figure.bbox` (if `.Figure.legend`). This argument allows arbitrary
146 placement of the legend.
148 Bbox coordinates are interpreted in the coordinate system given by
149 *bbox_transform*, with the default transform
150 Axes or Figure coordinates, depending on which ``legend`` is called.
152 If a 4-tuple or `.BboxBase` is given, then it specifies the bbox
153 ``(x, y, width, height)`` that the legend is placed in.
154 To put the legend in the best location in the bottom right
155 quadrant of the axes (or figure)::
157 loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.5)
159 A 2-tuple ``(x, y)`` places the corner of the legend specified by *loc* at
160 x, y. For example, to put the legend's upper right-hand corner in the
161 center of the axes (or figure) the following keywords can be used::
163 loc='upper right', bbox_to_anchor=(0.5, 0.5)
165ncols : int, default: 1
166 The number of columns that the legend has.
168 For backward compatibility, the spelling *ncol* is also supported
169 but it is discouraged. If both are given, *ncols* takes precedence.
171prop : None or `matplotlib.font_manager.FontProperties` or dict
172 The font properties of the legend. If None (default), the current
173 :data:`matplotlib.rcParams` will be used.
175fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \
176'x-large', 'xx-large'}
177 The font size of the legend. If the value is numeric the size will be the
178 absolute font size in points. String values are relative to the current
179 default font size. This argument is only used if *prop* is not specified.
181labelcolor : str or list, default: :rc:`legend.labelcolor`
182 The color of the text in the legend. Either a valid color string
183 (for example, 'red'), or a list of color strings. The labelcolor can
184 also be made to match the color of the line or marker using 'linecolor',
185 'markerfacecolor' (or 'mfc'), or 'markeredgecolor' (or 'mec').
187 Labelcolor can be set globally using :rc:`legend.labelcolor`. If None,
188 use :rc:`text.color`.
190numpoints : int, default: :rc:`legend.numpoints`
191 The number of marker points in the legend when creating a legend
192 entry for a `.Line2D` (line).
194scatterpoints : int, default: :rc:`legend.scatterpoints`
195 The number of marker points in the legend when creating
196 a legend entry for a `.PathCollection` (scatter plot).
198scatteryoffsets : iterable of floats, default: ``[0.375, 0.5, 0.3125]``
199 The vertical offset (relative to the font size) for the markers
200 created for a scatter plot legend entry. 0.0 is at the base the
201 legend text, and 1.0 is at the top. To draw all markers at the
202 same height, set to ``[0.5]``.
204markerscale : float, default: :rc:`legend.markerscale`
205 The relative size of legend markers compared with the originally
206 drawn ones.
208markerfirst : bool, default: True
209 If *True*, legend marker is placed to the left of the legend label.
210 If *False*, legend marker is placed to the right of the legend label.
212frameon : bool, default: :rc:`legend.frameon`
213 Whether the legend should be drawn on a patch (frame).
215fancybox : bool, default: :rc:`legend.fancybox`
216 Whether round edges should be enabled around the `.FancyBboxPatch` which
217 makes up the legend's background.
219shadow : bool, default: :rc:`legend.shadow`
220 Whether to draw a shadow behind the legend.
222framealpha : float, default: :rc:`legend.framealpha`
223 The alpha transparency of the legend's background.
224 If *shadow* is activated and *framealpha* is ``None``, the default value is
225 ignored.
227facecolor : "inherit" or color, default: :rc:`legend.facecolor`
228 The legend's background color.
229 If ``"inherit"``, use :rc:`axes.facecolor`.
231edgecolor : "inherit" or color, default: :rc:`legend.edgecolor`
232 The legend's background patch edge color.
233 If ``"inherit"``, use take :rc:`axes.edgecolor`.
235mode : {"expand", None}
236 If *mode* is set to ``"expand"`` the legend will be horizontally
237 expanded to fill the axes area (or *bbox_to_anchor* if defines
238 the legend's size).
240bbox_transform : None or `matplotlib.transforms.Transform`
241 The transform for the bounding box (*bbox_to_anchor*). For a value
242 of ``None`` (default) the Axes'
243 :data:`~matplotlib.axes.Axes.transAxes` transform will be used.
245title : str or None
246 The legend's title. Default is no title (``None``).
248title_fontproperties : None or `matplotlib.font_manager.FontProperties` or dict
249 The font properties of the legend's title. If None (default), the
250 *title_fontsize* argument will be used if present; if *title_fontsize* is
251 also None, the current :rc:`legend.title_fontsize` will be used.
253title_fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \
254'x-large', 'xx-large'}, default: :rc:`legend.title_fontsize`
255 The font size of the legend's title.
256 Note: This cannot be combined with *title_fontproperties*. If you want
257 to set the fontsize alongside other font properties, use the *size*
258 parameter in *title_fontproperties*.
260alignment : {'center', 'left', 'right'}, default: 'center'
261 The alignment of the legend title and the box of entries. The entries
262 are aligned as a single block, so that markers always lined up.
264borderpad : float, default: :rc:`legend.borderpad`
265 The fractional whitespace inside the legend border, in font-size units.
267labelspacing : float, default: :rc:`legend.labelspacing`
268 The vertical space between the legend entries, in font-size units.
270handlelength : float, default: :rc:`legend.handlelength`
271 The length of the legend handles, in font-size units.
273handleheight : float, default: :rc:`legend.handleheight`
274 The height of the legend handles, in font-size units.
276handletextpad : float, default: :rc:`legend.handletextpad`
277 The pad between the legend handle and text, in font-size units.
279borderaxespad : float, default: :rc:`legend.borderaxespad`
280 The pad between the axes and legend border, in font-size units.
282columnspacing : float, default: :rc:`legend.columnspacing`
283 The spacing between columns, in font-size units.
285handler_map : dict or None
286 The custom dictionary mapping instances or types to a legend
287 handler. This *handler_map* updates the default handler map
288 found at `matplotlib.legend.Legend.get_legend_handler_map`.
289""")
292class Legend(Artist):
293 """
294 Place a legend on the axes at location loc.
295 """
297 # 'best' is only implemented for axes legends
298 codes = {'best': 0, **AnchoredOffsetbox.codes}
299 zorder = 5
301 def __str__(self):
302 return "Legend"
304 @_api.make_keyword_only("3.6", "loc")
305 @_docstring.dedent_interpd
306 def __init__(
307 self, parent, handles, labels,
308 loc=None,
309 numpoints=None, # number of points in the legend line
310 markerscale=None, # relative size of legend markers vs. original
311 markerfirst=True, # left/right ordering of legend marker and label
312 scatterpoints=None, # number of scatter points
313 scatteryoffsets=None,
314 prop=None, # properties for the legend texts
315 fontsize=None, # keyword to set font size directly
316 labelcolor=None, # keyword to set the text color
318 # spacing & pad defined as a fraction of the font-size
319 borderpad=None, # whitespace inside the legend border
320 labelspacing=None, # vertical space between the legend entries
321 handlelength=None, # length of the legend handles
322 handleheight=None, # height of the legend handles
323 handletextpad=None, # pad between the legend handle and text
324 borderaxespad=None, # pad between the axes and legend border
325 columnspacing=None, # spacing between columns
327 ncols=1, # number of columns
328 mode=None, # horizontal distribution of columns: None or "expand"
330 fancybox=None, # True: fancy box, False: rounded box, None: rcParam
331 shadow=None,
332 title=None, # legend title
333 title_fontsize=None, # legend title font size
334 framealpha=None, # set frame alpha
335 edgecolor=None, # frame patch edgecolor
336 facecolor=None, # frame patch facecolor
338 bbox_to_anchor=None, # bbox to which the legend will be anchored
339 bbox_transform=None, # transform for the bbox
340 frameon=None, # draw frame
341 handler_map=None,
342 title_fontproperties=None, # properties for the legend title
343 alignment="center", # control the alignment within the legend box
344 *,
345 ncol=1 # synonym for ncols (backward compatibility)
346 ):
347 """
348 Parameters
349 ----------
350 parent : `~matplotlib.axes.Axes` or `.Figure`
351 The artist that contains the legend.
353 handles : list of `.Artist`
354 A list of Artists (lines, patches) to be added to the legend.
356 labels : list of str
357 A list of labels to show next to the artists. The length of handles
358 and labels should be the same. If they are not, they are truncated
359 to the smaller of both lengths.
361 Other Parameters
362 ----------------
363 %(_legend_kw_doc)s
365 Notes
366 -----
367 Users can specify any arbitrary location for the legend using the
368 *bbox_to_anchor* keyword argument. *bbox_to_anchor* can be a
369 `.BboxBase` (or derived there from) or a tuple of 2 or 4 floats.
370 See `set_bbox_to_anchor` for more detail.
372 The legend location can be specified by setting *loc* with a tuple of
373 2 floats, which is interpreted as the lower-left corner of the legend
374 in the normalized axes coordinate.
375 """
376 # local import only to avoid circularity
377 from matplotlib.axes import Axes
378 from matplotlib.figure import FigureBase
380 super().__init__()
382 if prop is None:
383 if fontsize is not None:
384 self.prop = FontProperties(size=fontsize)
385 else:
386 self.prop = FontProperties(
387 size=mpl.rcParams["legend.fontsize"])
388 else:
389 self.prop = FontProperties._from_any(prop)
390 if isinstance(prop, dict) and "size" not in prop:
391 self.prop.set_size(mpl.rcParams["legend.fontsize"])
393 self._fontsize = self.prop.get_size_in_points()
395 self.texts = []
396 self.legendHandles = []
397 self._legend_title_box = None
399 #: A dictionary with the extra handler mappings for this Legend
400 #: instance.
401 self._custom_handler_map = handler_map
403 def val_or_rc(val, rc_name):
404 return val if val is not None else mpl.rcParams[rc_name]
406 self.numpoints = val_or_rc(numpoints, 'legend.numpoints')
407 self.markerscale = val_or_rc(markerscale, 'legend.markerscale')
408 self.scatterpoints = val_or_rc(scatterpoints, 'legend.scatterpoints')
409 self.borderpad = val_or_rc(borderpad, 'legend.borderpad')
410 self.labelspacing = val_or_rc(labelspacing, 'legend.labelspacing')
411 self.handlelength = val_or_rc(handlelength, 'legend.handlelength')
412 self.handleheight = val_or_rc(handleheight, 'legend.handleheight')
413 self.handletextpad = val_or_rc(handletextpad, 'legend.handletextpad')
414 self.borderaxespad = val_or_rc(borderaxespad, 'legend.borderaxespad')
415 self.columnspacing = val_or_rc(columnspacing, 'legend.columnspacing')
416 self.shadow = val_or_rc(shadow, 'legend.shadow')
417 # trim handles and labels if illegal label...
418 _lab, _hand = [], []
419 for label, handle in zip(labels, handles):
420 if isinstance(label, str) and label.startswith('_'):
421 _api.warn_external(f"The label {label!r} of {handle!r} starts "
422 "with '_'. It is thus excluded from the "
423 "legend.")
424 else:
425 _lab.append(label)
426 _hand.append(handle)
427 labels, handles = _lab, _hand
429 handles = list(handles)
430 if len(handles) < 2:
431 ncols = 1
432 self._ncols = ncols if ncols != 1 else ncol
434 if self.numpoints <= 0:
435 raise ValueError("numpoints must be > 0; it was %d" % numpoints)
437 # introduce y-offset for handles of the scatter plot
438 if scatteryoffsets is None:
439 self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.])
440 else:
441 self._scatteryoffsets = np.asarray(scatteryoffsets)
442 reps = self.scatterpoints // len(self._scatteryoffsets) + 1
443 self._scatteryoffsets = np.tile(self._scatteryoffsets,
444 reps)[:self.scatterpoints]
446 # _legend_box is a VPacker instance that contains all
447 # legend items and will be initialized from _init_legend_box()
448 # method.
449 self._legend_box = None
451 if isinstance(parent, Axes):
452 self.isaxes = True
453 self.axes = parent
454 self.set_figure(parent.figure)
455 elif isinstance(parent, FigureBase):
456 self.isaxes = False
457 self.set_figure(parent)
458 else:
459 raise TypeError(
460 "Legend needs either Axes or FigureBase as parent"
461 )
462 self.parent = parent
464 self._loc_used_default = loc is None
465 if loc is None:
466 loc = mpl.rcParams["legend.loc"]
467 if not self.isaxes and loc in [0, 'best']:
468 loc = 'upper right'
469 if isinstance(loc, str):
470 loc = _api.check_getitem(self.codes, loc=loc)
471 if not self.isaxes and loc == 0:
472 raise ValueError(
473 "Automatic legend placement (loc='best') not implemented for "
474 "figure legend")
476 self._mode = mode
477 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
479 # We use FancyBboxPatch to draw a legend frame. The location
480 # and size of the box will be updated during the drawing time.
482 if facecolor is None:
483 facecolor = mpl.rcParams["legend.facecolor"]
484 if facecolor == 'inherit':
485 facecolor = mpl.rcParams["axes.facecolor"]
487 if edgecolor is None:
488 edgecolor = mpl.rcParams["legend.edgecolor"]
489 if edgecolor == 'inherit':
490 edgecolor = mpl.rcParams["axes.edgecolor"]
492 if fancybox is None:
493 fancybox = mpl.rcParams["legend.fancybox"]
495 self.legendPatch = FancyBboxPatch(
496 xy=(0, 0), width=1, height=1,
497 facecolor=facecolor, edgecolor=edgecolor,
498 # If shadow is used, default to alpha=1 (#8943).
499 alpha=(framealpha if framealpha is not None
500 else 1 if shadow
501 else mpl.rcParams["legend.framealpha"]),
502 # The width and height of the legendPatch will be set (in draw())
503 # to the length that includes the padding. Thus we set pad=0 here.
504 boxstyle=("round,pad=0,rounding_size=0.2" if fancybox
505 else "square,pad=0"),
506 mutation_scale=self._fontsize,
507 snap=True,
508 visible=(frameon if frameon is not None
509 else mpl.rcParams["legend.frameon"])
510 )
511 self._set_artist_props(self.legendPatch)
513 _api.check_in_list(["center", "left", "right"], alignment=alignment)
514 self._alignment = alignment
516 # init with null renderer
517 self._init_legend_box(handles, labels, markerfirst)
519 tmp = self._loc_used_default
520 self._set_loc(loc)
521 self._loc_used_default = tmp # ignore changes done by _set_loc
523 # figure out title font properties:
524 if title_fontsize is not None and title_fontproperties is not None:
525 raise ValueError(
526 "title_fontsize and title_fontproperties can't be specified "
527 "at the same time. Only use one of them. ")
528 title_prop_fp = FontProperties._from_any(title_fontproperties)
529 if isinstance(title_fontproperties, dict):
530 if "size" not in title_fontproperties:
531 title_fontsize = mpl.rcParams["legend.title_fontsize"]
532 title_prop_fp.set_size(title_fontsize)
533 elif title_fontsize is not None:
534 title_prop_fp.set_size(title_fontsize)
535 elif not isinstance(title_fontproperties, FontProperties):
536 title_fontsize = mpl.rcParams["legend.title_fontsize"]
537 title_prop_fp.set_size(title_fontsize)
539 self.set_title(title, prop=title_prop_fp)
540 self._draggable = None
542 # set the text color
544 color_getters = { # getter function depends on line or patch
545 'linecolor': ['get_color', 'get_facecolor'],
546 'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'],
547 'mfc': ['get_markerfacecolor', 'get_facecolor'],
548 'markeredgecolor': ['get_markeredgecolor', 'get_edgecolor'],
549 'mec': ['get_markeredgecolor', 'get_edgecolor'],
550 }
551 if labelcolor is None:
552 if mpl.rcParams['legend.labelcolor'] is not None:
553 labelcolor = mpl.rcParams['legend.labelcolor']
554 else:
555 labelcolor = mpl.rcParams['text.color']
556 if isinstance(labelcolor, str) and labelcolor in color_getters:
557 getter_names = color_getters[labelcolor]
558 for handle, text in zip(self.legendHandles, self.texts):
559 for getter_name in getter_names:
560 try:
561 color = getattr(handle, getter_name)()
562 text.set_color(color)
563 break
564 except AttributeError:
565 pass
566 elif isinstance(labelcolor, str) and labelcolor == 'none':
567 for text in self.texts:
568 text.set_color(labelcolor)
569 elif np.iterable(labelcolor):
570 for text, color in zip(self.texts,
571 itertools.cycle(
572 colors.to_rgba_array(labelcolor))):
573 text.set_color(color)
574 else:
575 raise ValueError(f"Invalid labelcolor: {labelcolor!r}")
577 def _set_artist_props(self, a):
578 """
579 Set the boilerplate props for artists added to axes.
580 """
581 a.set_figure(self.figure)
582 if self.isaxes:
583 # a.set_axes(self.axes)
584 a.axes = self.axes
586 a.set_transform(self.get_transform())
588 def _set_loc(self, loc):
589 # find_offset function will be provided to _legend_box and
590 # _legend_box will draw itself at the location of the return
591 # value of the find_offset.
592 self._loc_used_default = False
593 self._loc_real = loc
594 self.stale = True
595 self._legend_box.set_offset(self._findoffset)
597 def set_ncols(self, ncols):
598 """Set the number of columns."""
599 self._ncols = ncols
601 def _get_loc(self):
602 return self._loc_real
604 _loc = property(_get_loc, _set_loc)
606 def _findoffset(self, width, height, xdescent, ydescent, renderer):
607 """Helper function to locate the legend."""
609 if self._loc == 0: # "best".
610 x, y = self._find_best_position(width, height, renderer)
611 elif self._loc in Legend.codes.values(): # Fixed location.
612 bbox = Bbox.from_bounds(0, 0, width, height)
613 x, y = self._get_anchored_bbox(self._loc, bbox,
614 self.get_bbox_to_anchor(),
615 renderer)
616 else: # Axes or figure coordinates.
617 fx, fy = self._loc
618 bbox = self.get_bbox_to_anchor()
619 x, y = bbox.x0 + bbox.width * fx, bbox.y0 + bbox.height * fy
621 return x + xdescent, y + ydescent
623 @allow_rasterization
624 def draw(self, renderer):
625 # docstring inherited
626 if not self.get_visible():
627 return
629 renderer.open_group('legend', gid=self.get_gid())
631 fontsize = renderer.points_to_pixels(self._fontsize)
633 # if mode == fill, set the width of the legend_box to the
634 # width of the parent (minus pads)
635 if self._mode in ["expand"]:
636 pad = 2 * (self.borderaxespad + self.borderpad) * fontsize
637 self._legend_box.set_width(self.get_bbox_to_anchor().width - pad)
639 # update the location and size of the legend. This needs to
640 # be done in any case to clip the figure right.
641 bbox = self._legend_box.get_window_extent(renderer)
642 self.legendPatch.set_bounds(bbox.bounds)
643 self.legendPatch.set_mutation_scale(fontsize)
645 if self.shadow:
646 Shadow(self.legendPatch, 2, -2).draw(renderer)
648 self.legendPatch.draw(renderer)
649 self._legend_box.draw(renderer)
651 renderer.close_group('legend')
652 self.stale = False
654 # _default_handler_map defines the default mapping between plot
655 # elements and the legend handlers.
657 _default_handler_map = {
658 StemContainer: legend_handler.HandlerStem(),
659 ErrorbarContainer: legend_handler.HandlerErrorbar(),
660 Line2D: legend_handler.HandlerLine2D(),
661 Patch: legend_handler.HandlerPatch(),
662 StepPatch: legend_handler.HandlerStepPatch(),
663 LineCollection: legend_handler.HandlerLineCollection(),
664 RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
665 CircleCollection: legend_handler.HandlerCircleCollection(),
666 BarContainer: legend_handler.HandlerPatch(
667 update_func=legend_handler.update_from_first_child),
668 tuple: legend_handler.HandlerTuple(),
669 PathCollection: legend_handler.HandlerPathCollection(),
670 PolyCollection: legend_handler.HandlerPolyCollection()
671 }
673 # (get|set|update)_default_handler_maps are public interfaces to
674 # modify the default handler map.
676 @classmethod
677 def get_default_handler_map(cls):
678 """Return the global default handler map, shared by all legends."""
679 return cls._default_handler_map
681 @classmethod
682 def set_default_handler_map(cls, handler_map):
683 """Set the global default handler map, shared by all legends."""
684 cls._default_handler_map = handler_map
686 @classmethod
687 def update_default_handler_map(cls, handler_map):
688 """Update the global default handler map, shared by all legends."""
689 cls._default_handler_map.update(handler_map)
691 def get_legend_handler_map(self):
692 """Return this legend instance's handler map."""
693 default_handler_map = self.get_default_handler_map()
694 return ({**default_handler_map, **self._custom_handler_map}
695 if self._custom_handler_map else default_handler_map)
697 @staticmethod
698 def get_legend_handler(legend_handler_map, orig_handle):
699 """
700 Return a legend handler from *legend_handler_map* that
701 corresponds to *orig_handler*.
703 *legend_handler_map* should be a dictionary object (that is
704 returned by the get_legend_handler_map method).
706 It first checks if the *orig_handle* itself is a key in the
707 *legend_handler_map* and return the associated value.
708 Otherwise, it checks for each of the classes in its
709 method-resolution-order. If no matching key is found, it
710 returns ``None``.
711 """
712 try:
713 return legend_handler_map[orig_handle]
714 except (TypeError, KeyError): # TypeError if unhashable.
715 pass
716 for handle_type in type(orig_handle).mro():
717 try:
718 return legend_handler_map[handle_type]
719 except KeyError:
720 pass
721 return None
723 def _init_legend_box(self, handles, labels, markerfirst=True):
724 """
725 Initialize the legend_box. The legend_box is an instance of
726 the OffsetBox, which is packed with legend handles and
727 texts. Once packed, their location is calculated during the
728 drawing time.
729 """
731 fontsize = self._fontsize
733 # legend_box is a HPacker, horizontally packed with columns.
734 # Each column is a VPacker, vertically packed with legend items.
735 # Each legend item is a HPacker packed with:
736 # - handlebox: a DrawingArea which contains the legend handle.
737 # - labelbox: a TextArea which contains the legend text.
739 text_list = [] # the list of text instances
740 handle_list = [] # the list of handle instances
741 handles_and_labels = []
743 # The approximate height and descent of text. These values are
744 # only used for plotting the legend handle.
745 descent = 0.35 * fontsize * (self.handleheight - 0.7) # heuristic.
746 height = fontsize * self.handleheight - descent
747 # each handle needs to be drawn inside a box of (x, y, w, h) =
748 # (0, -descent, width, height). And their coordinates should
749 # be given in the display coordinates.
751 # The transformation of each handle will be automatically set
752 # to self.get_transform(). If the artist does not use its
753 # default transform (e.g., Collections), you need to
754 # manually set their transform to the self.get_transform().
755 legend_handler_map = self.get_legend_handler_map()
757 for orig_handle, label in zip(handles, labels):
758 handler = self.get_legend_handler(legend_handler_map, orig_handle)
759 if handler is None:
760 _api.warn_external(
761 "Legend does not support handles for {0} "
762 "instances.\nA proxy artist may be used "
763 "instead.\nSee: https://matplotlib.org/"
764 "stable/tutorials/intermediate/legend_guide.html"
765 "#controlling-the-legend-entries".format(
766 type(orig_handle).__name__))
767 # No handle for this artist, so we just defer to None.
768 handle_list.append(None)
769 else:
770 textbox = TextArea(label, multilinebaseline=True,
771 textprops=dict(
772 verticalalignment='baseline',
773 horizontalalignment='left',
774 fontproperties=self.prop))
775 handlebox = DrawingArea(width=self.handlelength * fontsize,
776 height=height,
777 xdescent=0., ydescent=descent)
779 text_list.append(textbox._text)
780 # Create the artist for the legend which represents the
781 # original artist/handle.
782 handle_list.append(handler.legend_artist(self, orig_handle,
783 fontsize, handlebox))
784 handles_and_labels.append((handlebox, textbox))
786 columnbox = []
787 # array_split splits n handles_and_labels into ncols columns, with the
788 # first n%ncols columns having an extra entry. filter(len, ...)
789 # handles the case where n < ncols: the last ncols-n columns are empty
790 # and get filtered out.
791 for handles_and_labels_column in filter(
792 len, np.array_split(handles_and_labels, self._ncols)):
793 # pack handlebox and labelbox into itembox
794 itemboxes = [HPacker(pad=0,
795 sep=self.handletextpad * fontsize,
796 children=[h, t] if markerfirst else [t, h],
797 align="baseline")
798 for h, t in handles_and_labels_column]
799 # pack columnbox
800 alignment = "baseline" if markerfirst else "right"
801 columnbox.append(VPacker(pad=0,
802 sep=self.labelspacing * fontsize,
803 align=alignment,
804 children=itemboxes))
806 mode = "expand" if self._mode == "expand" else "fixed"
807 sep = self.columnspacing * fontsize
808 self._legend_handle_box = HPacker(pad=0,
809 sep=sep, align="baseline",
810 mode=mode,
811 children=columnbox)
812 self._legend_title_box = TextArea("")
813 self._legend_box = VPacker(pad=self.borderpad * fontsize,
814 sep=self.labelspacing * fontsize,
815 align=self._alignment,
816 children=[self._legend_title_box,
817 self._legend_handle_box])
818 self._legend_box.set_figure(self.figure)
819 self._legend_box.axes = self.axes
820 self.texts = text_list
821 self.legendHandles = handle_list
823 def _auto_legend_data(self):
824 """
825 Return display coordinates for hit testing for "best" positioning.
827 Returns
828 -------
829 bboxes
830 List of bounding boxes of all patches.
831 lines
832 List of `.Path` corresponding to each line.
833 offsets
834 List of (x, y) offsets of all collection.
835 """
836 assert self.isaxes # always holds, as this is only called internally
837 bboxes = []
838 lines = []
839 offsets = []
840 for artist in self.parent._children:
841 if isinstance(artist, Line2D):
842 lines.append(
843 artist.get_transform().transform_path(artist.get_path()))
844 elif isinstance(artist, Rectangle):
845 bboxes.append(
846 artist.get_bbox().transformed(artist.get_data_transform()))
847 elif isinstance(artist, Patch):
848 bboxes.append(
849 artist.get_path().get_extents(artist.get_transform()))
850 elif isinstance(artist, Collection):
851 _, offset_trf, hoffsets, _ = artist._prepare_points()
852 for offset in offset_trf.transform(hoffsets):
853 offsets.append(offset)
854 return bboxes, lines, offsets
856 def get_children(self):
857 # docstring inherited
858 return [self._legend_box, self.get_frame()]
860 def get_frame(self):
861 """Return the `~.patches.Rectangle` used to frame the legend."""
862 return self.legendPatch
864 def get_lines(self):
865 r"""Return the list of `~.lines.Line2D`\s in the legend."""
866 return [h for h in self.legendHandles if isinstance(h, Line2D)]
868 def get_patches(self):
869 r"""Return the list of `~.patches.Patch`\s in the legend."""
870 return silent_list('Patch',
871 [h for h in self.legendHandles
872 if isinstance(h, Patch)])
874 def get_texts(self):
875 r"""Return the list of `~.text.Text`\s in the legend."""
876 return silent_list('Text', self.texts)
878 def set_alignment(self, alignment):
879 """
880 Set the alignment of the legend title and the box of entries.
882 The entries are aligned as a single block, so that markers always
883 lined up.
885 Parameters
886 ----------
887 alignment : {'center', 'left', 'right'}.
889 """
890 _api.check_in_list(["center", "left", "right"], alignment=alignment)
891 self._alignment = alignment
892 self._legend_box.align = alignment
894 def get_alignment(self):
895 """Get the alignment value of the legend box"""
896 return self._legend_box.align
898 def set_title(self, title, prop=None):
899 """
900 Set legend title and title style.
902 Parameters
903 ----------
904 title : str
905 The legend title.
907 prop : `.font_manager.FontProperties` or `str` or `pathlib.Path`
908 The font properties of the legend title.
909 If a `str`, it is interpreted as a fontconfig pattern parsed by
910 `.FontProperties`. If a `pathlib.Path`, it is interpreted as the
911 absolute path to a font file.
913 """
914 self._legend_title_box._text.set_text(title)
915 if title:
916 self._legend_title_box._text.set_visible(True)
917 self._legend_title_box.set_visible(True)
918 else:
919 self._legend_title_box._text.set_visible(False)
920 self._legend_title_box.set_visible(False)
922 if prop is not None:
923 self._legend_title_box._text.set_fontproperties(prop)
925 self.stale = True
927 def get_title(self):
928 """Return the `.Text` instance for the legend title."""
929 return self._legend_title_box._text
931 def get_window_extent(self, renderer=None):
932 # docstring inherited
933 if renderer is None:
934 renderer = self.figure._get_renderer()
935 return self._legend_box.get_window_extent(renderer=renderer)
937 def get_tightbbox(self, renderer=None):
938 # docstring inherited
939 return self._legend_box.get_window_extent(renderer)
941 def get_frame_on(self):
942 """Get whether the legend box patch is drawn."""
943 return self.legendPatch.get_visible()
945 def set_frame_on(self, b):
946 """
947 Set whether the legend box patch is drawn.
949 Parameters
950 ----------
951 b : bool
952 """
953 self.legendPatch.set_visible(b)
954 self.stale = True
956 draw_frame = set_frame_on # Backcompat alias.
958 def get_bbox_to_anchor(self):
959 """Return the bbox that the legend will be anchored to."""
960 if self._bbox_to_anchor is None:
961 return self.parent.bbox
962 else:
963 return self._bbox_to_anchor
965 def set_bbox_to_anchor(self, bbox, transform=None):
966 """
967 Set the bbox that the legend will be anchored to.
969 Parameters
970 ----------
971 bbox : `~matplotlib.transforms.BboxBase` or tuple
972 The bounding box can be specified in the following ways:
974 - A `.BboxBase` instance
975 - A tuple of ``(left, bottom, width, height)`` in the given
976 transform (normalized axes coordinate if None)
977 - A tuple of ``(left, bottom)`` where the width and height will be
978 assumed to be zero.
979 - *None*, to remove the bbox anchoring, and use the parent bbox.
981 transform : `~matplotlib.transforms.Transform`, optional
982 A transform to apply to the bounding box. If not specified, this
983 will use a transform to the bounding box of the parent.
984 """
985 if bbox is None:
986 self._bbox_to_anchor = None
987 return
988 elif isinstance(bbox, BboxBase):
989 self._bbox_to_anchor = bbox
990 else:
991 try:
992 l = len(bbox)
993 except TypeError as err:
994 raise ValueError(f"Invalid bbox: {bbox}") from err
996 if l == 2:
997 bbox = [bbox[0], bbox[1], 0, 0]
999 self._bbox_to_anchor = Bbox.from_bounds(*bbox)
1001 if transform is None:
1002 transform = BboxTransformTo(self.parent.bbox)
1004 self._bbox_to_anchor = TransformedBbox(self._bbox_to_anchor,
1005 transform)
1006 self.stale = True
1008 def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
1009 """
1010 Place the *bbox* inside the *parentbbox* according to a given
1011 location code. Return the (x, y) coordinate of the bbox.
1013 Parameters
1014 ----------
1015 loc : int
1016 A location code in range(1, 11). This corresponds to the possible
1017 values for ``self._loc``, excluding "best".
1018 bbox : `~matplotlib.transforms.Bbox`
1019 bbox to be placed, in display coordinates.
1020 parentbbox : `~matplotlib.transforms.Bbox`
1021 A parent box which will contain the bbox, in display coordinates.
1022 """
1023 return offsetbox._get_anchored_bbox(
1024 loc, bbox, parentbbox,
1025 self.borderaxespad * renderer.points_to_pixels(self._fontsize))
1027 def _find_best_position(self, width, height, renderer, consider=None):
1028 """
1029 Determine the best location to place the legend.
1031 *consider* is a list of ``(x, y)`` pairs to consider as a potential
1032 lower-left corner of the legend. All are display coords.
1033 """
1034 assert self.isaxes # always holds, as this is only called internally
1036 start_time = time.perf_counter()
1038 bboxes, lines, offsets = self._auto_legend_data()
1040 bbox = Bbox.from_bounds(0, 0, width, height)
1041 if consider is None:
1042 consider = [self._get_anchored_bbox(x, bbox,
1043 self.get_bbox_to_anchor(),
1044 renderer)
1045 for x in range(1, len(self.codes))]
1047 candidates = []
1048 for idx, (l, b) in enumerate(consider):
1049 legendBox = Bbox.from_bounds(l, b, width, height)
1050 badness = 0
1051 # XXX TODO: If markers are present, it would be good to take them
1052 # into account when checking vertex overlaps in the next line.
1053 badness = (sum(legendBox.count_contains(line.vertices)
1054 for line in lines)
1055 + legendBox.count_contains(offsets)
1056 + legendBox.count_overlaps(bboxes)
1057 + sum(line.intersects_bbox(legendBox, filled=False)
1058 for line in lines))
1059 if badness == 0:
1060 return l, b
1061 # Include the index to favor lower codes in case of a tie.
1062 candidates.append((badness, idx, (l, b)))
1064 _, _, (l, b) = min(candidates)
1066 if self._loc_used_default and time.perf_counter() - start_time > 1:
1067 _api.warn_external(
1068 'Creating legend with loc="best" can be slow with large '
1069 'amounts of data.')
1071 return l, b
1073 def contains(self, event):
1074 inside, info = self._default_contains(event)
1075 if inside is not None:
1076 return inside, info
1077 return self.legendPatch.contains(event)
1079 def set_draggable(self, state, use_blit=False, update='loc'):
1080 """
1081 Enable or disable mouse dragging support of the legend.
1083 Parameters
1084 ----------
1085 state : bool
1086 Whether mouse dragging is enabled.
1087 use_blit : bool, optional
1088 Use blitting for faster image composition. For details see
1089 :ref:`func-animation`.
1090 update : {'loc', 'bbox'}, optional
1091 The legend parameter to be changed when dragged:
1093 - 'loc': update the *loc* parameter of the legend
1094 - 'bbox': update the *bbox_to_anchor* parameter of the legend
1096 Returns
1097 -------
1098 `.DraggableLegend` or *None*
1099 If *state* is ``True`` this returns the `.DraggableLegend` helper
1100 instance. Otherwise this returns *None*.
1101 """
1102 if state:
1103 if self._draggable is None:
1104 self._draggable = DraggableLegend(self,
1105 use_blit,
1106 update=update)
1107 else:
1108 if self._draggable is not None:
1109 self._draggable.disconnect()
1110 self._draggable = None
1111 return self._draggable
1113 def get_draggable(self):
1114 """Return ``True`` if the legend is draggable, ``False`` otherwise."""
1115 return self._draggable is not None
1118# Helper functions to parse legend arguments for both `figure.legend` and
1119# `axes.legend`:
1120def _get_legend_handles(axs, legend_handler_map=None):
1121 """Yield artists that can be used as handles in a legend."""
1122 handles_original = []
1123 for ax in axs:
1124 handles_original += [
1125 *(a for a in ax._children
1126 if isinstance(a, (Line2D, Patch, Collection, Text))),
1127 *ax.containers]
1128 # support parasite axes:
1129 if hasattr(ax, 'parasites'):
1130 for axx in ax.parasites:
1131 handles_original += [
1132 *(a for a in axx._children
1133 if isinstance(a, (Line2D, Patch, Collection, Text))),
1134 *axx.containers]
1136 handler_map = {**Legend.get_default_handler_map(),
1137 **(legend_handler_map or {})}
1138 has_handler = Legend.get_legend_handler
1139 for handle in handles_original:
1140 label = handle.get_label()
1141 if label != '_nolegend_' and has_handler(handler_map, handle):
1142 yield handle
1143 elif (label and not label.startswith('_') and
1144 not has_handler(handler_map, handle)):
1145 _api.warn_external(
1146 "Legend does not support handles for {0} "
1147 "instances.\nSee: https://matplotlib.org/stable/"
1148 "tutorials/intermediate/legend_guide.html"
1149 "#implementing-a-custom-legend-handler".format(
1150 type(handle).__name__))
1151 continue
1154def _get_legend_handles_labels(axs, legend_handler_map=None):
1155 """Return handles and labels for legend."""
1156 handles = []
1157 labels = []
1158 for handle in _get_legend_handles(axs, legend_handler_map):
1159 label = handle.get_label()
1160 if label and not label.startswith('_'):
1161 handles.append(handle)
1162 labels.append(label)
1163 return handles, labels
1166def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs):
1167 """
1168 Get the handles and labels from the calls to either ``figure.legend``
1169 or ``axes.legend``.
1171 The parser is a bit involved because we support::
1173 legend()
1174 legend(labels)
1175 legend(handles, labels)
1176 legend(labels=labels)
1177 legend(handles=handles)
1178 legend(handles=handles, labels=labels)
1180 The behavior for a mixture of positional and keyword handles and labels
1181 is undefined and issues a warning.
1183 Parameters
1184 ----------
1185 axs : list of `.Axes`
1186 If handles are not given explicitly, the artists in these Axes are
1187 used as handles.
1188 *args : tuple
1189 Positional parameters passed to ``legend()``.
1190 handles
1191 The value of the keyword argument ``legend(handles=...)``, or *None*
1192 if that keyword argument was not used.
1193 labels
1194 The value of the keyword argument ``legend(labels=...)``, or *None*
1195 if that keyword argument was not used.
1196 **kwargs
1197 All other keyword arguments passed to ``legend()``.
1199 Returns
1200 -------
1201 handles : list of `.Artist`
1202 The legend handles.
1203 labels : list of str
1204 The legend labels.
1205 extra_args : tuple
1206 *args* with positional handles and labels removed.
1207 kwargs : dict
1208 *kwargs* with keywords handles and labels removed.
1210 """
1211 log = logging.getLogger(__name__)
1213 handlers = kwargs.get('handler_map')
1214 extra_args = ()
1216 if (handles is not None or labels is not None) and args:
1217 _api.warn_external("You have mixed positional and keyword arguments, "
1218 "some input may be discarded.")
1220 # if got both handles and labels as kwargs, make same length
1221 if handles and labels:
1222 handles, labels = zip(*zip(handles, labels))
1224 elif handles is not None and labels is None:
1225 labels = [handle.get_label() for handle in handles]
1227 elif labels is not None and handles is None:
1228 # Get as many handles as there are labels.
1229 handles = [handle for handle, label
1230 in zip(_get_legend_handles(axs, handlers), labels)]
1232 # No arguments - automatically detect labels and handles.
1233 elif len(args) == 0:
1234 handles, labels = _get_legend_handles_labels(axs, handlers)
1235 if not handles:
1236 log.warning(
1237 "No artists with labels found to put in legend. Note that "
1238 "artists whose label start with an underscore are ignored "
1239 "when legend() is called with no argument.")
1241 # One argument. User defined labels - automatic handle detection.
1242 elif len(args) == 1:
1243 labels, = args
1244 if any(isinstance(l, Artist) for l in labels):
1245 raise TypeError("A single argument passed to legend() must be a "
1246 "list of labels, but found an Artist in there.")
1248 # Get as many handles as there are labels.
1249 handles = [handle for handle, label
1250 in zip(_get_legend_handles(axs, handlers), labels)]
1252 # Two arguments:
1253 # * user defined handles and labels
1254 elif len(args) >= 2:
1255 handles, labels = args[:2]
1256 extra_args = args[2:]
1258 else:
1259 raise TypeError('Invalid arguments to legend.')
1261 return handles, labels, extra_args, kwargs