You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1307 lines
48 KiB

4 years ago
  1. """
  2. The legend module defines the Legend class, which is responsible for
  3. drawing legends associated with axes and/or figures.
  4. .. important::
  5. It is unlikely that you would ever create a Legend instance
  6. manually. Most users would normally create a legend via the
  7. :meth:`~matplotlib.axes.Axes.legend` function. For more details on legends
  8. there is also a :doc:`legend guide </tutorials/intermediate/legend_guide>`.
  9. The Legend class can be considered as a container of legend handles and
  10. legend texts. Creation of corresponding legend handles from the plot elements
  11. in the axes or figures (e.g., lines, patches, etc.) are specified by the
  12. handler map, which defines the mapping between the plot elements and the
  13. legend handlers to be used (the default legend handlers are defined in the
  14. :mod:`~matplotlib.legend_handler` module). Note that not all kinds of
  15. artist are supported by the legend yet by default but it is possible to
  16. extend the legend handler's capabilities to support arbitrary objects. See
  17. the :doc:`legend guide </tutorials/intermediate/legend_guide>` for more
  18. information.
  19. """
  20. import logging
  21. import warnings
  22. import numpy as np
  23. from matplotlib import rcParams
  24. from matplotlib import docstring
  25. from matplotlib.artist import Artist, allow_rasterization
  26. from matplotlib.cbook import silent_list, is_hashable, warn_deprecated
  27. import matplotlib.colors as colors
  28. from matplotlib.font_manager import FontProperties
  29. from matplotlib.lines import Line2D
  30. from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch
  31. from matplotlib.collections import (LineCollection, RegularPolyCollection,
  32. CircleCollection, PathCollection,
  33. PolyCollection)
  34. from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
  35. from matplotlib.transforms import BboxTransformTo, BboxTransformFrom
  36. from matplotlib.offsetbox import HPacker, VPacker, TextArea, DrawingArea
  37. from matplotlib.offsetbox import DraggableOffsetBox
  38. from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer
  39. from . import legend_handler
  40. class DraggableLegend(DraggableOffsetBox):
  41. def __init__(self, legend, use_blit=False, update="loc"):
  42. """
  43. Wrapper around a `.Legend` to support mouse dragging.
  44. Parameters
  45. ----------
  46. legend : `.Legend`
  47. The `.Legend` instance to wrap.
  48. use_blit : bool, optional
  49. Use blitting for faster image composition. For details see
  50. :ref:`func-animation`.
  51. update : {'loc', 'bbox'}, optional
  52. If "loc", update the *loc* parameter of the legend upon finalizing.
  53. If "bbox", update the *bbox_to_anchor* parameter.
  54. """
  55. self.legend = legend
  56. if update in ["loc", "bbox"]:
  57. self._update = update
  58. else:
  59. raise ValueError("update parameter '%s' is not supported." %
  60. update)
  61. DraggableOffsetBox.__init__(self, legend, legend._legend_box,
  62. use_blit=use_blit)
  63. def artist_picker(self, legend, evt):
  64. return self.legend.contains(evt)
  65. def finalize_offset(self):
  66. loc_in_canvas = self.get_loc_in_canvas()
  67. if self._update == "loc":
  68. self._update_loc(loc_in_canvas)
  69. elif self._update == "bbox":
  70. self._update_bbox_to_anchor(loc_in_canvas)
  71. else:
  72. raise RuntimeError("update parameter '%s' is not supported." %
  73. self.update)
  74. def _update_loc(self, loc_in_canvas):
  75. bbox = self.legend.get_bbox_to_anchor()
  76. # if bbox has zero width or height, the transformation is
  77. # ill-defined. Fall back to the defaul bbox_to_anchor.
  78. if bbox.width == 0 or bbox.height == 0:
  79. self.legend.set_bbox_to_anchor(None)
  80. bbox = self.legend.get_bbox_to_anchor()
  81. _bbox_transform = BboxTransformFrom(bbox)
  82. self.legend._loc = tuple(
  83. _bbox_transform.transform_point(loc_in_canvas)
  84. )
  85. def _update_bbox_to_anchor(self, loc_in_canvas):
  86. tr = self.legend.axes.transAxes
  87. loc_in_bbox = tr.transform_point(loc_in_canvas)
  88. self.legend.set_bbox_to_anchor(loc_in_bbox)
  89. _legend_kw_doc = '''
  90. loc : int or string or pair of floats, default: :rc:`legend.loc` ('best' for \
  91. axes, 'upper right' for figures)
  92. The location of the legend. Possible codes are:
  93. =============== =============
  94. Location String Location Code
  95. =============== =============
  96. 'best' 0
  97. 'upper right' 1
  98. 'upper left' 2
  99. 'lower left' 3
  100. 'lower right' 4
  101. 'right' 5
  102. 'center left' 6
  103. 'center right' 7
  104. 'lower center' 8
  105. 'upper center' 9
  106. 'center' 10
  107. =============== =============
  108. Alternatively can be a 2-tuple giving ``x, y`` of the lower-left
  109. corner of the legend in axes coordinates (in which case
  110. ``bbox_to_anchor`` will be ignored).
  111. The 'best' option can be quite slow for plots with large amounts
  112. of data. Your plotting speed may benefit from providing a specific
  113. location.
  114. bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
  115. Box that is used to position the legend in conjunction with *loc*.
  116. Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or
  117. `figure.bbox` (if `.Figure.legend`). This argument allows arbitrary
  118. placement of the legend.
  119. Bbox coordinates are interpreted in the coordinate system given by
  120. `bbox_transform`, with the default transform
  121. Axes or Figure coordinates, depending on which ``legend`` is called.
  122. If a 4-tuple or `.BboxBase` is given, then it specifies the bbox
  123. ``(x, y, width, height)`` that the legend is placed in.
  124. To put the legend in the best location in the bottom right
  125. quadrant of the axes (or figure)::
  126. loc='best', bbox_to_anchor=(0.5, 0., 0.5, 0.5)
  127. A 2-tuple ``(x, y)`` places the corner of the legend specified by *loc* at
  128. x, y. For example, to put the legend's upper right-hand corner in the
  129. center of the axes (or figure) the following keywords can be used::
  130. loc='upper right', bbox_to_anchor=(0.5, 0.5)
  131. ncol : integer
  132. The number of columns that the legend has. Default is 1.
  133. prop : None or :class:`matplotlib.font_manager.FontProperties` or dict
  134. The font properties of the legend. If None (default), the current
  135. :data:`matplotlib.rcParams` will be used.
  136. fontsize : int or float or {'xx-small', 'x-small', 'small', 'medium', \
  137. 'large', 'x-large', 'xx-large'}
  138. Controls the font size of the legend. If the value is numeric the
  139. size will be the absolute font size in points. String values are
  140. relative to the current default font size. This argument is only
  141. used if `prop` is not specified.
  142. numpoints : None or int
  143. The number of marker points in the legend when creating a legend
  144. entry for a `.Line2D` (line).
  145. Default is ``None``, which will take the value from
  146. :rc:`legend.numpoints`.
  147. scatterpoints : None or int
  148. The number of marker points in the legend when creating
  149. a legend entry for a `.PathCollection` (scatter plot).
  150. Default is ``None``, which will take the value from
  151. :rc:`legend.scatterpoints`.
  152. scatteryoffsets : iterable of floats
  153. The vertical offset (relative to the font size) for the markers
  154. created for a scatter plot legend entry. 0.0 is at the base the
  155. legend text, and 1.0 is at the top. To draw all markers at the
  156. same height, set to ``[0.5]``. Default is ``[0.375, 0.5, 0.3125]``.
  157. markerscale : None or int or float
  158. The relative size of legend markers compared with the originally
  159. drawn ones.
  160. Default is ``None``, which will take the value from
  161. :rc:`legend.markerscale`.
  162. markerfirst : bool
  163. If *True*, legend marker is placed to the left of the legend label.
  164. If *False*, legend marker is placed to the right of the legend
  165. label.
  166. Default is *True*.
  167. frameon : None or bool
  168. Control whether the legend should be drawn on a patch
  169. (frame).
  170. Default is ``None``, which will take the value from
  171. :rc:`legend.frameon`.
  172. fancybox : None or bool
  173. Control whether round edges should be enabled around the
  174. :class:`~matplotlib.patches.FancyBboxPatch` which makes up the
  175. legend's background.
  176. Default is ``None``, which will take the value from
  177. :rc:`legend.fancybox`.
  178. shadow : None or bool
  179. Control whether to draw a shadow behind the legend.
  180. Default is ``None``, which will take the value from
  181. :rc:`legend.shadow`.
  182. framealpha : None or float
  183. Control the alpha transparency of the legend's background.
  184. Default is ``None``, which will take the value from
  185. :rc:`legend.framealpha`. If shadow is activated and
  186. *framealpha* is ``None``, the default value is ignored.
  187. facecolor : None or "inherit" or a color spec
  188. Control the legend's background color.
  189. Default is ``None``, which will take the value from
  190. :rc:`legend.facecolor`. If ``"inherit"``, it will take
  191. :rc:`axes.facecolor`.
  192. edgecolor : None or "inherit" or a color spec
  193. Control the legend's background patch edge color.
  194. Default is ``None``, which will take the value from
  195. :rc:`legend.edgecolor` If ``"inherit"``, it will take
  196. :rc:`axes.edgecolor`.
  197. mode : {"expand", None}
  198. If `mode` is set to ``"expand"`` the legend will be horizontally
  199. expanded to fill the axes area (or `bbox_to_anchor` if defines
  200. the legend's size).
  201. bbox_transform : None or :class:`matplotlib.transforms.Transform`
  202. The transform for the bounding box (`bbox_to_anchor`). For a value
  203. of ``None`` (default) the Axes'
  204. :data:`~matplotlib.axes.Axes.transAxes` transform will be used.
  205. title : str or None
  206. The legend's title. Default is no title (``None``).
  207. title_fontsize: str or None
  208. The fontsize of the legend's title. Default is the default fontsize.
  209. borderpad : float or None
  210. The fractional whitespace inside the legend border.
  211. Measured in font-size units.
  212. Default is ``None``, which will take the value from
  213. :rc:`legend.borderpad`.
  214. labelspacing : float or None
  215. The vertical space between the legend entries.
  216. Measured in font-size units.
  217. Default is ``None``, which will take the value from
  218. :rc:`legend.labelspacing`.
  219. handlelength : float or None
  220. The length of the legend handles.
  221. Measured in font-size units.
  222. Default is ``None``, which will take the value from
  223. :rc:`legend.handlelength`.
  224. handletextpad : float or None
  225. The pad between the legend handle and text.
  226. Measured in font-size units.
  227. Default is ``None``, which will take the value from
  228. :rc:`legend.handletextpad`.
  229. borderaxespad : float or None
  230. The pad between the axes and legend border.
  231. Measured in font-size units.
  232. Default is ``None``, which will take the value from
  233. :rc:`legend.borderaxespad`.
  234. columnspacing : float or None
  235. The spacing between columns.
  236. Measured in font-size units.
  237. Default is ``None``, which will take the value from
  238. :rc:`legend.columnspacing`.
  239. handler_map : dict or None
  240. The custom dictionary mapping instances or types to a legend
  241. handler. This `handler_map` updates the default handler map
  242. found at :func:`matplotlib.legend.Legend.get_legend_handler_map`.
  243. '''
  244. docstring.interpd.update(_legend_kw_doc=_legend_kw_doc)
  245. class Legend(Artist):
  246. """
  247. Place a legend on the axes at location loc.
  248. """
  249. codes = {'best': 0, # only implemented for axes legends
  250. 'upper right': 1,
  251. 'upper left': 2,
  252. 'lower left': 3,
  253. 'lower right': 4,
  254. 'right': 5,
  255. 'center left': 6,
  256. 'center right': 7,
  257. 'lower center': 8,
  258. 'upper center': 9,
  259. 'center': 10,
  260. }
  261. zorder = 5
  262. def __str__(self):
  263. return "Legend"
  264. @docstring.dedent_interpd
  265. def __init__(self, parent, handles, labels,
  266. loc=None,
  267. numpoints=None, # the number of points in the legend line
  268. markerscale=None, # the relative size of legend markers
  269. # vs. original
  270. markerfirst=True, # controls ordering (left-to-right) of
  271. # legend marker and label
  272. scatterpoints=None, # number of scatter points
  273. scatteryoffsets=None,
  274. prop=None, # properties for the legend texts
  275. fontsize=None, # keyword to set font size directly
  276. # spacing & pad defined as a fraction of the font-size
  277. borderpad=None, # the whitespace inside the legend border
  278. labelspacing=None, # the vertical space between the legend
  279. # entries
  280. handlelength=None, # the length of the legend handles
  281. handleheight=None, # the height of the legend handles
  282. handletextpad=None, # the pad between the legend handle
  283. # and text
  284. borderaxespad=None, # the pad between the axes and legend
  285. # border
  286. columnspacing=None, # spacing between columns
  287. ncol=1, # number of columns
  288. mode=None, # mode for horizontal distribution of columns.
  289. # None, "expand"
  290. fancybox=None, # True use a fancy box, false use a rounded
  291. # box, none use rc
  292. shadow=None,
  293. title=None, # set a title for the legend
  294. title_fontsize=None, # set to ax.fontsize if None
  295. framealpha=None, # set frame alpha
  296. edgecolor=None, # frame patch edgecolor
  297. facecolor=None, # frame patch facecolor
  298. bbox_to_anchor=None, # bbox that the legend will be anchored.
  299. bbox_transform=None, # transform for the bbox
  300. frameon=None, # draw frame
  301. handler_map=None,
  302. ):
  303. """
  304. Parameters
  305. ----------
  306. parent : `~matplotlib.axes.Axes` or `.Figure`
  307. The artist that contains the legend.
  308. handles : sequence of `.Artist`
  309. A list of Artists (lines, patches) to be added to the legend.
  310. labels : sequence of strings
  311. A list of labels to show next to the artists. The length of handles
  312. and labels should be the same. If they are not, they are truncated
  313. to the smaller of both lengths.
  314. Other Parameters
  315. ----------------
  316. %(_legend_kw_doc)s
  317. Notes
  318. -----
  319. Users can specify any arbitrary location for the legend using the
  320. *bbox_to_anchor* keyword argument. bbox_to_anchor can be an instance
  321. of BboxBase(or its derivatives) or a tuple of 2 or 4 floats.
  322. See :meth:`set_bbox_to_anchor` for more detail.
  323. The legend location can be specified by setting *loc* with a tuple of
  324. 2 floats, which is interpreted as the lower-left corner of the legend
  325. in the normalized axes coordinate.
  326. """
  327. # local import only to avoid circularity
  328. from matplotlib.axes import Axes
  329. from matplotlib.figure import Figure
  330. Artist.__init__(self)
  331. if prop is None:
  332. if fontsize is not None:
  333. self.prop = FontProperties(size=fontsize)
  334. else:
  335. self.prop = FontProperties(size=rcParams["legend.fontsize"])
  336. elif isinstance(prop, dict):
  337. self.prop = FontProperties(**prop)
  338. if "size" not in prop:
  339. self.prop.set_size(rcParams["legend.fontsize"])
  340. else:
  341. self.prop = prop
  342. self._fontsize = self.prop.get_size_in_points()
  343. self.texts = []
  344. self.legendHandles = []
  345. self._legend_title_box = None
  346. #: A dictionary with the extra handler mappings for this Legend
  347. #: instance.
  348. self._custom_handler_map = handler_map
  349. locals_view = locals()
  350. for name in ["numpoints", "markerscale", "shadow", "columnspacing",
  351. "scatterpoints", "handleheight", 'borderpad',
  352. 'labelspacing', 'handlelength', 'handletextpad',
  353. 'borderaxespad']:
  354. if locals_view[name] is None:
  355. value = rcParams["legend." + name]
  356. else:
  357. value = locals_view[name]
  358. setattr(self, name, value)
  359. del locals_view
  360. # trim handles and labels if illegal label...
  361. _lab, _hand = [], []
  362. for label, handle in zip(labels, handles):
  363. if isinstance(label, str) and label.startswith('_'):
  364. warnings.warn('The handle {!r} has a label of {!r} which '
  365. 'cannot be automatically added to the '
  366. 'legend.'.format(handle, label))
  367. else:
  368. _lab.append(label)
  369. _hand.append(handle)
  370. labels, handles = _lab, _hand
  371. handles = list(handles)
  372. if len(handles) < 2:
  373. ncol = 1
  374. self._ncol = ncol
  375. if self.numpoints <= 0:
  376. raise ValueError("numpoints must be > 0; it was %d" % numpoints)
  377. # introduce y-offset for handles of the scatter plot
  378. if scatteryoffsets is None:
  379. self._scatteryoffsets = np.array([3. / 8., 4. / 8., 2.5 / 8.])
  380. else:
  381. self._scatteryoffsets = np.asarray(scatteryoffsets)
  382. reps = self.scatterpoints // len(self._scatteryoffsets) + 1
  383. self._scatteryoffsets = np.tile(self._scatteryoffsets,
  384. reps)[:self.scatterpoints]
  385. # _legend_box is an OffsetBox instance that contains all
  386. # legend items and will be initialized from _init_legend_box()
  387. # method.
  388. self._legend_box = None
  389. if isinstance(parent, Axes):
  390. self.isaxes = True
  391. self.axes = parent
  392. self.set_figure(parent.figure)
  393. elif isinstance(parent, Figure):
  394. self.isaxes = False
  395. self.set_figure(parent)
  396. else:
  397. raise TypeError("Legend needs either Axes or Figure as parent")
  398. self.parent = parent
  399. if loc is None:
  400. loc = rcParams["legend.loc"]
  401. if not self.isaxes and loc in [0, 'best']:
  402. loc = 'upper right'
  403. if isinstance(loc, str):
  404. if loc not in self.codes:
  405. if self.isaxes:
  406. warnings.warn('Unrecognized location "%s". Falling back '
  407. 'on "best"; valid locations are\n\t%s\n'
  408. % (loc, '\n\t'.join(self.codes)))
  409. loc = 0
  410. else:
  411. warnings.warn('Unrecognized location "%s". Falling back '
  412. 'on "upper right"; '
  413. 'valid locations are\n\t%s\n'
  414. % (loc, '\n\t'.join(self.codes)))
  415. loc = 1
  416. else:
  417. loc = self.codes[loc]
  418. if not self.isaxes and loc == 0:
  419. warnings.warn('Automatic legend placement (loc="best") not '
  420. 'implemented for figure legend. '
  421. 'Falling back on "upper right".')
  422. loc = 1
  423. self._mode = mode
  424. self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
  425. # We use FancyBboxPatch to draw a legend frame. The location
  426. # and size of the box will be updated during the drawing time.
  427. if facecolor is None:
  428. facecolor = rcParams["legend.facecolor"]
  429. if facecolor == 'inherit':
  430. facecolor = rcParams["axes.facecolor"]
  431. if edgecolor is None:
  432. edgecolor = rcParams["legend.edgecolor"]
  433. if edgecolor == 'inherit':
  434. edgecolor = rcParams["axes.edgecolor"]
  435. self.legendPatch = FancyBboxPatch(
  436. xy=(0.0, 0.0), width=1., height=1.,
  437. facecolor=facecolor,
  438. edgecolor=edgecolor,
  439. mutation_scale=self._fontsize,
  440. snap=True
  441. )
  442. # The width and height of the legendPatch will be set (in the
  443. # draw()) to the length that includes the padding. Thus we set
  444. # pad=0 here.
  445. if fancybox is None:
  446. fancybox = rcParams["legend.fancybox"]
  447. if fancybox:
  448. self.legendPatch.set_boxstyle("round", pad=0,
  449. rounding_size=0.2)
  450. else:
  451. self.legendPatch.set_boxstyle("square", pad=0)
  452. self._set_artist_props(self.legendPatch)
  453. self._drawFrame = frameon
  454. if frameon is None:
  455. self._drawFrame = rcParams["legend.frameon"]
  456. # init with null renderer
  457. self._init_legend_box(handles, labels, markerfirst)
  458. # If shadow is activated use framealpha if not
  459. # explicitly passed. See Issue 8943
  460. if framealpha is None:
  461. if shadow:
  462. self.get_frame().set_alpha(1)
  463. else:
  464. self.get_frame().set_alpha(rcParams["legend.framealpha"])
  465. else:
  466. self.get_frame().set_alpha(framealpha)
  467. self._loc = loc
  468. # figure out title fontsize:
  469. if title_fontsize is None:
  470. title_fontsize = rcParams['legend.title_fontsize']
  471. tprop = FontProperties(size=title_fontsize)
  472. self.set_title(title, prop=tprop)
  473. self._last_fontsize_points = self._fontsize
  474. self._draggable = None
  475. def _set_artist_props(self, a):
  476. """
  477. Set the boilerplate props for artists added to axes.
  478. """
  479. a.set_figure(self.figure)
  480. if self.isaxes:
  481. # a.set_axes(self.axes)
  482. a.axes = self.axes
  483. a.set_transform(self.get_transform())
  484. def _set_loc(self, loc):
  485. # find_offset function will be provided to _legend_box and
  486. # _legend_box will draw itself at the location of the return
  487. # value of the find_offset.
  488. self._loc_real = loc
  489. self.stale = True
  490. self._legend_box.set_offset(self._findoffset)
  491. def _get_loc(self):
  492. return self._loc_real
  493. _loc = property(_get_loc, _set_loc)
  494. def _findoffset(self, width, height, xdescent, ydescent, renderer):
  495. "Helper function to locate the legend."
  496. if self._loc == 0: # "best".
  497. x, y = self._find_best_position(width, height, renderer)
  498. elif self._loc in Legend.codes.values(): # Fixed location.
  499. bbox = Bbox.from_bounds(0, 0, width, height)
  500. x, y = self._get_anchored_bbox(self._loc, bbox,
  501. self.get_bbox_to_anchor(),
  502. renderer)
  503. else: # Axes or figure coordinates.
  504. fx, fy = self._loc
  505. bbox = self.get_bbox_to_anchor()
  506. x, y = bbox.x0 + bbox.width * fx, bbox.y0 + bbox.height * fy
  507. return x + xdescent, y + ydescent
  508. @allow_rasterization
  509. def draw(self, renderer):
  510. "Draw everything that belongs to the legend."
  511. if not self.get_visible():
  512. return
  513. renderer.open_group('legend')
  514. fontsize = renderer.points_to_pixels(self._fontsize)
  515. # if mode == fill, set the width of the legend_box to the
  516. # width of the paret (minus pads)
  517. if self._mode in ["expand"]:
  518. pad = 2 * (self.borderaxespad + self.borderpad) * fontsize
  519. self._legend_box.set_width(self.get_bbox_to_anchor().width - pad)
  520. # update the location and size of the legend. This needs to
  521. # be done in any case to clip the figure right.
  522. bbox = self._legend_box.get_window_extent(renderer)
  523. self.legendPatch.set_bounds(bbox.x0, bbox.y0,
  524. bbox.width, bbox.height)
  525. self.legendPatch.set_mutation_scale(fontsize)
  526. if self._drawFrame:
  527. if self.shadow:
  528. shadow = Shadow(self.legendPatch, 2, -2)
  529. shadow.draw(renderer)
  530. self.legendPatch.draw(renderer)
  531. self._legend_box.draw(renderer)
  532. renderer.close_group('legend')
  533. self.stale = False
  534. def _approx_text_height(self, renderer=None):
  535. """
  536. Return the approximate height of the text. This is used to place
  537. the legend handle.
  538. """
  539. if renderer is None:
  540. return self._fontsize
  541. else:
  542. return renderer.points_to_pixels(self._fontsize)
  543. # _default_handler_map defines the default mapping between plot
  544. # elements and the legend handlers.
  545. _default_handler_map = {
  546. StemContainer: legend_handler.HandlerStem(),
  547. ErrorbarContainer: legend_handler.HandlerErrorbar(),
  548. Line2D: legend_handler.HandlerLine2D(),
  549. Patch: legend_handler.HandlerPatch(),
  550. LineCollection: legend_handler.HandlerLineCollection(),
  551. RegularPolyCollection: legend_handler.HandlerRegularPolyCollection(),
  552. CircleCollection: legend_handler.HandlerCircleCollection(),
  553. BarContainer: legend_handler.HandlerPatch(
  554. update_func=legend_handler.update_from_first_child),
  555. tuple: legend_handler.HandlerTuple(),
  556. PathCollection: legend_handler.HandlerPathCollection(),
  557. PolyCollection: legend_handler.HandlerPolyCollection()
  558. }
  559. # (get|set|update)_default_handler_maps are public interfaces to
  560. # modify the default handler map.
  561. @classmethod
  562. def get_default_handler_map(cls):
  563. """
  564. A class method that returns the default handler map.
  565. """
  566. return cls._default_handler_map
  567. @classmethod
  568. def set_default_handler_map(cls, handler_map):
  569. """
  570. A class method to set the default handler map.
  571. """
  572. cls._default_handler_map = handler_map
  573. @classmethod
  574. def update_default_handler_map(cls, handler_map):
  575. """
  576. A class method to update the default handler map.
  577. """
  578. cls._default_handler_map.update(handler_map)
  579. def get_legend_handler_map(self):
  580. """
  581. Return the handler map.
  582. """
  583. default_handler_map = self.get_default_handler_map()
  584. if self._custom_handler_map:
  585. hm = default_handler_map.copy()
  586. hm.update(self._custom_handler_map)
  587. return hm
  588. else:
  589. return default_handler_map
  590. @staticmethod
  591. def get_legend_handler(legend_handler_map, orig_handle):
  592. """
  593. Return a legend handler from *legend_handler_map* that
  594. corresponds to *orig_handler*.
  595. *legend_handler_map* should be a dictionary object (that is
  596. returned by the get_legend_handler_map method).
  597. It first checks if the *orig_handle* itself is a key in the
  598. *legend_hanler_map* and return the associated value.
  599. Otherwise, it checks for each of the classes in its
  600. method-resolution-order. If no matching key is found, it
  601. returns ``None``.
  602. """
  603. if is_hashable(orig_handle):
  604. try:
  605. return legend_handler_map[orig_handle]
  606. except KeyError:
  607. pass
  608. for handle_type in type(orig_handle).mro():
  609. try:
  610. return legend_handler_map[handle_type]
  611. except KeyError:
  612. pass
  613. return None
  614. def _init_legend_box(self, handles, labels, markerfirst=True):
  615. """
  616. Initialize the legend_box. The legend_box is an instance of
  617. the OffsetBox, which is packed with legend handles and
  618. texts. Once packed, their location is calculated during the
  619. drawing time.
  620. """
  621. fontsize = self._fontsize
  622. # legend_box is a HPacker, horizontally packed with
  623. # columns. Each column is a VPacker, vertically packed with
  624. # legend items. Each legend item is HPacker packed with
  625. # legend handleBox and labelBox. handleBox is an instance of
  626. # offsetbox.DrawingArea which contains legend handle. labelBox
  627. # is an instance of offsetbox.TextArea which contains legend
  628. # text.
  629. text_list = [] # the list of text instances
  630. handle_list = [] # the list of text instances
  631. handles_and_labels = []
  632. label_prop = dict(verticalalignment='baseline',
  633. horizontalalignment='left',
  634. fontproperties=self.prop,
  635. )
  636. # The approximate height and descent of text. These values are
  637. # only used for plotting the legend handle.
  638. descent = 0.35 * self._approx_text_height() * (self.handleheight - 0.7)
  639. # 0.35 and 0.7 are just heuristic numbers and may need to be improved.
  640. height = self._approx_text_height() * self.handleheight - descent
  641. # each handle needs to be drawn inside a box of (x, y, w, h) =
  642. # (0, -descent, width, height). And their coordinates should
  643. # be given in the display coordinates.
  644. # The transformation of each handle will be automatically set
  645. # to self.get_trasnform(). If the artist does not use its
  646. # default transform (e.g., Collections), you need to
  647. # manually set their transform to the self.get_transform().
  648. legend_handler_map = self.get_legend_handler_map()
  649. for orig_handle, lab in zip(handles, labels):
  650. handler = self.get_legend_handler(legend_handler_map, orig_handle)
  651. if handler is None:
  652. warnings.warn(
  653. "Legend does not support {!r} instances.\nA proxy artist "
  654. "may be used instead.\nSee: "
  655. "http://matplotlib.org/users/legend_guide.html"
  656. "#creating-artists-specifically-for-adding-to-the-legend-"
  657. "aka-proxy-artists".format(orig_handle)
  658. )
  659. # We don't have a handle for this artist, so we just defer
  660. # to None.
  661. handle_list.append(None)
  662. else:
  663. textbox = TextArea(lab, textprops=label_prop,
  664. multilinebaseline=True,
  665. minimumdescent=True)
  666. handlebox = DrawingArea(width=self.handlelength * fontsize,
  667. height=height,
  668. xdescent=0., ydescent=descent)
  669. text_list.append(textbox._text)
  670. # Create the artist for the legend which represents the
  671. # original artist/handle.
  672. handle_list.append(handler.legend_artist(self, orig_handle,
  673. fontsize, handlebox))
  674. handles_and_labels.append((handlebox, textbox))
  675. if handles_and_labels:
  676. # We calculate number of rows in each column. The first
  677. # (num_largecol) columns will have (nrows+1) rows, and remaining
  678. # (num_smallcol) columns will have (nrows) rows.
  679. ncol = min(self._ncol, len(handles_and_labels))
  680. nrows, num_largecol = divmod(len(handles_and_labels), ncol)
  681. num_smallcol = ncol - num_largecol
  682. # starting index of each column and number of rows in it.
  683. rows_per_col = [nrows + 1] * num_largecol + [nrows] * num_smallcol
  684. start_idxs = np.concatenate([[0], np.cumsum(rows_per_col)[:-1]])
  685. cols = zip(start_idxs, rows_per_col)
  686. else:
  687. cols = []
  688. columnbox = []
  689. for i0, di in cols:
  690. # pack handleBox and labelBox into itemBox
  691. itemBoxes = [HPacker(pad=0,
  692. sep=self.handletextpad * fontsize,
  693. children=[h, t] if markerfirst else [t, h],
  694. align="baseline")
  695. for h, t in handles_and_labels[i0:i0 + di]]
  696. # minimumdescent=False for the text of the last row of the column
  697. if markerfirst:
  698. itemBoxes[-1].get_children()[1].set_minimumdescent(False)
  699. else:
  700. itemBoxes[-1].get_children()[0].set_minimumdescent(False)
  701. # pack columnBox
  702. alignment = "baseline" if markerfirst else "right"
  703. columnbox.append(VPacker(pad=0,
  704. sep=self.labelspacing * fontsize,
  705. align=alignment,
  706. children=itemBoxes))
  707. mode = "expand" if self._mode == "expand" else "fixed"
  708. sep = self.columnspacing * fontsize
  709. self._legend_handle_box = HPacker(pad=0,
  710. sep=sep, align="baseline",
  711. mode=mode,
  712. children=columnbox)
  713. self._legend_title_box = TextArea("")
  714. self._legend_box = VPacker(pad=self.borderpad * fontsize,
  715. sep=self.labelspacing * fontsize,
  716. align="center",
  717. children=[self._legend_title_box,
  718. self._legend_handle_box])
  719. self._legend_box.set_figure(self.figure)
  720. self.texts = text_list
  721. self.legendHandles = handle_list
  722. def _auto_legend_data(self):
  723. """
  724. Returns list of vertices and extents covered by the plot.
  725. Returns a two long list.
  726. First element is a list of (x, y) vertices (in
  727. display-coordinates) covered by all the lines and line
  728. collections, in the legend's handles.
  729. Second element is a list of bounding boxes for all the patches in
  730. the legend's handles.
  731. """
  732. # should always hold because function is only called internally
  733. assert self.isaxes
  734. ax = self.parent
  735. bboxes = []
  736. lines = []
  737. offsets = []
  738. for handle in ax.lines:
  739. assert isinstance(handle, Line2D)
  740. path = handle.get_path()
  741. trans = handle.get_transform()
  742. tpath = trans.transform_path(path)
  743. lines.append(tpath)
  744. for handle in ax.patches:
  745. assert isinstance(handle, Patch)
  746. if isinstance(handle, Rectangle):
  747. transform = handle.get_data_transform()
  748. bboxes.append(handle.get_bbox().transformed(transform))
  749. else:
  750. transform = handle.get_transform()
  751. bboxes.append(handle.get_path().get_extents(transform))
  752. for handle in ax.collections:
  753. transform, transOffset, hoffsets, paths = handle._prepare_points()
  754. if len(hoffsets):
  755. for offset in transOffset.transform(hoffsets):
  756. offsets.append(offset)
  757. try:
  758. vertices = np.concatenate([l.vertices for l in lines])
  759. except ValueError:
  760. vertices = np.array([])
  761. return [vertices, bboxes, lines, offsets]
  762. def draw_frame(self, b):
  763. '''
  764. Set draw frame to b.
  765. Parameters
  766. ----------
  767. b : bool
  768. '''
  769. self.set_frame_on(b)
  770. def get_children(self):
  771. 'Return a list of child artists.'
  772. children = []
  773. if self._legend_box:
  774. children.append(self._legend_box)
  775. children.append(self.get_frame())
  776. return children
  777. def get_frame(self):
  778. '''
  779. Return the `~.patches.Rectangle` instances used to frame the legend.
  780. '''
  781. return self.legendPatch
  782. def get_lines(self):
  783. 'Return a list of `~.lines.Line2D` instances in the legend.'
  784. return [h for h in self.legendHandles if isinstance(h, Line2D)]
  785. def get_patches(self):
  786. 'Return a list of `~.patches.Patch` instances in the legend.'
  787. return silent_list('Patch',
  788. [h for h in self.legendHandles
  789. if isinstance(h, Patch)])
  790. def get_texts(self):
  791. 'Return a list of `~.text.Text` instances in the legend.'
  792. return silent_list('Text', self.texts)
  793. def set_title(self, title, prop=None):
  794. """
  795. Set the legend title. Fontproperties can be optionally set
  796. with *prop* parameter.
  797. """
  798. self._legend_title_box._text.set_text(title)
  799. if title:
  800. self._legend_title_box._text.set_visible(True)
  801. self._legend_title_box.set_visible(True)
  802. else:
  803. self._legend_title_box._text.set_visible(False)
  804. self._legend_title_box.set_visible(False)
  805. if prop is not None:
  806. if isinstance(prop, dict):
  807. prop = FontProperties(**prop)
  808. self._legend_title_box._text.set_fontproperties(prop)
  809. self.stale = True
  810. def get_title(self):
  811. 'Return the `.Text` instance for the legend title.'
  812. return self._legend_title_box._text
  813. def get_window_extent(self, renderer=None):
  814. 'Return extent of the legend.'
  815. if renderer is None:
  816. renderer = self.figure._cachedRenderer
  817. return self._legend_box.get_window_extent(renderer=renderer)
  818. def get_tightbbox(self, renderer):
  819. """
  820. Like `.Legend.get_window_extent`, but uses the box for the legend.
  821. Parameters
  822. ----------
  823. renderer : `.RendererBase` instance
  824. renderer that will be used to draw the figures (i.e.
  825. ``fig.canvas.get_renderer()``)
  826. Returns
  827. -------
  828. `.BboxBase` : containing the bounding box in figure pixel co-ordinates.
  829. """
  830. return self._legend_box.get_window_extent(renderer)
  831. def get_frame_on(self):
  832. """Get whether the legend box patch is drawn."""
  833. return self._drawFrame
  834. def set_frame_on(self, b):
  835. """
  836. Set whether the legend box patch is drawn.
  837. Parameters
  838. ----------
  839. b : bool
  840. """
  841. self._drawFrame = b
  842. self.stale = True
  843. def get_bbox_to_anchor(self):
  844. """Return the bbox that the legend will be anchored to."""
  845. if self._bbox_to_anchor is None:
  846. return self.parent.bbox
  847. else:
  848. return self._bbox_to_anchor
  849. def set_bbox_to_anchor(self, bbox, transform=None):
  850. """
  851. Set the bbox that the legend will be anchored to.
  852. *bbox* can be
  853. - A `.BboxBase` instance
  854. - A tuple of ``(left, bottom, width, height)`` in the given transform
  855. (normalized axes coordinate if None)
  856. - A tuple of ``(left, bottom)`` where the width and height will be
  857. assumed to be zero.
  858. """
  859. if bbox is None:
  860. self._bbox_to_anchor = None
  861. return
  862. elif isinstance(bbox, BboxBase):
  863. self._bbox_to_anchor = bbox
  864. else:
  865. try:
  866. l = len(bbox)
  867. except TypeError:
  868. raise ValueError("Invalid argument for bbox : %s" % str(bbox))
  869. if l == 2:
  870. bbox = [bbox[0], bbox[1], 0, 0]
  871. self._bbox_to_anchor = Bbox.from_bounds(*bbox)
  872. if transform is None:
  873. transform = BboxTransformTo(self.parent.bbox)
  874. self._bbox_to_anchor = TransformedBbox(self._bbox_to_anchor,
  875. transform)
  876. self.stale = True
  877. def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer):
  878. """
  879. Place the *bbox* inside the *parentbbox* according to a given
  880. location code. Return the (x,y) coordinate of the bbox.
  881. - loc: a location code in range(1, 11).
  882. This corresponds to the possible values for self._loc, excluding
  883. "best".
  884. - bbox: bbox to be placed, display coordinate units.
  885. - parentbbox: a parent box which will contain the bbox. In
  886. display coordinates.
  887. """
  888. assert loc in range(1, 11) # called only internally
  889. BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
  890. anchor_coefs = {UR: "NE",
  891. UL: "NW",
  892. LL: "SW",
  893. LR: "SE",
  894. R: "E",
  895. CL: "W",
  896. CR: "E",
  897. LC: "S",
  898. UC: "N",
  899. C: "C"}
  900. c = anchor_coefs[loc]
  901. fontsize = renderer.points_to_pixels(self._fontsize)
  902. container = parentbbox.padded(-(self.borderaxespad) * fontsize)
  903. anchored_box = bbox.anchored(c, container=container)
  904. return anchored_box.x0, anchored_box.y0
  905. def _find_best_position(self, width, height, renderer, consider=None):
  906. """
  907. Determine the best location to place the legend.
  908. *consider* is a list of ``(x, y)`` pairs to consider as a potential
  909. lower-left corner of the legend. All are display coords.
  910. """
  911. # should always hold because function is only called internally
  912. assert self.isaxes
  913. verts, bboxes, lines, offsets = self._auto_legend_data()
  914. bbox = Bbox.from_bounds(0, 0, width, height)
  915. if consider is None:
  916. consider = [self._get_anchored_bbox(x, bbox,
  917. self.get_bbox_to_anchor(),
  918. renderer)
  919. for x in range(1, len(self.codes))]
  920. candidates = []
  921. for idx, (l, b) in enumerate(consider):
  922. legendBox = Bbox.from_bounds(l, b, width, height)
  923. badness = 0
  924. # XXX TODO: If markers are present, it would be good to
  925. # take them into account when checking vertex overlaps in
  926. # the next line.
  927. badness = (legendBox.count_contains(verts)
  928. + legendBox.count_contains(offsets)
  929. + legendBox.count_overlaps(bboxes)
  930. + sum(line.intersects_bbox(legendBox, filled=False)
  931. for line in lines))
  932. if badness == 0:
  933. return l, b
  934. # Include the index to favor lower codes in case of a tie.
  935. candidates.append((badness, idx, (l, b)))
  936. _, _, (l, b) = min(candidates)
  937. return l, b
  938. def contains(self, event):
  939. return self.legendPatch.contains(event)
  940. def set_draggable(self, state, use_blit=False, update='loc'):
  941. """
  942. Enable or disable mouse dragging support of the legend.
  943. Parameters
  944. ----------
  945. state : bool
  946. Whether mouse dragging is enabled.
  947. use_blit : bool, optional
  948. Use blitting for faster image composition. For details see
  949. :ref:`func-animation`.
  950. update : {'loc', 'bbox'}, optional
  951. The legend parameter to be changed when dragged:
  952. - 'loc': update the *loc* parameter of the legend
  953. - 'bbox': update the *bbox_to_anchor* parameter of the legend
  954. Returns
  955. -------
  956. If *state* is ``True`` this returns the `~.DraggableLegend` helper
  957. instance. Otherwise this returns ``None``.
  958. """
  959. if state:
  960. if self._draggable is None:
  961. self._draggable = DraggableLegend(self,
  962. use_blit,
  963. update=update)
  964. else:
  965. if self._draggable is not None:
  966. self._draggable.disconnect()
  967. self._draggable = None
  968. return self._draggable
  969. def get_draggable(self):
  970. """Return ``True`` if the legend is draggable, ``False`` otherwise."""
  971. return self._draggable is not None
  972. def draggable(self, state=None, use_blit=False, update="loc"):
  973. """
  974. Set the draggable state -- if state is
  975. * None : toggle the current state
  976. * True : turn draggable on
  977. * False : turn draggable off
  978. If draggable is on, you can drag the legend on the canvas with
  979. the mouse. The `.DraggableLegend` helper instance is returned if
  980. draggable is on.
  981. The update parameter control which parameter of the legend changes
  982. when dragged. If update is "loc", the *loc* parameter of the legend
  983. is changed. If "bbox", the *bbox_to_anchor* parameter is changed.
  984. """
  985. warn_deprecated("2.2",
  986. message="Legend.draggable() is drepecated in "
  987. "favor of Legend.set_draggable(). "
  988. "Legend.draggable may be reintroduced as a "
  989. "property in future releases.")
  990. if state is None:
  991. state = not self.get_draggable() # toggle state
  992. self.set_draggable(state, use_blit, update)
  993. return self._draggable
  994. # Helper functions to parse legend arguments for both `figure.legend` and
  995. # `axes.legend`:
  996. def _get_legend_handles(axs, legend_handler_map=None):
  997. """
  998. Return a generator of artists that can be used as handles in
  999. a legend.
  1000. """
  1001. handles_original = []
  1002. for ax in axs:
  1003. handles_original += (ax.lines + ax.patches +
  1004. ax.collections + ax.containers)
  1005. # support parasite axes:
  1006. if hasattr(ax, 'parasites'):
  1007. for axx in ax.parasites:
  1008. handles_original += (axx.lines + axx.patches +
  1009. axx.collections + axx.containers)
  1010. handler_map = Legend.get_default_handler_map()
  1011. if legend_handler_map is not None:
  1012. handler_map = handler_map.copy()
  1013. handler_map.update(legend_handler_map)
  1014. has_handler = Legend.get_legend_handler
  1015. for handle in handles_original:
  1016. label = handle.get_label()
  1017. if label != '_nolegend_' and has_handler(handler_map, handle):
  1018. yield handle
  1019. def _get_legend_handles_labels(axs, legend_handler_map=None):
  1020. """
  1021. Return handles and labels for legend, internal method.
  1022. """
  1023. handles = []
  1024. labels = []
  1025. for handle in _get_legend_handles(axs, legend_handler_map):
  1026. label = handle.get_label()
  1027. if label and not label.startswith('_'):
  1028. handles.append(handle)
  1029. labels.append(label)
  1030. return handles, labels
  1031. def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs):
  1032. """
  1033. Get the handles and labels from the calls to either ``figure.legend``
  1034. or ``axes.legend``.
  1035. ``axs`` is a list of axes (to get legend artists from)
  1036. """
  1037. log = logging.getLogger(__name__)
  1038. handlers = kwargs.get('handler_map', {}) or {}
  1039. extra_args = ()
  1040. if (handles is not None or labels is not None) and args:
  1041. warnings.warn("You have mixed positional and keyword arguments, some "
  1042. "input may be discarded.")
  1043. # if got both handles and labels as kwargs, make same length
  1044. if handles and labels:
  1045. handles, labels = zip(*zip(handles, labels))
  1046. elif handles is not None and labels is None:
  1047. labels = [handle.get_label() for handle in handles]
  1048. elif labels is not None and handles is None:
  1049. # Get as many handles as there are labels.
  1050. handles = [handle for handle, label
  1051. in zip(_get_legend_handles(axs, handlers), labels)]
  1052. # No arguments - automatically detect labels and handles.
  1053. elif len(args) == 0:
  1054. handles, labels = _get_legend_handles_labels(axs, handlers)
  1055. if not handles:
  1056. log.warning('No handles with labels found to put in legend.')
  1057. # One argument. User defined labels - automatic handle detection.
  1058. elif len(args) == 1:
  1059. labels, = args
  1060. # Get as many handles as there are labels.
  1061. handles = [handle for handle, label
  1062. in zip(_get_legend_handles(axs, handlers), labels)]
  1063. # Two arguments:
  1064. # * user defined handles and labels
  1065. elif len(args) >= 2:
  1066. handles, labels = args[:2]
  1067. extra_args = args[2:]
  1068. else:
  1069. raise TypeError('Invalid arguments to legend.')
  1070. return handles, labels, extra_args, kwargs