12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265 |
- """
- The legend module defines the Legend class, which is responsible for
- drawing legends associated with axes and/or figures.
- .. important::
- It is unlikely that you would ever create a Legend instance
- manually. Most users would normally create a legend via the
- :meth:`~matplotlib.axes.Axes.legend` function. For more details on legends
- there is also a :doc:`legend guide </tutorials/intermediate/legend_guide>`.
- The Legend class can be considered as a container of legend handles and
- legend texts. Creation of corresponding legend handles from the plot elements
- in the axes or figures (e.g., lines, patches, etc.) are specified by the
- handler map, which defines the mapping between the plot elements and the
- legend handlers to be used (the default legend handlers are defined in the
- :mod:`~matplotlib.legend_handler` module). Note that not all kinds of
- artist are supported by the legend yet by default but it is possible to
- extend the legend handler's capabilities to support arbitrary objects. See
- the :doc:`legend guide </tutorials/intermediate/legend_guide>` for more
- information.
- """
- import logging
- import time
- import numpy as np
- from matplotlib import rcParams
- from matplotlib import cbook, docstring
- from matplotlib.artist import Artist, allow_rasterization
- from matplotlib.cbook import silent_list
- from matplotlib.font_manager import FontProperties
- from matplotlib.lines import Line2D
- from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch
- from matplotlib.collections import (LineCollection, RegularPolyCollection,
- CircleCollection, PathCollection,
- PolyCollection)
- from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
- from matplotlib.transforms import BboxTransformTo, BboxTransformFrom
- from matplotlib.offsetbox import HPacker, VPacker, TextArea, DrawingArea
- from matplotlib.offsetbox import DraggableOffsetBox
- from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer
- from . import legend_handler
- class DraggableLegend(DraggableOffsetBox):
- def __init__(self, legend, use_blit=False, update="loc"):
- """
- Wrapper around a `.Legend` to support mouse dragging.
- Parameters
- ----------
- legend : `.Legend`
- The `.Legend` instance to wrap.
- use_blit : bool, optional
- Use blitting for faster image composition. For details see
- :ref:`func-animation`.
- update : {'loc', 'bbox'}, optional
- If "loc", update the *loc* parameter of the legend upon finalizing.
- If "bbox", update the *bbox_to_anchor* parameter.
- """
- self.legend = legend
- cbook._check_in_list(["loc", "bbox"], update=update)
- self._update = update
- DraggableOffsetBox.__init__(self, legend, legend._legend_box,
- use_blit=use_blit)
- def artist_picker(self, legend, evt):
- return self.legend.contains(evt)
- def finalize_offset(self):
- if self._update == "loc":
- self._update_loc(self.get_loc_in_canvas())
- elif self._update == "bbox":
- self._bbox_to_anchor(self.get_loc_in_canvas())
- def _update_loc(self, loc_in_canvas):
- bbox = self.legend.get_bbox_to_anchor()
- # if bbox has zero width or height, the transformation is
- # ill-defined. Fall back to the default bbox_to_anchor.
- if bbox.width == 0 or bbox.height == 0:
- self.legend.set_bbox_to_anchor(None)
- bbox = self.legend.get_bbox_to_anchor()
- _bbox_transform = BboxTransformFrom(bbox)
- self.legend._loc = tuple(_bbox_transform.transform(loc_in_canvas))
- def _update_bbox_to_anchor(self, loc_in_canvas):
- loc_in_bbox = self.legend.axes.transAxes.transform(loc_in_canvas)
- self.legend.set_bbox_to_anchor(loc_in_bbox)
- _legend_kw_doc = '''
- loc : str or pair of floats, default: :rc:`legend.loc` ('best' for axes, \
- 'upper right' for figures)
- The location of the legend.
- The strings
- ``'upper left', 'upper right', 'lower left', 'lower right'``
- place the legend at the corresponding corner of the axes/figure.
- The strings
- ``'upper center', 'lower center', 'center left', 'center right'``
- place the legend at the center of the corresponding edge of the
- axes/figure.
- The string ``'center'`` places the legend at the center of the axes/figure.
- The string ``'best'`` places the legend at the location, among the nine
- locations defined so far, with the minimum overlap with other drawn
- artists. This option can be quite slow for plots with large amounts of
- data; your plotting speed may benefit from providing a specific location.
- The location can also be a 2-tuple giving the coordinates of the lower-left
- corner of the legend in axes coordinates (in which case *bbox_to_anchor*
- will be ignored).
- For back-compatibility, ``'center right'`` (but no other location) can also
- be spelled ``'right'``, and each "string" locations can also be given as a
- numeric value:
- =============== =============
- Location String Location Code
- =============== =============
- 'best' 0
- 'upper right' 1
- 'upper left' 2
- 'lower left' 3
- 'lower right' 4
- 'right' 5
- 'center left' 6
- 'center right' 7
- 'lower center' 8
- 'upper center' 9
- 'center' 10
- =============== =============
- bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
- Box that is used to position the legend in conjunction with *loc*.
- Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or
- `figure.bbox` (if `.Figure.legend`). This argument allows arbitrary
- placement of the legend.
- Bbox coordinates are interpreted in the coordinate system given by
- `bbox_transform`, with the default transform
- Axes or Figure coordinates, depending on which ``legend`` is called.
- If a 4-tuple or `.BboxBase` is given, then it specifies the bbox
- ``(x, y, width, height)`` that the legend is placed in.
- To put the legend in the best location in the bottom right
- quadrant of the axes (or figure)::
- loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.5)
- A 2-tuple ``(x, y)`` places the corner of the legend specified by *loc* at
- x, y. For example, to put the legend's upper right-hand corner in the
- center of the axes (or figure) the following keywords can be used::
- loc='upper right', bbox_to_anchor=(0.5, 0.5)
- ncol : integer
- The number of columns that the legend has. Default is 1.
- prop : None or :class:`matplotlib.font_manager.FontProperties` or dict
- The font properties of the legend. If None (default), the current
- :data:`matplotlib.rcParams` will be used.
- fontsize : int or float or {'xx-small', 'x-small', 'small', 'medium', \
- 'large', 'x-large', 'xx-large'}
- The font size of the legend. If the value is numeric the size will be the
- absolute font size in points. String values are relative to the current
- default font size. This argument is only used if *prop* is not specified.
- numpoints : None or int
- The number of marker points in the legend when creating a legend
- entry for a `.Line2D` (line).
- Default is ``None``, which means using :rc:`legend.numpoints`.
- scatterpoints : None or int
- The number of marker points in the legend when creating
- a legend entry for a `.PathCollection` (scatter plot).
- Default is ``None``, which means using :rc:`legend.scatterpoints`.
- scatteryoffsets : iterable of floats
- The vertical offset (relative to the font size) for the markers
- created for a scatter plot legend entry. 0.0 is at the base the
- legend text, and 1.0 is at the top. To draw all markers at the
- same height, set to ``[0.5]``. Default is ``[0.375, 0.5, 0.3125]``.
- markerscale : None or int or float
- The relative size of legend markers compared with the originally
- drawn ones.
- Default is ``None``, which means using :rc:`legend.markerscale`.
- markerfirst : bool
- If *True*, legend marker is placed to the left of the legend label.
- If *False*, legend marker is placed to the right of the legend
- label.
- Default is *True*.
- frameon : None or bool
- Whether the legend should be drawn on a patch (frame).
- Default is ``None``, which means using :rc:`legend.frameon`.
- fancybox : None or bool
- Whether round edges should be enabled around the `~.FancyBboxPatch` which
- makes up the legend's background.
- Default is ``None``, which means using :rc:`legend.fancybox`.
- shadow : None or bool
- Whether to draw a shadow behind the legend.
- Default is ``None``, which means using :rc:`legend.shadow`.
- framealpha : None or float
- The alpha transparency of the legend's background.
- Default is ``None``, which means using :rc:`legend.framealpha`.
- If *shadow* is activated and *framealpha* is ``None``, the default value is
- ignored.
- facecolor : None or "inherit" or color
- The legend's background color.
- Default is ``None``, which means using :rc:`legend.facecolor`.
- If ``"inherit"``, use :rc:`axes.facecolor`.
- edgecolor : None or "inherit" or color
- The legend's background patch edge color.
- Default is ``None``, which means using :rc:`legend.edgecolor`.
- If ``"inherit"``, use take :rc:`axes.edgecolor`.
- mode : {"expand", None}
- If *mode* is set to ``"expand"`` the legend will be horizontally
- expanded to fill the axes area (or `bbox_to_anchor` if defines
- the legend's size).
- bbox_transform : None or :class:`matplotlib.transforms.Transform`
- The transform for the bounding box (`bbox_to_anchor`). For a value
- of ``None`` (default) the Axes'
- :data:`~matplotlib.axes.Axes.transAxes` transform will be used.
- title : str or None
- The legend's title. Default is no title (``None``).
- title_fontsize: str or None
- The fontsize of the legend's title. Default is the default fontsize.
- borderpad : float or None
- The fractional whitespace inside the legend border, in font-size units.
- Default is ``None``, which means using :rc:`legend.borderpad`.
- labelspacing : float or None
- The vertical space between the legend entries, in font-size units.
- Default is ``None``, which means using :rc:`legend.labelspacing`.
- handlelength : float or None
- The length of the legend handles, in font-size units.
- Default is ``None``, which means using :rc:`legend.handlelength`.
- handletextpad : float or None
- The pad between the legend handle and text, in font-size units.
- Default is ``None``, which means using :rc:`legend.handletextpad`.
- borderaxespad : float or None
- The pad between the axes and legend border, in font-size units.
- Default is ``None``, which means using :rc:`legend.borderaxespad`.
- columnspacing : float or None
- The spacing between columns, in font-size units.
- Default is ``None``, which means using :rc:`legend.columnspacing`.
- handler_map : dict or None
- The custom dictionary mapping instances or types to a legend
- handler. This `handler_map` updates the default handler map
- found at :func:`matplotlib.legend.Legend.get_legend_handler_map`.
- '''
- docstring.interpd.update(_legend_kw_doc=_legend_kw_doc)
- class Legend(Artist):
- """
- Place a legend on the axes at location loc.
- """
- codes = {'best': 0, # only implemented for axes legends
- 'upper right': 1,
- 'upper left': 2,
- 'lower left': 3,
- 'lower right': 4,
- 'right': 5,
- 'center left': 6,
- 'center right': 7,
- 'lower center': 8,
- 'upper center': 9,
- 'center': 10,
- }
- zorder = 5
- def __str__(self):
- return "Legend"
- @docstring.dedent_interpd
- def __init__(self, parent, handles, labels,
- loc=None,
- numpoints=None, # the number of points in the legend line
- markerscale=None, # the relative size of legend markers
- # vs. original
- markerfirst=True, # controls ordering (left-to-right) of
- # legend marker and label
- scatterpoints=None, # number of scatter points
- scatteryoffsets=None,
- prop=None, # properties for the legend texts
- fontsize=None, # keyword to set font size directly
- # spacing & pad defined as a fraction of the font-size
- borderpad=None, # the whitespace inside the legend border
- labelspacing=None, # the vertical space between the legend
- # entries
- handlelength=None, # the length of the legend handles
- handleheight=None, # the height of the legend handles
- handletextpad=None, # the pad between the legend handle
- # and text
- borderaxespad=None, # the pad between the axes and legend
- # border
- columnspacing=None, # spacing between columns
- ncol=1, # number of columns
- mode=None, # mode for horizontal distribution of columns.
- # None, "expand"
- fancybox=None, # True use a fancy box, false use a rounded
- # box, none use rc
- shadow=None,
- title=None, # set a title for the legend
- title_fontsize=None, # set to ax.fontsize if None
- framealpha=None, # set frame alpha
- edgecolor=None, # frame patch edgecolor
- facecolor=None, # frame patch facecolor
- bbox_to_anchor=None, # bbox that the legend will be anchored.
- bbox_transform=None, # transform for the bbox
- frameon=None, # draw frame
- handler_map=None,
- ):
- """
- Parameters
- ----------
- parent : `~matplotlib.axes.Axes` or `.Figure`
- The artist that contains the legend.
- handles : list of `.Artist`
- A list of Artists (lines, patches) to be added to the legend.
- labels : list of str
- A list of labels to show next to the artists. The length of handles
- and labels should be the same. If they are not, they are truncated
- to the smaller of both lengths.
- Other Parameters
- ----------------
- %(_legend_kw_doc)s
- Notes
- -----
- Users can specify any arbitrary location for the legend using the
- *bbox_to_anchor* keyword argument. *bbox_to_anchor* can be a
- `.BboxBase` (or derived therefrom) or a tuple of 2 or 4 floats.
- See :meth:`set_bbox_to_anchor` for more detail.
- The legend location can be specified by setting *loc* with a tuple of
- 2 floats, which is interpreted as the lower-left corner of the legend
- in the normalized axes coordinate.
- """
- # local import only to avoid circularity
- from matplotlib.axes import Axes
- from matplotlib.figure import Figure
- Artist.__init__(self)
- if prop is None:
- if fontsize is not None:
- self.prop = FontProperties(size=fontsize)
- else:
- self.prop = FontProperties(size=rcParams["legend.fontsize"])
- elif isinstance(prop, dict):
- self.prop = FontProperties(**prop)
- if "size" not in prop:
- self.prop.set_size(rcParams["legend.fontsize"])
- else:
- self.prop = prop
- self._fontsize = self.prop.get_size_in_points()
- self.texts = []
- self.legendHandles = []
- self._legend_title_box = None
- #: A dictionary with the extra handler mappings for this Legend
- #: instance.
- self._custom_handler_map = handler_map
- locals_view = locals()
- for name in ["numpoints", "markerscale", "shadow", "columnspacing",
- "scatterpoints", "handleheight", 'borderpad',
- 'labelspacing', 'handlelength', 'handletextpad',
- 'borderaxespad']:
- if locals_view[name] is None:
- value = rcParams["legend." + name]
- else:
- value = locals_view[name]
- setattr(self, name, value)
- del locals_view
- # trim handles and labels if illegal label...
- _lab, _hand = [], []
- for label, handle in zip(labels, handles):
- if isinstance(label, str) and label.startswith('_'):
- cbook._warn_external('The handle {!r} has a label of {!r} '
- 'which cannot be automatically added to'
- ' the legend.'.format(handle, label))
- else:
- _lab.append(label)
- _hand.append(handle)
- labels, handles = _lab, _hand
- handles = list(handles)
- if len(handles) < 2:
- ncol = 1
- self._ncol = ncol
- if self.numpoints <= 0:
- raise ValueError("numpoints must be > 0; it was %d" % numpoints)
- # introduce y-offset for handles of the scatter plot
- if scatteryoffsets is None:
- self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.])
- else:
- self._scatteryoffsets = np.asarray(scatteryoffsets)
- reps = self.scatterpoints // len(self._scatteryoffsets) + 1
- self._scatteryoffsets = np.tile(self._scatteryoffsets,
- reps)[:self.scatterpoints]
- # _legend_box is an OffsetBox instance that contains all
- # legend items and will be initialized from _init_legend_box()
- # method.
- self._legend_box = None
- if isinstance(parent, Axes):
- self.isaxes = True
- self.axes = parent
- self.set_figure(parent.figure)
- elif isinstance(parent, Figure):
- self.isaxes = False
- self.set_figure(parent)
- else:
- raise TypeError("Legend needs either Axes or Figure as parent")
- self.parent = parent
- self._loc_used_default = loc is None
- if loc is None:
- loc = rcParams["legend.loc"]
- if not self.isaxes and loc in [0, 'best']:
- loc = 'upper right'
- if isinstance(loc, str):
- if loc not in self.codes:
- if self.isaxes:
- cbook.warn_deprecated(
- "3.1", message="Unrecognized location {!r}. Falling "
- "back on 'best'; valid locations are\n\t{}\n"
- "This will raise an exception %(removal)s."
- .format(loc, '\n\t'.join(self.codes)))
- loc = 0
- else:
- cbook.warn_deprecated(
- "3.1", message="Unrecognized location {!r}. Falling "
- "back on 'upper right'; valid locations are\n\t{}\n'"
- "This will raise an exception %(removal)s."
- .format(loc, '\n\t'.join(self.codes)))
- loc = 1
- else:
- loc = self.codes[loc]
- if not self.isaxes and loc == 0:
- cbook.warn_deprecated(
- "3.1", message="Automatic legend placement (loc='best') not "
- "implemented for figure legend. Falling back on 'upper "
- "right'. This will raise an exception %(removal)s.")
- loc = 1
- self._mode = mode
- self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
- # We use FancyBboxPatch to draw a legend frame. The location
- # and size of the box will be updated during the drawing time.
- if facecolor is None:
- facecolor = rcParams["legend.facecolor"]
- if facecolor == 'inherit':
- facecolor = rcParams["axes.facecolor"]
- if edgecolor is None:
- edgecolor = rcParams["legend.edgecolor"]
- if edgecolor == 'inherit':
- edgecolor = rcParams["axes.edgecolor"]
- self.legendPatch = FancyBboxPatch(
- xy=(0.0, 0.0), width=1., height=1.,
- facecolor=facecolor,
- edgecolor=edgecolor,
- mutation_scale=self._fontsize,
- snap=True
- )
- # The width and height of the legendPatch will be set (in the
- # draw()) to the length that includes the padding. Thus we set
- # pad=0 here.
- if fancybox is None:
- fancybox = rcParams["legend.fancybox"]
- if fancybox:
- self.legendPatch.set_boxstyle("round", pad=0,
- rounding_size=0.2)
- else:
- self.legendPatch.set_boxstyle("square", pad=0)
- self._set_artist_props(self.legendPatch)
- self._drawFrame = frameon
- if frameon is None:
- self._drawFrame = rcParams["legend.frameon"]
- # init with null renderer
- self._init_legend_box(handles, labels, markerfirst)
- # If shadow is activated use framealpha if not
- # explicitly passed. See Issue 8943
- if framealpha is None:
- if shadow:
- self.get_frame().set_alpha(1)
- else:
- self.get_frame().set_alpha(rcParams["legend.framealpha"])
- else:
- self.get_frame().set_alpha(framealpha)
- tmp = self._loc_used_default
- self._set_loc(loc)
- self._loc_used_default = tmp # ignore changes done by _set_loc
- # figure out title fontsize:
- if title_fontsize is None:
- title_fontsize = rcParams['legend.title_fontsize']
- tprop = FontProperties(size=title_fontsize)
- self.set_title(title, prop=tprop)
- self._draggable = None
- def _set_artist_props(self, a):
- """
- Set the boilerplate props for artists added to axes.
- """
- a.set_figure(self.figure)
- if self.isaxes:
- # a.set_axes(self.axes)
- a.axes = self.axes
- a.set_transform(self.get_transform())
- def _set_loc(self, loc):
- # find_offset function will be provided to _legend_box and
- # _legend_box will draw itself at the location of the return
- # value of the find_offset.
- self._loc_used_default = False
- self._loc_real = loc
- self.stale = True
- self._legend_box.set_offset(self._findoffset)
- def _get_loc(self):
- return self._loc_real
- _loc = property(_get_loc, _set_loc)
- def _findoffset(self, width, height, xdescent, ydescent, renderer):
- "Helper function to locate the legend."
- if self._loc == 0: # "best".
- x, y = self._find_best_position(width, height, renderer)
- elif self._loc in Legend.codes.values(): # Fixed location.
- bbox = Bbox.from_bounds(0, 0, width, height)
- x, y = self._get_anchored_bbox(self._loc, bbox,
- self.get_bbox_to_anchor(),
- renderer)
- else: # Axes or figure coordinates.
- fx, fy = self._loc
- bbox = self.get_bbox_to_anchor()
- x, y = bbox.x0 + bbox.width * fx, bbox.y0 + bbox.height * fy
- return x + xdescent, y + ydescent
- @allow_rasterization
- def draw(self, renderer):
- "Draw everything that belongs to the legend."
- if not self.get_visible():
- return
- renderer.open_group('legend', gid=self.get_gid())
- fontsize = renderer.points_to_pixels(self._fontsize)
- # if mode == fill, set the width of the legend_box to the
- # width of the parent (minus pads)
- if self._mode in ["expand"]:
- pad = 2 * (self.borderaxespad + self.borderpad) * fontsize
- self._legend_box.set_width(self.get_bbox_to_anchor().width - pad)
- # update the location and size of the legend. This needs to
- # be done in any case to clip the figure right.
- bbox = self._legend_box.get_window_extent(renderer)
- self.legendPatch.set_bounds(bbox.x0, bbox.y0,
- bbox.width, bbox.height)
- self.legendPatch.set_mutation_scale(fontsize)
- if self._drawFrame:
- if self.shadow:
- shadow = Shadow(self.legendPatch, 2, -2)
- shadow.draw(renderer)
- self.legendPatch.draw(renderer)
- self._legend_box.draw(renderer)
- renderer.close_group('legend')
- self.stale = False
- def _approx_text_height(self, renderer=None):
- """
- Return the approximate height of the text. This is used to place
- the legend handle.
- """
- if renderer is None:
- return self._fontsize
- else:
- return renderer.points_to_pixels(self._fontsize)
- # _default_handler_map defines the default mapping between plot
- # elements and the legend handlers.
- _default_handler_map = {
- StemContainer: legend_handler.HandlerStem(),
- ErrorbarContainer: legend_handler.HandlerErrorbar(),
- Line2D: legend_handler.HandlerLine2D(),
- Patch: legend_handler.HandlerPatch(),
- LineCollection: legend_handler.HandlerLineCollection(),
- RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
- CircleCollection: legend_handler.HandlerCircleCollection(),
- BarContainer: legend_handler.HandlerPatch(
- update_func=legend_handler.update_from_first_child),
- tuple: legend_handler.HandlerTuple(),
- PathCollection: legend_handler.HandlerPathCollection(),
- PolyCollection: legend_handler.HandlerPolyCollection()
- }
- # (get|set|update)_default_handler_maps are public interfaces to
- # modify the default handler map.
- @classmethod
- def get_default_handler_map(cls):
- """
- A class method that returns the default handler map.
- """
- return cls._default_handler_map
- @classmethod
- def set_default_handler_map(cls, handler_map):
- """
- A class method to set the default handler map.
- """
- cls._default_handler_map = handler_map
- @classmethod
- def update_default_handler_map(cls, handler_map):
- """
- A class method to update the default handler map.
- """
- cls._default_handler_map.update(handler_map)
- def get_legend_handler_map(self):
- """
- Return the handler map.
- """
- default_handler_map = self.get_default_handler_map()
- if self._custom_handler_map:
- hm = default_handler_map.copy()
- hm.update(self._custom_handler_map)
- return hm
- else:
- return default_handler_map
- @staticmethod
- def get_legend_handler(legend_handler_map, orig_handle):
- """
- Return a legend handler from *legend_handler_map* that
- corresponds to *orig_handler*.
- *legend_handler_map* should be a dictionary object (that is
- returned by the get_legend_handler_map method).
- It first checks if the *orig_handle* itself is a key in the
- *legend_handler_map* and return the associated value.
- Otherwise, it checks for each of the classes in its
- method-resolution-order. If no matching key is found, it
- returns ``None``.
- """
- try:
- return legend_handler_map[orig_handle]
- except (TypeError, KeyError): # TypeError if unhashable.
- pass
- for handle_type in type(orig_handle).mro():
- try:
- return legend_handler_map[handle_type]
- except KeyError:
- pass
- return None
- def _init_legend_box(self, handles, labels, markerfirst=True):
- """
- Initialize the legend_box. The legend_box is an instance of
- the OffsetBox, which is packed with legend handles and
- texts. Once packed, their location is calculated during the
- drawing time.
- """
- fontsize = self._fontsize
- # legend_box is a HPacker, horizontally packed with
- # columns. Each column is a VPacker, vertically packed with
- # legend items. Each legend item is HPacker packed with
- # legend handleBox and labelBox. handleBox is an instance of
- # offsetbox.DrawingArea which contains legend handle. labelBox
- # is an instance of offsetbox.TextArea which contains legend
- # text.
- text_list = [] # the list of text instances
- handle_list = [] # the list of text instances
- handles_and_labels = []
- label_prop = dict(verticalalignment='baseline',
- horizontalalignment='left',
- fontproperties=self.prop,
- )
- # The approximate height and descent of text. These values are
- # only used for plotting the legend handle.
- descent = 0.35 * self._approx_text_height() * (self.handleheight - 0.7)
- # 0.35 and 0.7 are just heuristic numbers and may need to be improved.
- height = self._approx_text_height() * self.handleheight - descent
- # each handle needs to be drawn inside a box of (x, y, w, h) =
- # (0, -descent, width, height). And their coordinates should
- # be given in the display coordinates.
- # The transformation of each handle will be automatically set
- # to self.get_transform(). If the artist does not use its
- # default transform (e.g., Collections), you need to
- # manually set their transform to the self.get_transform().
- legend_handler_map = self.get_legend_handler_map()
- for orig_handle, lab in zip(handles, labels):
- handler = self.get_legend_handler(legend_handler_map, orig_handle)
- if handler is None:
- cbook._warn_external(
- "Legend does not support {!r} instances.\nA proxy artist "
- "may be used instead.\nSee: "
- "http://matplotlib.org/users/legend_guide.html"
- "#creating-artists-specifically-for-adding-to-the-legend-"
- "aka-proxy-artists".format(orig_handle))
- # We don't have a handle for this artist, so we just defer
- # to None.
- handle_list.append(None)
- else:
- textbox = TextArea(lab, textprops=label_prop,
- multilinebaseline=True,
- minimumdescent=True)
- handlebox = DrawingArea(width=self.handlelength * fontsize,
- height=height,
- xdescent=0., ydescent=descent)
- text_list.append(textbox._text)
- # Create the artist for the legend which represents the
- # original artist/handle.
- handle_list.append(handler.legend_artist(self, orig_handle,
- fontsize, handlebox))
- handles_and_labels.append((handlebox, textbox))
- if handles_and_labels:
- # We calculate number of rows in each column. The first
- # (num_largecol) columns will have (nrows+1) rows, and remaining
- # (num_smallcol) columns will have (nrows) rows.
- ncol = min(self._ncol, len(handles_and_labels))
- nrows, num_largecol = divmod(len(handles_and_labels), ncol)
- num_smallcol = ncol - num_largecol
- # starting index of each column and number of rows in it.
- rows_per_col = [nrows + 1] * num_largecol + [nrows] * num_smallcol
- start_idxs = np.concatenate([[0], np.cumsum(rows_per_col)[:-1]])
- cols = zip(start_idxs, rows_per_col)
- else:
- cols = []
- columnbox = []
- for i0, di in cols:
- # pack handleBox and labelBox into itemBox
- itemBoxes = [HPacker(pad=0,
- sep=self.handletextpad * fontsize,
- children=[h, t] if markerfirst else [t, h],
- align="baseline")
- for h, t in handles_and_labels[i0:i0 + di]]
- # minimumdescent=False for the text of the last row of the column
- if markerfirst:
- itemBoxes[-1].get_children()[1].set_minimumdescent(False)
- else:
- itemBoxes[-1].get_children()[0].set_minimumdescent(False)
- # pack columnBox
- alignment = "baseline" if markerfirst else "right"
- columnbox.append(VPacker(pad=0,
- sep=self.labelspacing * fontsize,
- align=alignment,
- children=itemBoxes))
- mode = "expand" if self._mode == "expand" else "fixed"
- sep = self.columnspacing * fontsize
- self._legend_handle_box = HPacker(pad=0,
- sep=sep, align="baseline",
- mode=mode,
- children=columnbox)
- self._legend_title_box = TextArea("")
- self._legend_box = VPacker(pad=self.borderpad * fontsize,
- sep=self.labelspacing * fontsize,
- align="center",
- children=[self._legend_title_box,
- self._legend_handle_box])
- self._legend_box.set_figure(self.figure)
- self.texts = text_list
- self.legendHandles = handle_list
- def _auto_legend_data(self):
- """
- Returns list of vertices and extents covered by the plot.
- Returns a two long list.
- First element is a list of (x, y) vertices (in
- display-coordinates) covered by all the lines and line
- collections, in the legend's handles.
- Second element is a list of bounding boxes for all the patches in
- the legend's handles.
- """
- # should always hold because function is only called internally
- assert self.isaxes
- ax = self.parent
- bboxes = []
- lines = []
- offsets = []
- for handle in ax.lines:
- assert isinstance(handle, Line2D)
- path = handle.get_path()
- trans = handle.get_transform()
- tpath = trans.transform_path(path)
- lines.append(tpath)
- for handle in ax.patches:
- assert isinstance(handle, Patch)
- if isinstance(handle, Rectangle):
- transform = handle.get_data_transform()
- bboxes.append(handle.get_bbox().transformed(transform))
- else:
- transform = handle.get_transform()
- bboxes.append(handle.get_path().get_extents(transform))
- for handle in ax.collections:
- transform, transOffset, hoffsets, paths = handle._prepare_points()
- if len(hoffsets):
- for offset in transOffset.transform(hoffsets):
- offsets.append(offset)
- try:
- vertices = np.concatenate([l.vertices for l in lines])
- except ValueError:
- vertices = np.array([])
- return [vertices, bboxes, lines, offsets]
- def draw_frame(self, b):
- '''
- Set draw frame to b.
- Parameters
- ----------
- b : bool
- '''
- self.set_frame_on(b)
- def get_children(self):
- 'Return a list of child artists.'
- children = []
- if self._legend_box:
- children.append(self._legend_box)
- children.append(self.get_frame())
- return children
- def get_frame(self):
- '''
- Return the `~.patches.Rectangle` instances used to frame the legend.
- '''
- return self.legendPatch
- def get_lines(self):
- 'Return a list of `~.lines.Line2D` instances in the legend.'
- return [h for h in self.legendHandles if isinstance(h, Line2D)]
- def get_patches(self):
- 'Return a list of `~.patches.Patch` instances in the legend.'
- return silent_list('Patch',
- [h for h in self.legendHandles
- if isinstance(h, Patch)])
- def get_texts(self):
- 'Return a list of `~.text.Text` instances in the legend.'
- return silent_list('Text', self.texts)
- def set_title(self, title, prop=None):
- """
- Set the legend title. Fontproperties can be optionally set
- with *prop* parameter.
- """
- self._legend_title_box._text.set_text(title)
- if title:
- self._legend_title_box._text.set_visible(True)
- self._legend_title_box.set_visible(True)
- else:
- self._legend_title_box._text.set_visible(False)
- self._legend_title_box.set_visible(False)
- if prop is not None:
- if isinstance(prop, dict):
- prop = FontProperties(**prop)
- self._legend_title_box._text.set_fontproperties(prop)
- self.stale = True
- def get_title(self):
- 'Return the `.Text` instance for the legend title.'
- return self._legend_title_box._text
- def get_window_extent(self, renderer=None):
- 'Return extent of the legend.'
- if renderer is None:
- renderer = self.figure._cachedRenderer
- return self._legend_box.get_window_extent(renderer=renderer)
- def get_tightbbox(self, renderer):
- """
- Like `.Legend.get_window_extent`, but uses the box for the legend.
- Parameters
- ----------
- renderer : `.RendererBase` instance
- renderer that will be used to draw the figures (i.e.
- ``fig.canvas.get_renderer()``)
- Returns
- -------
- `.BboxBase` : containing the bounding box in figure pixel co-ordinates.
- """
- return self._legend_box.get_window_extent(renderer)
- def get_frame_on(self):
- """Get whether the legend box patch is drawn."""
- return self._drawFrame
- def set_frame_on(self, b):
- """
- Set whether the legend box patch is drawn.
- Parameters
- ----------
- b : bool
- """
- self._drawFrame = b
- self.stale = True
- def get_bbox_to_anchor(self):
- """Return the bbox that the legend will be anchored to."""
- if self._bbox_to_anchor is None:
- return self.parent.bbox
- else:
- return self._bbox_to_anchor
- def set_bbox_to_anchor(self, bbox, transform=None):
- """
- Set the bbox that the legend will be anchored to.
- *bbox* can be
- - A `.BboxBase` instance
- - A tuple of ``(left, bottom, width, height)`` in the given transform
- (normalized axes coordinate if None)
- - A tuple of ``(left, bottom)`` where the width and height will be
- assumed to be zero.
- """
- if bbox is None:
- self._bbox_to_anchor = None
- return
- elif isinstance(bbox, BboxBase):
- self._bbox_to_anchor = bbox
- else:
- try:
- l = len(bbox)
- except TypeError:
- raise ValueError("Invalid argument for bbox : %s" % str(bbox))
- if l == 2:
- bbox = [bbox[0], bbox[1], 0, 0]
- self._bbox_to_anchor = Bbox.from_bounds(*bbox)
- if transform is None:
- transform = BboxTransformTo(self.parent.bbox)
- self._bbox_to_anchor = TransformedBbox(self._bbox_to_anchor,
- transform)
- self.stale = True
- def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
- """
- Place the *bbox* inside the *parentbbox* according to a given
- location code. Return the (x, y) coordinate of the bbox.
- - loc: a location code in range(1, 11).
- This corresponds to the possible values for self._loc, excluding
- "best".
- - bbox: bbox to be placed, display coordinate units.
- - parentbbox: a parent box which will contain the bbox. In
- display coordinates.
- """
- assert loc in range(1, 11) # called only internally
- BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
- anchor_coefs = {UR: "NE",
- UL: "NW",
- LL: "SW",
- LR: "SE",
- R: "E",
- CL: "W",
- CR: "E",
- LC: "S",
- UC: "N",
- C: "C"}
- c = anchor_coefs[loc]
- fontsize = renderer.points_to_pixels(self._fontsize)
- container = parentbbox.padded(-(self.borderaxespad) * fontsize)
- anchored_box = bbox.anchored(c, container=container)
- return anchored_box.x0, anchored_box.y0
- def _find_best_position(self, width, height, renderer, consider=None):
- """
- Determine the best location to place the legend.
- *consider* is a list of ``(x, y)`` pairs to consider as a potential
- lower-left corner of the legend. All are display coords.
- """
- # should always hold because function is only called internally
- assert self.isaxes
- start_time = time.perf_counter()
- verts, bboxes, lines, offsets = self._auto_legend_data()
- bbox = Bbox.from_bounds(0, 0, width, height)
- if consider is None:
- consider = [self._get_anchored_bbox(x, bbox,
- self.get_bbox_to_anchor(),
- renderer)
- for x in range(1, len(self.codes))]
- candidates = []
- for idx, (l, b) in enumerate(consider):
- legendBox = Bbox.from_bounds(l, b, width, height)
- badness = 0
- # XXX TODO: If markers are present, it would be good to
- # take them into account when checking vertex overlaps in
- # the next line.
- badness = (legendBox.count_contains(verts)
- + legendBox.count_contains(offsets)
- + legendBox.count_overlaps(bboxes)
- + sum(line.intersects_bbox(legendBox, filled=False)
- for line in lines))
- if badness == 0:
- return l, b
- # Include the index to favor lower codes in case of a tie.
- candidates.append((badness, idx, (l, b)))
- _, _, (l, b) = min(candidates)
- if self._loc_used_default and time.perf_counter() - start_time > 1:
- cbook._warn_external(
- 'Creating legend with loc="best" can be slow with large '
- 'amounts of data.')
- return l, b
- def contains(self, event):
- inside, info = self._default_contains(event)
- if inside is not None:
- return inside, info
- return self.legendPatch.contains(event)
- def set_draggable(self, state, use_blit=False, update='loc'):
- """
- Enable or disable mouse dragging support of the legend.
- Parameters
- ----------
- state : bool
- Whether mouse dragging is enabled.
- use_blit : bool, optional
- Use blitting for faster image composition. For details see
- :ref:`func-animation`.
- update : {'loc', 'bbox'}, optional
- The legend parameter to be changed when dragged:
- - 'loc': update the *loc* parameter of the legend
- - 'bbox': update the *bbox_to_anchor* parameter of the legend
- Returns
- -------
- If *state* is ``True`` this returns the `~.DraggableLegend` helper
- instance. Otherwise this returns ``None``.
- """
- if state:
- if self._draggable is None:
- self._draggable = DraggableLegend(self,
- use_blit,
- update=update)
- else:
- if self._draggable is not None:
- self._draggable.disconnect()
- self._draggable = None
- return self._draggable
- def get_draggable(self):
- """Return ``True`` if the legend is draggable, ``False`` otherwise."""
- return self._draggable is not None
- # Helper functions to parse legend arguments for both `figure.legend` and
- # `axes.legend`:
- def _get_legend_handles(axs, legend_handler_map=None):
- """
- Return a generator of artists that can be used as handles in
- a legend.
- """
- handles_original = []
- for ax in axs:
- handles_original += (ax.lines + ax.patches +
- ax.collections + ax.containers)
- # support parasite axes:
- if hasattr(ax, 'parasites'):
- for axx in ax.parasites:
- handles_original += (axx.lines + axx.patches +
- axx.collections + axx.containers)
- handler_map = Legend.get_default_handler_map()
- if legend_handler_map is not None:
- handler_map = handler_map.copy()
- handler_map.update(legend_handler_map)
- has_handler = Legend.get_legend_handler
- for handle in handles_original:
- label = handle.get_label()
- if label != '_nolegend_' and has_handler(handler_map, handle):
- yield handle
- def _get_legend_handles_labels(axs, legend_handler_map=None):
- """
- Return handles and labels for legend, internal method.
- """
- handles = []
- labels = []
- for handle in _get_legend_handles(axs, legend_handler_map):
- label = handle.get_label()
- if label and not label.startswith('_'):
- handles.append(handle)
- labels.append(label)
- return handles, labels
- def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs):
- """
- Get the handles and labels from the calls to either ``figure.legend``
- or ``axes.legend``.
- ``axs`` is a list of axes (to get legend artists from)
- """
- log = logging.getLogger(__name__)
- handlers = kwargs.get('handler_map', {}) or {}
- extra_args = ()
- if (handles is not None or labels is not None) and args:
- cbook._warn_external("You have mixed positional and keyword "
- "arguments, some input may be discarded.")
- # if got both handles and labels as kwargs, make same length
- if handles and labels:
- handles, labels = zip(*zip(handles, labels))
- elif handles is not None and labels is None:
- labels = [handle.get_label() for handle in handles]
- elif labels is not None and handles is None:
- # Get as many handles as there are labels.
- handles = [handle for handle, label
- in zip(_get_legend_handles(axs, handlers), labels)]
- # No arguments - automatically detect labels and handles.
- elif len(args) == 0:
- handles, labels = _get_legend_handles_labels(axs, handlers)
- if not handles:
- log.warning('No handles with labels found to put in legend.')
- # One argument. User defined labels - automatic handle detection.
- elif len(args) == 1:
- labels, = args
- # Get as many handles as there are labels.
- handles = [handle for handle, label
- in zip(_get_legend_handles(axs, handlers), labels)]
- # Two arguments:
- # * user defined handles and labels
- elif len(args) >= 2:
- handles, labels = args[:2]
- extra_args = args[2:]
- else:
- raise TypeError('Invalid arguments to legend.')
- return handles, labels, extra_args, kwargs
|