Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/legend.py : 15%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
8 manually. Most users would normally create a legend via the
9 :meth:`~matplotlib.axes.Axes.legend` function. For more details on legends
10 there is also a :doc:`legend guide </tutorials/intermediate/legend_guide>`.
12The Legend class can be considered as a container of legend handles and
13legend texts. Creation of corresponding legend handles from the plot elements
14in the axes or figures (e.g., lines, patches, etc.) are specified by the
15handler map, which defines the mapping between the plot elements and the
16legend handlers to be used (the default legend handlers are defined in the
17:mod:`~matplotlib.legend_handler` module). Note that not all kinds of
18artist are supported by the legend yet by default but it is possible to
19extend the legend handler's capabilities to support arbitrary objects. See
20the :doc:`legend guide </tutorials/intermediate/legend_guide>` for more
21information.
22"""
24import logging
25import time
27import numpy as np
29from matplotlib import rcParams
30from matplotlib import cbook, docstring
31from matplotlib.artist import Artist, allow_rasterization
32from matplotlib.cbook import silent_list
33from matplotlib.font_manager import FontProperties
34from matplotlib.lines import Line2D
35from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch
36from matplotlib.collections import (LineCollection, RegularPolyCollection,
37 CircleCollection, PathCollection,
38 PolyCollection)
39from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
40from matplotlib.transforms import BboxTransformTo, BboxTransformFrom
42from matplotlib.offsetbox import HPacker, VPacker, TextArea, DrawingArea
43from matplotlib.offsetbox import DraggableOffsetBox
45from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer
46from . import legend_handler
49class DraggableLegend(DraggableOffsetBox):
50 def __init__(self, legend, use_blit=False, update="loc"):
51 """
52 Wrapper around a `.Legend` to support mouse dragging.
54 Parameters
55 ----------
56 legend : `.Legend`
57 The `.Legend` instance to wrap.
58 use_blit : bool, optional
59 Use blitting for faster image composition. For details see
60 :ref:`func-animation`.
61 update : {'loc', 'bbox'}, optional
62 If "loc", update the *loc* parameter of the legend upon finalizing.
63 If "bbox", update the *bbox_to_anchor* parameter.
64 """
65 self.legend = legend
67 cbook._check_in_list(["loc", "bbox"], update=update)
68 self._update = update
70 DraggableOffsetBox.__init__(self, legend, legend._legend_box,
71 use_blit=use_blit)
73 def artist_picker(self, legend, evt):
74 return self.legend.contains(evt)
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_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)
165ncol : integer
166 The number of columns that the legend has. Default is 1.
168prop : None or :class:`matplotlib.font_manager.FontProperties` or dict
169 The font properties of the legend. If None (default), the current
170 :data:`matplotlib.rcParams` will be used.
172fontsize : int or float or {'xx-small', 'x-small', 'small', 'medium', \
173'large', 'x-large', 'xx-large'}
174 The font size of the legend. If the value is numeric the size will be the
175 absolute font size in points. String values are relative to the current
176 default font size. This argument is only used if *prop* is not specified.
178numpoints : None or int
179 The number of marker points in the legend when creating a legend
180 entry for a `.Line2D` (line).
181 Default is ``None``, which means using :rc:`legend.numpoints`.
183scatterpoints : None or int
184 The number of marker points in the legend when creating
185 a legend entry for a `.PathCollection` (scatter plot).
186 Default is ``None``, which means using :rc:`legend.scatterpoints`.
188scatteryoffsets : iterable of floats
189 The vertical offset (relative to the font size) for the markers
190 created for a scatter plot legend entry. 0.0 is at the base the
191 legend text, and 1.0 is at the top. To draw all markers at the
192 same height, set to ``[0.5]``. Default is ``[0.375, 0.5, 0.3125]``.
194markerscale : None or int or float
195 The relative size of legend markers compared with the originally
196 drawn ones.
197 Default is ``None``, which means using :rc:`legend.markerscale`.
199markerfirst : bool
200 If *True*, legend marker is placed to the left of the legend label.
201 If *False*, legend marker is placed to the right of the legend
202 label.
203 Default is *True*.
205frameon : None or bool
206 Whether the legend should be drawn on a patch (frame).
207 Default is ``None``, which means using :rc:`legend.frameon`.
209fancybox : None or bool
210 Whether round edges should be enabled around the `~.FancyBboxPatch` which
211 makes up the legend's background.
212 Default is ``None``, which means using :rc:`legend.fancybox`.
214shadow : None or bool
215 Whether to draw a shadow behind the legend.
216 Default is ``None``, which means using :rc:`legend.shadow`.
218framealpha : None or float
219 The alpha transparency of the legend's background.
220 Default is ``None``, which means using :rc:`legend.framealpha`.
221 If *shadow* is activated and *framealpha* is ``None``, the default value is
222 ignored.
224facecolor : None or "inherit" or color
225 The legend's background color.
226 Default is ``None``, which means using :rc:`legend.facecolor`.
227 If ``"inherit"``, use :rc:`axes.facecolor`.
229edgecolor : None or "inherit" or color
230 The legend's background patch edge color.
231 Default is ``None``, which means using :rc:`legend.edgecolor`.
232 If ``"inherit"``, use take :rc:`axes.edgecolor`.
234mode : {"expand", None}
235 If *mode* is set to ``"expand"`` the legend will be horizontally
236 expanded to fill the axes area (or `bbox_to_anchor` if defines
237 the legend's size).
239bbox_transform : None or :class:`matplotlib.transforms.Transform`
240 The transform for the bounding box (`bbox_to_anchor`). For a value
241 of ``None`` (default) the Axes'
242 :data:`~matplotlib.axes.Axes.transAxes` transform will be used.
244title : str or None
245 The legend's title. Default is no title (``None``).
247title_fontsize: str or None
248 The fontsize of the legend's title. Default is the default fontsize.
250borderpad : float or None
251 The fractional whitespace inside the legend border, in font-size units.
252 Default is ``None``, which means using :rc:`legend.borderpad`.
254labelspacing : float or None
255 The vertical space between the legend entries, in font-size units.
256 Default is ``None``, which means using :rc:`legend.labelspacing`.
258handlelength : float or None
259 The length of the legend handles, in font-size units.
260 Default is ``None``, which means using :rc:`legend.handlelength`.
262handletextpad : float or None
263 The pad between the legend handle and text, in font-size units.
264 Default is ``None``, which means using :rc:`legend.handletextpad`.
266borderaxespad : float or None
267 The pad between the axes and legend border, in font-size units.
268 Default is ``None``, which means using :rc:`legend.borderaxespad`.
270columnspacing : float or None
271 The spacing between columns, in font-size units.
272 Default is ``None``, which means using :rc:`legend.columnspacing`.
274handler_map : dict or None
275 The custom dictionary mapping instances or types to a legend
276 handler. This `handler_map` updates the default handler map
277 found at :func:`matplotlib.legend.Legend.get_legend_handler_map`.
279'''
280docstring.interpd.update(_legend_kw_doc=_legend_kw_doc)
283class Legend(Artist):
284 """
285 Place a legend on the axes at location loc.
287 """
288 codes = {'best': 0, # only implemented for axes legends
289 'upper right': 1,
290 'upper left': 2,
291 'lower left': 3,
292 'lower right': 4,
293 'right': 5,
294 'center left': 6,
295 'center right': 7,
296 'lower center': 8,
297 'upper center': 9,
298 'center': 10,
299 }
301 zorder = 5
303 def __str__(self):
304 return "Legend"
306 @docstring.dedent_interpd
307 def __init__(self, parent, handles, labels,
308 loc=None,
309 numpoints=None, # the number of points in the legend line
310 markerscale=None, # the relative size of legend markers
311 # vs. original
312 markerfirst=True, # controls ordering (left-to-right) of
313 # legend marker and label
314 scatterpoints=None, # number of scatter points
315 scatteryoffsets=None,
316 prop=None, # properties for the legend texts
317 fontsize=None, # keyword to set font size directly
319 # spacing & pad defined as a fraction of the font-size
320 borderpad=None, # the whitespace inside the legend border
321 labelspacing=None, # the vertical space between the legend
322 # entries
323 handlelength=None, # the length of the legend handles
324 handleheight=None, # the height of the legend handles
325 handletextpad=None, # the pad between the legend handle
326 # and text
327 borderaxespad=None, # the pad between the axes and legend
328 # border
329 columnspacing=None, # spacing between columns
331 ncol=1, # number of columns
332 mode=None, # mode for horizontal distribution of columns.
333 # None, "expand"
335 fancybox=None, # True use a fancy box, false use a rounded
336 # box, none use rc
337 shadow=None,
338 title=None, # set a title for the legend
339 title_fontsize=None, # set to ax.fontsize if None
340 framealpha=None, # set frame alpha
341 edgecolor=None, # frame patch edgecolor
342 facecolor=None, # frame patch facecolor
344 bbox_to_anchor=None, # bbox that the legend will be anchored.
345 bbox_transform=None, # transform for the bbox
346 frameon=None, # draw frame
347 handler_map=None,
348 ):
349 """
350 Parameters
351 ----------
352 parent : `~matplotlib.axes.Axes` or `.Figure`
353 The artist that contains the legend.
355 handles : list of `.Artist`
356 A list of Artists (lines, patches) to be added to the legend.
358 labels : list of str
359 A list of labels to show next to the artists. The length of handles
360 and labels should be the same. If they are not, they are truncated
361 to the smaller of both lengths.
363 Other Parameters
364 ----------------
365 %(_legend_kw_doc)s
367 Notes
368 -----
369 Users can specify any arbitrary location for the legend using the
370 *bbox_to_anchor* keyword argument. *bbox_to_anchor* can be a
371 `.BboxBase` (or derived therefrom) or a tuple of 2 or 4 floats.
372 See :meth:`set_bbox_to_anchor` for more detail.
374 The legend location can be specified by setting *loc* with a tuple of
375 2 floats, which is interpreted as the lower-left corner of the legend
376 in the normalized axes coordinate.
377 """
378 # local import only to avoid circularity
379 from matplotlib.axes import Axes
380 from matplotlib.figure import Figure
382 Artist.__init__(self)
384 if prop is None:
385 if fontsize is not None:
386 self.prop = FontProperties(size=fontsize)
387 else:
388 self.prop = FontProperties(size=rcParams["legend.fontsize"])
389 elif isinstance(prop, dict):
390 self.prop = FontProperties(**prop)
391 if "size" not in prop:
392 self.prop.set_size(rcParams["legend.fontsize"])
393 else:
394 self.prop = prop
396 self._fontsize = self.prop.get_size_in_points()
398 self.texts = []
399 self.legendHandles = []
400 self._legend_title_box = None
402 #: A dictionary with the extra handler mappings for this Legend
403 #: instance.
404 self._custom_handler_map = handler_map
406 locals_view = locals()
407 for name in ["numpoints", "markerscale", "shadow", "columnspacing",
408 "scatterpoints", "handleheight", 'borderpad',
409 'labelspacing', 'handlelength', 'handletextpad',
410 'borderaxespad']:
411 if locals_view[name] is None:
412 value = rcParams["legend." + name]
413 else:
414 value = locals_view[name]
415 setattr(self, name, value)
416 del locals_view
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 cbook._warn_external('The handle {!r} has a label of {!r} '
422 'which cannot be automatically added to'
423 ' the legend.'.format(handle, label))
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 ncol = 1
432 self._ncol = 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 an OffsetBox 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, Figure):
456 self.isaxes = False
457 self.set_figure(parent)
458 else:
459 raise TypeError("Legend needs either Axes or Figure as parent")
460 self.parent = parent
462 self._loc_used_default = loc is None
463 if loc is None:
464 loc = rcParams["legend.loc"]
465 if not self.isaxes and loc in [0, 'best']:
466 loc = 'upper right'
467 if isinstance(loc, str):
468 if loc not in self.codes:
469 if self.isaxes:
470 cbook.warn_deprecated(
471 "3.1", message="Unrecognized location {!r}. Falling "
472 "back on 'best'; valid locations are\n\t{}\n"
473 "This will raise an exception %(removal)s."
474 .format(loc, '\n\t'.join(self.codes)))
475 loc = 0
476 else:
477 cbook.warn_deprecated(
478 "3.1", message="Unrecognized location {!r}. Falling "
479 "back on 'upper right'; valid locations are\n\t{}\n'"
480 "This will raise an exception %(removal)s."
481 .format(loc, '\n\t'.join(self.codes)))
482 loc = 1
483 else:
484 loc = self.codes[loc]
485 if not self.isaxes and loc == 0:
486 cbook.warn_deprecated(
487 "3.1", message="Automatic legend placement (loc='best') not "
488 "implemented for figure legend. Falling back on 'upper "
489 "right'. This will raise an exception %(removal)s.")
490 loc = 1
492 self._mode = mode
493 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
495 # We use FancyBboxPatch to draw a legend frame. The location
496 # and size of the box will be updated during the drawing time.
498 if facecolor is None:
499 facecolor = rcParams["legend.facecolor"]
500 if facecolor == 'inherit':
501 facecolor = rcParams["axes.facecolor"]
503 if edgecolor is None:
504 edgecolor = rcParams["legend.edgecolor"]
505 if edgecolor == 'inherit':
506 edgecolor = rcParams["axes.edgecolor"]
508 self.legendPatch = FancyBboxPatch(
509 xy=(0.0, 0.0), width=1., height=1.,
510 facecolor=facecolor,
511 edgecolor=edgecolor,
512 mutation_scale=self._fontsize,
513 snap=True
514 )
516 # The width and height of the legendPatch will be set (in the
517 # draw()) to the length that includes the padding. Thus we set
518 # pad=0 here.
519 if fancybox is None:
520 fancybox = rcParams["legend.fancybox"]
522 if fancybox:
523 self.legendPatch.set_boxstyle("round", pad=0,
524 rounding_size=0.2)
525 else:
526 self.legendPatch.set_boxstyle("square", pad=0)
528 self._set_artist_props(self.legendPatch)
530 self._drawFrame = frameon
531 if frameon is None:
532 self._drawFrame = rcParams["legend.frameon"]
534 # init with null renderer
535 self._init_legend_box(handles, labels, markerfirst)
537 # If shadow is activated use framealpha if not
538 # explicitly passed. See Issue 8943
539 if framealpha is None:
540 if shadow:
541 self.get_frame().set_alpha(1)
542 else:
543 self.get_frame().set_alpha(rcParams["legend.framealpha"])
544 else:
545 self.get_frame().set_alpha(framealpha)
547 tmp = self._loc_used_default
548 self._set_loc(loc)
549 self._loc_used_default = tmp # ignore changes done by _set_loc
551 # figure out title fontsize:
552 if title_fontsize is None:
553 title_fontsize = rcParams['legend.title_fontsize']
554 tprop = FontProperties(size=title_fontsize)
555 self.set_title(title, prop=tprop)
556 self._draggable = None
558 def _set_artist_props(self, a):
559 """
560 Set the boilerplate props for artists added to axes.
561 """
562 a.set_figure(self.figure)
563 if self.isaxes:
564 # a.set_axes(self.axes)
565 a.axes = self.axes
567 a.set_transform(self.get_transform())
569 def _set_loc(self, loc):
570 # find_offset function will be provided to _legend_box and
571 # _legend_box will draw itself at the location of the return
572 # value of the find_offset.
573 self._loc_used_default = False
574 self._loc_real = loc
575 self.stale = True
576 self._legend_box.set_offset(self._findoffset)
578 def _get_loc(self):
579 return self._loc_real
581 _loc = property(_get_loc, _set_loc)
583 def _findoffset(self, width, height, xdescent, ydescent, renderer):
584 "Helper function to locate the legend."
586 if self._loc == 0: # "best".
587 x, y = self._find_best_position(width, height, renderer)
588 elif self._loc in Legend.codes.values(): # Fixed location.
589 bbox = Bbox.from_bounds(0, 0, width, height)
590 x, y = self._get_anchored_bbox(self._loc, bbox,
591 self.get_bbox_to_anchor(),
592 renderer)
593 else: # Axes or figure coordinates.
594 fx, fy = self._loc
595 bbox = self.get_bbox_to_anchor()
596 x, y = bbox.x0 + bbox.width * fx, bbox.y0 + bbox.height * fy
598 return x + xdescent, y + ydescent
600 @allow_rasterization
601 def draw(self, renderer):
602 "Draw everything that belongs to the legend."
603 if not self.get_visible():
604 return
606 renderer.open_group('legend', gid=self.get_gid())
608 fontsize = renderer.points_to_pixels(self._fontsize)
610 # if mode == fill, set the width of the legend_box to the
611 # width of the parent (minus pads)
612 if self._mode in ["expand"]:
613 pad = 2 * (self.borderaxespad + self.borderpad) * fontsize
614 self._legend_box.set_width(self.get_bbox_to_anchor().width - pad)
616 # update the location and size of the legend. This needs to
617 # be done in any case to clip the figure right.
618 bbox = self._legend_box.get_window_extent(renderer)
619 self.legendPatch.set_bounds(bbox.x0, bbox.y0,
620 bbox.width, bbox.height)
621 self.legendPatch.set_mutation_scale(fontsize)
623 if self._drawFrame:
624 if self.shadow:
625 shadow = Shadow(self.legendPatch, 2, -2)
626 shadow.draw(renderer)
628 self.legendPatch.draw(renderer)
630 self._legend_box.draw(renderer)
632 renderer.close_group('legend')
633 self.stale = False
635 def _approx_text_height(self, renderer=None):
636 """
637 Return the approximate height of the text. This is used to place
638 the legend handle.
639 """
640 if renderer is None:
641 return self._fontsize
642 else:
643 return renderer.points_to_pixels(self._fontsize)
645 # _default_handler_map defines the default mapping between plot
646 # elements and the legend handlers.
648 _default_handler_map = {
649 StemContainer: legend_handler.HandlerStem(),
650 ErrorbarContainer: legend_handler.HandlerErrorbar(),
651 Line2D: legend_handler.HandlerLine2D(),
652 Patch: legend_handler.HandlerPatch(),
653 LineCollection: legend_handler.HandlerLineCollection(),
654 RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
655 CircleCollection: legend_handler.HandlerCircleCollection(),
656 BarContainer: legend_handler.HandlerPatch(
657 update_func=legend_handler.update_from_first_child),
658 tuple: legend_handler.HandlerTuple(),
659 PathCollection: legend_handler.HandlerPathCollection(),
660 PolyCollection: legend_handler.HandlerPolyCollection()
661 }
663 # (get|set|update)_default_handler_maps are public interfaces to
664 # modify the default handler map.
666 @classmethod
667 def get_default_handler_map(cls):
668 """
669 A class method that returns the default handler map.
670 """
671 return cls._default_handler_map
673 @classmethod
674 def set_default_handler_map(cls, handler_map):
675 """
676 A class method to set the default handler map.
677 """
678 cls._default_handler_map = handler_map
680 @classmethod
681 def update_default_handler_map(cls, handler_map):
682 """
683 A class method to update the default handler map.
684 """
685 cls._default_handler_map.update(handler_map)
687 def get_legend_handler_map(self):
688 """
689 Return the handler map.
690 """
692 default_handler_map = self.get_default_handler_map()
694 if self._custom_handler_map:
695 hm = default_handler_map.copy()
696 hm.update(self._custom_handler_map)
697 return hm
698 else:
699 return default_handler_map
701 @staticmethod
702 def get_legend_handler(legend_handler_map, orig_handle):
703 """
704 Return a legend handler from *legend_handler_map* that
705 corresponds to *orig_handler*.
707 *legend_handler_map* should be a dictionary object (that is
708 returned by the get_legend_handler_map method).
710 It first checks if the *orig_handle* itself is a key in the
711 *legend_handler_map* and return the associated value.
712 Otherwise, it checks for each of the classes in its
713 method-resolution-order. If no matching key is found, it
714 returns ``None``.
715 """
716 try:
717 return legend_handler_map[orig_handle]
718 except (TypeError, KeyError): # TypeError if unhashable.
719 pass
720 for handle_type in type(orig_handle).mro():
721 try:
722 return legend_handler_map[handle_type]
723 except KeyError:
724 pass
725 return None
727 def _init_legend_box(self, handles, labels, markerfirst=True):
728 """
729 Initialize the legend_box. The legend_box is an instance of
730 the OffsetBox, which is packed with legend handles and
731 texts. Once packed, their location is calculated during the
732 drawing time.
733 """
735 fontsize = self._fontsize
737 # legend_box is a HPacker, horizontally packed with
738 # columns. Each column is a VPacker, vertically packed with
739 # legend items. Each legend item is HPacker packed with
740 # legend handleBox and labelBox. handleBox is an instance of
741 # offsetbox.DrawingArea which contains legend handle. labelBox
742 # is an instance of offsetbox.TextArea which contains legend
743 # text.
745 text_list = [] # the list of text instances
746 handle_list = [] # the list of text instances
747 handles_and_labels = []
749 label_prop = dict(verticalalignment='baseline',
750 horizontalalignment='left',
751 fontproperties=self.prop,
752 )
754 # The approximate height and descent of text. These values are
755 # only used for plotting the legend handle.
756 descent = 0.35 * self._approx_text_height() * (self.handleheight - 0.7)
757 # 0.35 and 0.7 are just heuristic numbers and may need to be improved.
758 height = self._approx_text_height() * self.handleheight - descent
759 # each handle needs to be drawn inside a box of (x, y, w, h) =
760 # (0, -descent, width, height). And their coordinates should
761 # be given in the display coordinates.
763 # The transformation of each handle will be automatically set
764 # to self.get_transform(). If the artist does not use its
765 # default transform (e.g., Collections), you need to
766 # manually set their transform to the self.get_transform().
767 legend_handler_map = self.get_legend_handler_map()
769 for orig_handle, lab in zip(handles, labels):
770 handler = self.get_legend_handler(legend_handler_map, orig_handle)
771 if handler is None:
772 cbook._warn_external(
773 "Legend does not support {!r} instances.\nA proxy artist "
774 "may be used instead.\nSee: "
775 "http://matplotlib.org/users/legend_guide.html"
776 "#creating-artists-specifically-for-adding-to-the-legend-"
777 "aka-proxy-artists".format(orig_handle))
778 # We don't have a handle for this artist, so we just defer
779 # to None.
780 handle_list.append(None)
781 else:
782 textbox = TextArea(lab, textprops=label_prop,
783 multilinebaseline=True,
784 minimumdescent=True)
785 handlebox = DrawingArea(width=self.handlelength * fontsize,
786 height=height,
787 xdescent=0., ydescent=descent)
789 text_list.append(textbox._text)
790 # Create the artist for the legend which represents the
791 # original artist/handle.
792 handle_list.append(handler.legend_artist(self, orig_handle,
793 fontsize, handlebox))
794 handles_and_labels.append((handlebox, textbox))
796 if handles_and_labels:
797 # We calculate number of rows in each column. The first
798 # (num_largecol) columns will have (nrows+1) rows, and remaining
799 # (num_smallcol) columns will have (nrows) rows.
800 ncol = min(self._ncol, len(handles_and_labels))
801 nrows, num_largecol = divmod(len(handles_and_labels), ncol)
802 num_smallcol = ncol - num_largecol
803 # starting index of each column and number of rows in it.
804 rows_per_col = [nrows + 1] * num_largecol + [nrows] * num_smallcol
805 start_idxs = np.concatenate([[0], np.cumsum(rows_per_col)[:-1]])
806 cols = zip(start_idxs, rows_per_col)
807 else:
808 cols = []
810 columnbox = []
811 for i0, di in cols:
812 # pack handleBox and labelBox into itemBox
813 itemBoxes = [HPacker(pad=0,
814 sep=self.handletextpad * fontsize,
815 children=[h, t] if markerfirst else [t, h],
816 align="baseline")
817 for h, t in handles_and_labels[i0:i0 + di]]
818 # minimumdescent=False for the text of the last row of the column
819 if markerfirst:
820 itemBoxes[-1].get_children()[1].set_minimumdescent(False)
821 else:
822 itemBoxes[-1].get_children()[0].set_minimumdescent(False)
824 # pack columnBox
825 alignment = "baseline" if markerfirst else "right"
826 columnbox.append(VPacker(pad=0,
827 sep=self.labelspacing * fontsize,
828 align=alignment,
829 children=itemBoxes))
831 mode = "expand" if self._mode == "expand" else "fixed"
832 sep = self.columnspacing * fontsize
833 self._legend_handle_box = HPacker(pad=0,
834 sep=sep, align="baseline",
835 mode=mode,
836 children=columnbox)
837 self._legend_title_box = TextArea("")
838 self._legend_box = VPacker(pad=self.borderpad * fontsize,
839 sep=self.labelspacing * fontsize,
840 align="center",
841 children=[self._legend_title_box,
842 self._legend_handle_box])
843 self._legend_box.set_figure(self.figure)
844 self.texts = text_list
845 self.legendHandles = handle_list
847 def _auto_legend_data(self):
848 """
849 Returns list of vertices and extents covered by the plot.
851 Returns a two long list.
853 First element is a list of (x, y) vertices (in
854 display-coordinates) covered by all the lines and line
855 collections, in the legend's handles.
857 Second element is a list of bounding boxes for all the patches in
858 the legend's handles.
859 """
860 # should always hold because function is only called internally
861 assert self.isaxes
863 ax = self.parent
864 bboxes = []
865 lines = []
866 offsets = []
868 for handle in ax.lines:
869 assert isinstance(handle, Line2D)
870 path = handle.get_path()
871 trans = handle.get_transform()
872 tpath = trans.transform_path(path)
873 lines.append(tpath)
875 for handle in ax.patches:
876 assert isinstance(handle, Patch)
878 if isinstance(handle, Rectangle):
879 transform = handle.get_data_transform()
880 bboxes.append(handle.get_bbox().transformed(transform))
881 else:
882 transform = handle.get_transform()
883 bboxes.append(handle.get_path().get_extents(transform))
885 for handle in ax.collections:
886 transform, transOffset, hoffsets, paths = handle._prepare_points()
888 if len(hoffsets):
889 for offset in transOffset.transform(hoffsets):
890 offsets.append(offset)
892 try:
893 vertices = np.concatenate([l.vertices for l in lines])
894 except ValueError:
895 vertices = np.array([])
897 return [vertices, bboxes, lines, offsets]
899 def draw_frame(self, b):
900 '''
901 Set draw frame to b.
903 Parameters
904 ----------
905 b : bool
906 '''
907 self.set_frame_on(b)
909 def get_children(self):
910 'Return a list of child artists.'
911 children = []
912 if self._legend_box:
913 children.append(self._legend_box)
914 children.append(self.get_frame())
916 return children
918 def get_frame(self):
919 '''
920 Return the `~.patches.Rectangle` instances used to frame the legend.
921 '''
922 return self.legendPatch
924 def get_lines(self):
925 'Return a list of `~.lines.Line2D` instances in the legend.'
926 return [h for h in self.legendHandles if isinstance(h, Line2D)]
928 def get_patches(self):
929 'Return a list of `~.patches.Patch` instances in the legend.'
930 return silent_list('Patch',
931 [h for h in self.legendHandles
932 if isinstance(h, Patch)])
934 def get_texts(self):
935 'Return a list of `~.text.Text` instances in the legend.'
936 return silent_list('Text', self.texts)
938 def set_title(self, title, prop=None):
939 """
940 Set the legend title. Fontproperties can be optionally set
941 with *prop* parameter.
942 """
943 self._legend_title_box._text.set_text(title)
944 if title:
945 self._legend_title_box._text.set_visible(True)
946 self._legend_title_box.set_visible(True)
947 else:
948 self._legend_title_box._text.set_visible(False)
949 self._legend_title_box.set_visible(False)
951 if prop is not None:
952 if isinstance(prop, dict):
953 prop = FontProperties(**prop)
954 self._legend_title_box._text.set_fontproperties(prop)
956 self.stale = True
958 def get_title(self):
959 'Return the `.Text` instance for the legend title.'
960 return self._legend_title_box._text
962 def get_window_extent(self, renderer=None):
963 'Return extent of the legend.'
964 if renderer is None:
965 renderer = self.figure._cachedRenderer
966 return self._legend_box.get_window_extent(renderer=renderer)
968 def get_tightbbox(self, renderer):
969 """
970 Like `.Legend.get_window_extent`, but uses the box for the legend.
972 Parameters
973 ----------
974 renderer : `.RendererBase` instance
975 renderer that will be used to draw the figures (i.e.
976 ``fig.canvas.get_renderer()``)
978 Returns
979 -------
980 `.BboxBase` : containing the bounding box in figure pixel co-ordinates.
981 """
982 return self._legend_box.get_window_extent(renderer)
984 def get_frame_on(self):
985 """Get whether the legend box patch is drawn."""
986 return self._drawFrame
988 def set_frame_on(self, b):
989 """
990 Set whether the legend box patch is drawn.
992 Parameters
993 ----------
994 b : bool
995 """
996 self._drawFrame = b
997 self.stale = True
999 def get_bbox_to_anchor(self):
1000 """Return the bbox that the legend will be anchored to."""
1001 if self._bbox_to_anchor is None:
1002 return self.parent.bbox
1003 else:
1004 return self._bbox_to_anchor
1006 def set_bbox_to_anchor(self, bbox, transform=None):
1007 """
1008 Set the bbox that the legend will be anchored to.
1010 *bbox* can be
1012 - A `.BboxBase` instance
1013 - A tuple of ``(left, bottom, width, height)`` in the given transform
1014 (normalized axes coordinate if None)
1015 - A tuple of ``(left, bottom)`` where the width and height will be
1016 assumed to be zero.
1017 """
1018 if bbox is None:
1019 self._bbox_to_anchor = None
1020 return
1021 elif isinstance(bbox, BboxBase):
1022 self._bbox_to_anchor = bbox
1023 else:
1024 try:
1025 l = len(bbox)
1026 except TypeError:
1027 raise ValueError("Invalid argument for bbox : %s" % str(bbox))
1029 if l == 2:
1030 bbox = [bbox[0], bbox[1], 0, 0]
1032 self._bbox_to_anchor = Bbox.from_bounds(*bbox)
1034 if transform is None:
1035 transform = BboxTransformTo(self.parent.bbox)
1037 self._bbox_to_anchor = TransformedBbox(self._bbox_to_anchor,
1038 transform)
1039 self.stale = True
1041 def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
1042 """
1043 Place the *bbox* inside the *parentbbox* according to a given
1044 location code. Return the (x, y) coordinate of the bbox.
1046 - loc: a location code in range(1, 11).
1047 This corresponds to the possible values for self._loc, excluding
1048 "best".
1050 - bbox: bbox to be placed, display coordinate units.
1051 - parentbbox: a parent box which will contain the bbox. In
1052 display coordinates.
1053 """
1054 assert loc in range(1, 11) # called only internally
1056 BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
1058 anchor_coefs = {UR: "NE",
1059 UL: "NW",
1060 LL: "SW",
1061 LR: "SE",
1062 R: "E",
1063 CL: "W",
1064 CR: "E",
1065 LC: "S",
1066 UC: "N",
1067 C: "C"}
1069 c = anchor_coefs[loc]
1071 fontsize = renderer.points_to_pixels(self._fontsize)
1072 container = parentbbox.padded(-(self.borderaxespad) * fontsize)
1073 anchored_box = bbox.anchored(c, container=container)
1074 return anchored_box.x0, anchored_box.y0
1076 def _find_best_position(self, width, height, renderer, consider=None):
1077 """
1078 Determine the best location to place the legend.
1080 *consider* is a list of ``(x, y)`` pairs to consider as a potential
1081 lower-left corner of the legend. All are display coords.
1082 """
1083 # should always hold because function is only called internally
1084 assert self.isaxes
1086 start_time = time.perf_counter()
1088 verts, bboxes, lines, offsets = self._auto_legend_data()
1090 bbox = Bbox.from_bounds(0, 0, width, height)
1091 if consider is None:
1092 consider = [self._get_anchored_bbox(x, bbox,
1093 self.get_bbox_to_anchor(),
1094 renderer)
1095 for x in range(1, len(self.codes))]
1097 candidates = []
1098 for idx, (l, b) in enumerate(consider):
1099 legendBox = Bbox.from_bounds(l, b, width, height)
1100 badness = 0
1101 # XXX TODO: If markers are present, it would be good to
1102 # take them into account when checking vertex overlaps in
1103 # the next line.
1104 badness = (legendBox.count_contains(verts)
1105 + legendBox.count_contains(offsets)
1106 + legendBox.count_overlaps(bboxes)
1107 + sum(line.intersects_bbox(legendBox, filled=False)
1108 for line in lines))
1109 if badness == 0:
1110 return l, b
1111 # Include the index to favor lower codes in case of a tie.
1112 candidates.append((badness, idx, (l, b)))
1114 _, _, (l, b) = min(candidates)
1116 if self._loc_used_default and time.perf_counter() - start_time > 1:
1117 cbook._warn_external(
1118 'Creating legend with loc="best" can be slow with large '
1119 'amounts of data.')
1121 return l, b
1123 def contains(self, event):
1124 inside, info = self._default_contains(event)
1125 if inside is not None:
1126 return inside, info
1127 return self.legendPatch.contains(event)
1129 def set_draggable(self, state, use_blit=False, update='loc'):
1130 """
1131 Enable or disable mouse dragging support of the legend.
1133 Parameters
1134 ----------
1135 state : bool
1136 Whether mouse dragging is enabled.
1137 use_blit : bool, optional
1138 Use blitting for faster image composition. For details see
1139 :ref:`func-animation`.
1140 update : {'loc', 'bbox'}, optional
1141 The legend parameter to be changed when dragged:
1143 - 'loc': update the *loc* parameter of the legend
1144 - 'bbox': update the *bbox_to_anchor* parameter of the legend
1146 Returns
1147 -------
1148 If *state* is ``True`` this returns the `~.DraggableLegend` helper
1149 instance. Otherwise this returns ``None``.
1150 """
1151 if state:
1152 if self._draggable is None:
1153 self._draggable = DraggableLegend(self,
1154 use_blit,
1155 update=update)
1156 else:
1157 if self._draggable is not None:
1158 self._draggable.disconnect()
1159 self._draggable = None
1160 return self._draggable
1162 def get_draggable(self):
1163 """Return ``True`` if the legend is draggable, ``False`` otherwise."""
1164 return self._draggable is not None
1167# Helper functions to parse legend arguments for both `figure.legend` and
1168# `axes.legend`:
1169def _get_legend_handles(axs, legend_handler_map=None):
1170 """
1171 Return a generator of artists that can be used as handles in
1172 a legend.
1174 """
1175 handles_original = []
1176 for ax in axs:
1177 handles_original += (ax.lines + ax.patches +
1178 ax.collections + ax.containers)
1179 # support parasite axes:
1180 if hasattr(ax, 'parasites'):
1181 for axx in ax.parasites:
1182 handles_original += (axx.lines + axx.patches +
1183 axx.collections + axx.containers)
1185 handler_map = Legend.get_default_handler_map()
1187 if legend_handler_map is not None:
1188 handler_map = handler_map.copy()
1189 handler_map.update(legend_handler_map)
1191 has_handler = Legend.get_legend_handler
1193 for handle in handles_original:
1194 label = handle.get_label()
1195 if label != '_nolegend_' and has_handler(handler_map, handle):
1196 yield handle
1199def _get_legend_handles_labels(axs, legend_handler_map=None):
1200 """
1201 Return handles and labels for legend, internal method.
1203 """
1204 handles = []
1205 labels = []
1207 for handle in _get_legend_handles(axs, legend_handler_map):
1208 label = handle.get_label()
1209 if label and not label.startswith('_'):
1210 handles.append(handle)
1211 labels.append(label)
1212 return handles, labels
1215def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs):
1216 """
1217 Get the handles and labels from the calls to either ``figure.legend``
1218 or ``axes.legend``.
1220 ``axs`` is a list of axes (to get legend artists from)
1221 """
1222 log = logging.getLogger(__name__)
1224 handlers = kwargs.get('handler_map', {}) or {}
1225 extra_args = ()
1227 if (handles is not None or labels is not None) and args:
1228 cbook._warn_external("You have mixed positional and keyword "
1229 "arguments, some input may be discarded.")
1231 # if got both handles and labels as kwargs, make same length
1232 if handles and labels:
1233 handles, labels = zip(*zip(handles, labels))
1235 elif handles is not None and labels is None:
1236 labels = [handle.get_label() for handle in handles]
1238 elif labels is not None and handles is None:
1239 # Get as many handles as there are labels.
1240 handles = [handle for handle, label
1241 in zip(_get_legend_handles(axs, handlers), labels)]
1243 # No arguments - automatically detect labels and handles.
1244 elif len(args) == 0:
1245 handles, labels = _get_legend_handles_labels(axs, handlers)
1246 if not handles:
1247 log.warning('No handles with labels found to put in legend.')
1249 # One argument. User defined labels - automatic handle detection.
1250 elif len(args) == 1:
1251 labels, = args
1252 # Get as many handles as there are labels.
1253 handles = [handle for handle, label
1254 in zip(_get_legend_handles(axs, handlers), labels)]
1256 # Two arguments:
1257 # * user defined handles and labels
1258 elif len(args) >= 2:
1259 handles, labels = args[:2]
1260 extra_args = args[2:]
1262 else:
1263 raise TypeError('Invalid arguments to legend.')
1265 return handles, labels, extra_args, kwargs