legend_handler.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. """
  2. Default legend handlers.
  3. .. important::
  4. This is a low-level legend API, which most end users do not need.
  5. We recommend that you are familiar with the :ref:`legend guide
  6. <legend_guide>` before reading this documentation.
  7. Legend handlers are expected to be a callable object with a following
  8. signature::
  9. legend_handler(legend, orig_handle, fontsize, handlebox)
  10. Where *legend* is the legend itself, *orig_handle* is the original
  11. plot, *fontsize* is the fontsize in pixels, and *handlebox* is an
  12. `.OffsetBox` instance. Within the call, you should create relevant
  13. artists (using relevant properties from the *legend* and/or
  14. *orig_handle*) and add them into the *handlebox*. The artists need to
  15. be scaled according to the *fontsize* (note that the size is in pixels,
  16. i.e., this is dpi-scaled value).
  17. This module includes definition of several legend handler classes
  18. derived from the base class (HandlerBase) with the following method::
  19. def legend_artist(self, legend, orig_handle, fontsize, handlebox)
  20. """
  21. from itertools import cycle
  22. import numpy as np
  23. from matplotlib import cbook
  24. from matplotlib.lines import Line2D
  25. from matplotlib.patches import Rectangle
  26. import matplotlib.collections as mcoll
  27. def update_from_first_child(tgt, src):
  28. first_child = next(iter(src.get_children()), None)
  29. if first_child is not None:
  30. tgt.update_from(first_child)
  31. class HandlerBase:
  32. """
  33. A base class for default legend handlers.
  34. The derived classes are meant to override *create_artists* method, which
  35. has the following signature::
  36. def create_artists(self, legend, orig_handle,
  37. xdescent, ydescent, width, height, fontsize,
  38. trans):
  39. The overridden method needs to create artists of the given
  40. transform that fits in the given dimension (xdescent, ydescent,
  41. width, height) that are scaled by fontsize if necessary.
  42. """
  43. def __init__(self, xpad=0., ypad=0., update_func=None):
  44. """
  45. Parameters
  46. ----------
  47. xpad : float, optional
  48. Padding in x-direction.
  49. ypad : float, optional
  50. Padding in y-direction.
  51. update_func : callable, optional
  52. Function for updating the legend handler properties from another
  53. legend handler, used by `~HandlerBase.update_prop`.
  54. """
  55. self._xpad, self._ypad = xpad, ypad
  56. self._update_prop_func = update_func
  57. def _update_prop(self, legend_handle, orig_handle):
  58. if self._update_prop_func is None:
  59. self._default_update_prop(legend_handle, orig_handle)
  60. else:
  61. self._update_prop_func(legend_handle, orig_handle)
  62. def _default_update_prop(self, legend_handle, orig_handle):
  63. legend_handle.update_from(orig_handle)
  64. def update_prop(self, legend_handle, orig_handle, legend):
  65. self._update_prop(legend_handle, orig_handle)
  66. legend._set_artist_props(legend_handle)
  67. legend_handle.set_clip_box(None)
  68. legend_handle.set_clip_path(None)
  69. def adjust_drawing_area(self, legend, orig_handle,
  70. xdescent, ydescent, width, height, fontsize,
  71. ):
  72. xdescent = xdescent - self._xpad * fontsize
  73. ydescent = ydescent - self._ypad * fontsize
  74. width = width - self._xpad * fontsize
  75. height = height - self._ypad * fontsize
  76. return xdescent, ydescent, width, height
  77. def legend_artist(self, legend, orig_handle,
  78. fontsize, handlebox):
  79. """
  80. Return the artist that this HandlerBase generates for the given
  81. original artist/handle.
  82. Parameters
  83. ----------
  84. legend : `~matplotlib.legend.Legend`
  85. The legend for which these legend artists are being created.
  86. orig_handle : :class:`matplotlib.artist.Artist` or similar
  87. The object for which these legend artists are being created.
  88. fontsize : int
  89. The fontsize in pixels. The artists being created should
  90. be scaled according to the given fontsize.
  91. handlebox : `~matplotlib.offsetbox.OffsetBox`
  92. The box which has been created to hold this legend entry's
  93. artists. Artists created in the `legend_artist` method must
  94. be added to this handlebox inside this method.
  95. """
  96. xdescent, ydescent, width, height = self.adjust_drawing_area(
  97. legend, orig_handle,
  98. handlebox.xdescent, handlebox.ydescent,
  99. handlebox.width, handlebox.height,
  100. fontsize)
  101. artists = self.create_artists(legend, orig_handle,
  102. xdescent, ydescent, width, height,
  103. fontsize, handlebox.get_transform())
  104. # create_artists will return a list of artists.
  105. for a in artists:
  106. handlebox.add_artist(a)
  107. # we only return the first artist
  108. return artists[0]
  109. def create_artists(self, legend, orig_handle,
  110. xdescent, ydescent, width, height, fontsize,
  111. trans):
  112. """
  113. Return the legend artists generated.
  114. Parameters
  115. ----------
  116. legend : `~matplotlib.legend.Legend`
  117. The legend for which these legend artists are being created.
  118. orig_handle : `~matplotlib.artist.Artist` or similar
  119. The object for which these legend artists are being created.
  120. xdescent, ydescent, width, height : int
  121. The rectangle (*xdescent*, *ydescent*, *width*, *height*) that the
  122. legend artists being created should fit within.
  123. fontsize : int
  124. The fontsize in pixels. The legend artists being created should
  125. be scaled according to the given fontsize.
  126. trans : `~matplotlib.transforms.Transform`
  127. The transform that is applied to the legend artists being created.
  128. Typically from unit coordinates in the handler box to screen
  129. coordinates.
  130. """
  131. raise NotImplementedError('Derived must override')
  132. class HandlerNpoints(HandlerBase):
  133. """
  134. A legend handler that shows *numpoints* points in the legend entry.
  135. """
  136. def __init__(self, marker_pad=0.3, numpoints=None, **kwargs):
  137. """
  138. Parameters
  139. ----------
  140. marker_pad : float
  141. Padding between points in legend entry.
  142. numpoints : int
  143. Number of points to show in legend entry.
  144. **kwargs
  145. Keyword arguments forwarded to `.HandlerBase`.
  146. """
  147. super().__init__(**kwargs)
  148. self._numpoints = numpoints
  149. self._marker_pad = marker_pad
  150. def get_numpoints(self, legend):
  151. if self._numpoints is None:
  152. return legend.numpoints
  153. else:
  154. return self._numpoints
  155. def get_xdata(self, legend, xdescent, ydescent, width, height, fontsize):
  156. numpoints = self.get_numpoints(legend)
  157. if numpoints > 1:
  158. # we put some pad here to compensate the size of the marker
  159. pad = self._marker_pad * fontsize
  160. xdata = np.linspace(-xdescent + pad,
  161. -xdescent + width - pad,
  162. numpoints)
  163. xdata_marker = xdata
  164. else:
  165. xdata = [-xdescent, -xdescent + width]
  166. xdata_marker = [-xdescent + 0.5 * width]
  167. return xdata, xdata_marker
  168. class HandlerNpointsYoffsets(HandlerNpoints):
  169. """
  170. A legend handler that shows *numpoints* in the legend, and allows them to
  171. be individually offset in the y-direction.
  172. """
  173. def __init__(self, numpoints=None, yoffsets=None, **kwargs):
  174. """
  175. Parameters
  176. ----------
  177. numpoints : int
  178. Number of points to show in legend entry.
  179. yoffsets : array of floats
  180. Length *numpoints* list of y offsets for each point in
  181. legend entry.
  182. **kwargs
  183. Keyword arguments forwarded to `.HandlerNpoints`.
  184. """
  185. super().__init__(numpoints=numpoints, **kwargs)
  186. self._yoffsets = yoffsets
  187. def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize):
  188. if self._yoffsets is None:
  189. ydata = height * legend._scatteryoffsets
  190. else:
  191. ydata = height * np.asarray(self._yoffsets)
  192. return ydata
  193. class HandlerLine2DCompound(HandlerNpoints):
  194. """
  195. Original handler for `.Line2D` instances, that relies on combining
  196. a line-only with a marker-only artist. May be deprecated in the future.
  197. """
  198. def create_artists(self, legend, orig_handle,
  199. xdescent, ydescent, width, height, fontsize,
  200. trans):
  201. # docstring inherited
  202. xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
  203. width, height, fontsize)
  204. ydata = np.full_like(xdata, ((height - ydescent) / 2))
  205. legline = Line2D(xdata, ydata)
  206. self.update_prop(legline, orig_handle, legend)
  207. legline.set_drawstyle('default')
  208. legline.set_marker("")
  209. legline_marker = Line2D(xdata_marker, ydata[:len(xdata_marker)])
  210. self.update_prop(legline_marker, orig_handle, legend)
  211. legline_marker.set_linestyle('None')
  212. if legend.markerscale != 1:
  213. newsz = legline_marker.get_markersize() * legend.markerscale
  214. legline_marker.set_markersize(newsz)
  215. # we don't want to add this to the return list because
  216. # the texts and handles are assumed to be in one-to-one
  217. # correspondence.
  218. legline._legmarker = legline_marker
  219. legline.set_transform(trans)
  220. legline_marker.set_transform(trans)
  221. return [legline, legline_marker]
  222. class HandlerLine2D(HandlerNpoints):
  223. """
  224. Handler for `.Line2D` instances.
  225. See Also
  226. --------
  227. HandlerLine2DCompound : An earlier handler implementation, which used one
  228. artist for the line and another for the marker(s).
  229. """
  230. def create_artists(self, legend, orig_handle,
  231. xdescent, ydescent, width, height, fontsize,
  232. trans):
  233. # docstring inherited
  234. xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
  235. width, height, fontsize)
  236. markevery = None
  237. if self.get_numpoints(legend) == 1:
  238. # Special case: one wants a single marker in the center
  239. # and a line that extends on both sides. One will use a
  240. # 3 points line, but only mark the #1 (i.e. middle) point.
  241. xdata = np.linspace(xdata[0], xdata[-1], 3)
  242. markevery = [1]
  243. ydata = np.full_like(xdata, (height - ydescent) / 2)
  244. legline = Line2D(xdata, ydata, markevery=markevery)
  245. self.update_prop(legline, orig_handle, legend)
  246. if legend.markerscale != 1:
  247. newsz = legline.get_markersize() * legend.markerscale
  248. legline.set_markersize(newsz)
  249. legline.set_transform(trans)
  250. return [legline]
  251. class HandlerPatch(HandlerBase):
  252. """
  253. Handler for `.Patch` instances.
  254. """
  255. def __init__(self, patch_func=None, **kwargs):
  256. """
  257. Parameters
  258. ----------
  259. patch_func : callable, optional
  260. The function that creates the legend key artist.
  261. *patch_func* should have the signature::
  262. def patch_func(legend=legend, orig_handle=orig_handle,
  263. xdescent=xdescent, ydescent=ydescent,
  264. width=width, height=height, fontsize=fontsize)
  265. Subsequently, the created artist will have its ``update_prop``
  266. method called and the appropriate transform will be applied.
  267. **kwargs
  268. Keyword arguments forwarded to `.HandlerBase`.
  269. """
  270. super().__init__(**kwargs)
  271. self._patch_func = patch_func
  272. def _create_patch(self, legend, orig_handle,
  273. xdescent, ydescent, width, height, fontsize):
  274. if self._patch_func is None:
  275. p = Rectangle(xy=(-xdescent, -ydescent),
  276. width=width, height=height)
  277. else:
  278. p = self._patch_func(legend=legend, orig_handle=orig_handle,
  279. xdescent=xdescent, ydescent=ydescent,
  280. width=width, height=height, fontsize=fontsize)
  281. return p
  282. def create_artists(self, legend, orig_handle,
  283. xdescent, ydescent, width, height, fontsize, trans):
  284. # docstring inherited
  285. p = self._create_patch(legend, orig_handle,
  286. xdescent, ydescent, width, height, fontsize)
  287. self.update_prop(p, orig_handle, legend)
  288. p.set_transform(trans)
  289. return [p]
  290. class HandlerStepPatch(HandlerBase):
  291. """
  292. Handler for `~.matplotlib.patches.StepPatch` instances.
  293. """
  294. @staticmethod
  295. def _create_patch(orig_handle, xdescent, ydescent, width, height):
  296. return Rectangle(xy=(-xdescent, -ydescent), width=width,
  297. height=height, color=orig_handle.get_facecolor())
  298. @staticmethod
  299. def _create_line(orig_handle, width, height):
  300. # Unfilled StepPatch should show as a line
  301. legline = Line2D([0, width], [height/2, height/2],
  302. color=orig_handle.get_edgecolor(),
  303. linestyle=orig_handle.get_linestyle(),
  304. linewidth=orig_handle.get_linewidth(),
  305. )
  306. # Overwrite manually because patch and line properties don't mix
  307. legline.set_drawstyle('default')
  308. legline.set_marker("")
  309. return legline
  310. def create_artists(self, legend, orig_handle,
  311. xdescent, ydescent, width, height, fontsize, trans):
  312. # docstring inherited
  313. if orig_handle.get_fill() or (orig_handle.get_hatch() is not None):
  314. p = self._create_patch(orig_handle, xdescent, ydescent, width,
  315. height)
  316. self.update_prop(p, orig_handle, legend)
  317. else:
  318. p = self._create_line(orig_handle, width, height)
  319. p.set_transform(trans)
  320. return [p]
  321. class HandlerLineCollection(HandlerLine2D):
  322. """
  323. Handler for `.LineCollection` instances.
  324. """
  325. def get_numpoints(self, legend):
  326. if self._numpoints is None:
  327. return legend.scatterpoints
  328. else:
  329. return self._numpoints
  330. def _default_update_prop(self, legend_handle, orig_handle):
  331. lw = orig_handle.get_linewidths()[0]
  332. dashes = orig_handle._us_linestyles[0]
  333. color = orig_handle.get_colors()[0]
  334. legend_handle.set_color(color)
  335. legend_handle.set_linestyle(dashes)
  336. legend_handle.set_linewidth(lw)
  337. def create_artists(self, legend, orig_handle,
  338. xdescent, ydescent, width, height, fontsize, trans):
  339. # docstring inherited
  340. xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
  341. width, height, fontsize)
  342. ydata = np.full_like(xdata, (height - ydescent) / 2)
  343. legline = Line2D(xdata, ydata)
  344. self.update_prop(legline, orig_handle, legend)
  345. legline.set_transform(trans)
  346. return [legline]
  347. class HandlerRegularPolyCollection(HandlerNpointsYoffsets):
  348. r"""Handler for `.RegularPolyCollection`\s."""
  349. def __init__(self, yoffsets=None, sizes=None, **kwargs):
  350. super().__init__(yoffsets=yoffsets, **kwargs)
  351. self._sizes = sizes
  352. def get_numpoints(self, legend):
  353. if self._numpoints is None:
  354. return legend.scatterpoints
  355. else:
  356. return self._numpoints
  357. def get_sizes(self, legend, orig_handle,
  358. xdescent, ydescent, width, height, fontsize):
  359. if self._sizes is None:
  360. handle_sizes = orig_handle.get_sizes()
  361. if not len(handle_sizes):
  362. handle_sizes = [1]
  363. size_max = max(handle_sizes) * legend.markerscale ** 2
  364. size_min = min(handle_sizes) * legend.markerscale ** 2
  365. numpoints = self.get_numpoints(legend)
  366. if numpoints < 4:
  367. sizes = [.5 * (size_max + size_min), size_max,
  368. size_min][:numpoints]
  369. else:
  370. rng = (size_max - size_min)
  371. sizes = rng * np.linspace(0, 1, numpoints) + size_min
  372. else:
  373. sizes = self._sizes
  374. return sizes
  375. def update_prop(self, legend_handle, orig_handle, legend):
  376. self._update_prop(legend_handle, orig_handle)
  377. legend_handle.set_figure(legend.figure)
  378. # legend._set_artist_props(legend_handle)
  379. legend_handle.set_clip_box(None)
  380. legend_handle.set_clip_path(None)
  381. def create_collection(self, orig_handle, sizes, offsets, offset_transform):
  382. return type(orig_handle)(
  383. orig_handle.get_numsides(),
  384. rotation=orig_handle.get_rotation(), sizes=sizes,
  385. offsets=offsets, offset_transform=offset_transform,
  386. )
  387. def create_artists(self, legend, orig_handle,
  388. xdescent, ydescent, width, height, fontsize,
  389. trans):
  390. # docstring inherited
  391. xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
  392. width, height, fontsize)
  393. ydata = self.get_ydata(legend, xdescent, ydescent,
  394. width, height, fontsize)
  395. sizes = self.get_sizes(legend, orig_handle, xdescent, ydescent,
  396. width, height, fontsize)
  397. p = self.create_collection(
  398. orig_handle, sizes,
  399. offsets=list(zip(xdata_marker, ydata)), offset_transform=trans)
  400. self.update_prop(p, orig_handle, legend)
  401. p.set_offset_transform(trans)
  402. return [p]
  403. class HandlerPathCollection(HandlerRegularPolyCollection):
  404. r"""Handler for `.PathCollection`\s, which are used by `~.Axes.scatter`."""
  405. def create_collection(self, orig_handle, sizes, offsets, offset_transform):
  406. return type(orig_handle)(
  407. [orig_handle.get_paths()[0]], sizes=sizes,
  408. offsets=offsets, offset_transform=offset_transform,
  409. )
  410. class HandlerCircleCollection(HandlerRegularPolyCollection):
  411. r"""Handler for `.CircleCollection`\s."""
  412. def create_collection(self, orig_handle, sizes, offsets, offset_transform):
  413. return type(orig_handle)(
  414. sizes, offsets=offsets, offset_transform=offset_transform)
  415. class HandlerErrorbar(HandlerLine2D):
  416. """Handler for Errorbars."""
  417. def __init__(self, xerr_size=0.5, yerr_size=None,
  418. marker_pad=0.3, numpoints=None, **kwargs):
  419. self._xerr_size = xerr_size
  420. self._yerr_size = yerr_size
  421. super().__init__(marker_pad=marker_pad, numpoints=numpoints, **kwargs)
  422. def get_err_size(self, legend, xdescent, ydescent,
  423. width, height, fontsize):
  424. xerr_size = self._xerr_size * fontsize
  425. if self._yerr_size is None:
  426. yerr_size = xerr_size
  427. else:
  428. yerr_size = self._yerr_size * fontsize
  429. return xerr_size, yerr_size
  430. def create_artists(self, legend, orig_handle,
  431. xdescent, ydescent, width, height, fontsize,
  432. trans):
  433. # docstring inherited
  434. plotlines, caplines, barlinecols = orig_handle
  435. xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
  436. width, height, fontsize)
  437. ydata = np.full_like(xdata, (height - ydescent) / 2)
  438. legline = Line2D(xdata, ydata)
  439. xdata_marker = np.asarray(xdata_marker)
  440. ydata_marker = np.asarray(ydata[:len(xdata_marker)])
  441. xerr_size, yerr_size = self.get_err_size(legend, xdescent, ydescent,
  442. width, height, fontsize)
  443. legline_marker = Line2D(xdata_marker, ydata_marker)
  444. # when plotlines are None (only errorbars are drawn), we just
  445. # make legline invisible.
  446. if plotlines is None:
  447. legline.set_visible(False)
  448. legline_marker.set_visible(False)
  449. else:
  450. self.update_prop(legline, plotlines, legend)
  451. legline.set_drawstyle('default')
  452. legline.set_marker('none')
  453. self.update_prop(legline_marker, plotlines, legend)
  454. legline_marker.set_linestyle('None')
  455. if legend.markerscale != 1:
  456. newsz = legline_marker.get_markersize() * legend.markerscale
  457. legline_marker.set_markersize(newsz)
  458. handle_barlinecols = []
  459. handle_caplines = []
  460. if orig_handle.has_xerr:
  461. verts = [((x - xerr_size, y), (x + xerr_size, y))
  462. for x, y in zip(xdata_marker, ydata_marker)]
  463. coll = mcoll.LineCollection(verts)
  464. self.update_prop(coll, barlinecols[0], legend)
  465. handle_barlinecols.append(coll)
  466. if caplines:
  467. capline_left = Line2D(xdata_marker - xerr_size, ydata_marker)
  468. capline_right = Line2D(xdata_marker + xerr_size, ydata_marker)
  469. self.update_prop(capline_left, caplines[0], legend)
  470. self.update_prop(capline_right, caplines[0], legend)
  471. capline_left.set_marker("|")
  472. capline_right.set_marker("|")
  473. handle_caplines.append(capline_left)
  474. handle_caplines.append(capline_right)
  475. if orig_handle.has_yerr:
  476. verts = [((x, y - yerr_size), (x, y + yerr_size))
  477. for x, y in zip(xdata_marker, ydata_marker)]
  478. coll = mcoll.LineCollection(verts)
  479. self.update_prop(coll, barlinecols[0], legend)
  480. handle_barlinecols.append(coll)
  481. if caplines:
  482. capline_left = Line2D(xdata_marker, ydata_marker - yerr_size)
  483. capline_right = Line2D(xdata_marker, ydata_marker + yerr_size)
  484. self.update_prop(capline_left, caplines[0], legend)
  485. self.update_prop(capline_right, caplines[0], legend)
  486. capline_left.set_marker("_")
  487. capline_right.set_marker("_")
  488. handle_caplines.append(capline_left)
  489. handle_caplines.append(capline_right)
  490. artists = [
  491. *handle_barlinecols, *handle_caplines, legline, legline_marker,
  492. ]
  493. for artist in artists:
  494. artist.set_transform(trans)
  495. return artists
  496. class HandlerStem(HandlerNpointsYoffsets):
  497. """
  498. Handler for plots produced by `~.Axes.stem`.
  499. """
  500. def __init__(self, marker_pad=0.3, numpoints=None,
  501. bottom=None, yoffsets=None, **kwargs):
  502. """
  503. Parameters
  504. ----------
  505. marker_pad : float, default: 0.3
  506. Padding between points in legend entry.
  507. numpoints : int, optional
  508. Number of points to show in legend entry.
  509. bottom : float, optional
  510. yoffsets : array of floats, optional
  511. Length *numpoints* list of y offsets for each point in
  512. legend entry.
  513. **kwargs
  514. Keyword arguments forwarded to `.HandlerNpointsYoffsets`.
  515. """
  516. super().__init__(marker_pad=marker_pad, numpoints=numpoints,
  517. yoffsets=yoffsets, **kwargs)
  518. self._bottom = bottom
  519. def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize):
  520. if self._yoffsets is None:
  521. ydata = height * (0.5 * legend._scatteryoffsets + 0.5)
  522. else:
  523. ydata = height * np.asarray(self._yoffsets)
  524. return ydata
  525. def create_artists(self, legend, orig_handle,
  526. xdescent, ydescent, width, height, fontsize,
  527. trans):
  528. # docstring inherited
  529. markerline, stemlines, baseline = orig_handle
  530. # Check to see if the stemcontainer is storing lines as a list or a
  531. # LineCollection. Eventually using a list will be removed, and this
  532. # logic can also be removed.
  533. using_linecoll = isinstance(stemlines, mcoll.LineCollection)
  534. xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
  535. width, height, fontsize)
  536. ydata = self.get_ydata(legend, xdescent, ydescent,
  537. width, height, fontsize)
  538. if self._bottom is None:
  539. bottom = 0.
  540. else:
  541. bottom = self._bottom
  542. leg_markerline = Line2D(xdata_marker, ydata[:len(xdata_marker)])
  543. self.update_prop(leg_markerline, markerline, legend)
  544. leg_stemlines = [Line2D([x, x], [bottom, y])
  545. for x, y in zip(xdata_marker, ydata)]
  546. if using_linecoll:
  547. # change the function used by update_prop() from the default
  548. # to one that handles LineCollection
  549. with cbook._setattr_cm(
  550. self, _update_prop_func=self._copy_collection_props):
  551. for line in leg_stemlines:
  552. self.update_prop(line, stemlines, legend)
  553. else:
  554. for lm, m in zip(leg_stemlines, stemlines):
  555. self.update_prop(lm, m, legend)
  556. leg_baseline = Line2D([np.min(xdata), np.max(xdata)],
  557. [bottom, bottom])
  558. self.update_prop(leg_baseline, baseline, legend)
  559. artists = [*leg_stemlines, leg_baseline, leg_markerline]
  560. for artist in artists:
  561. artist.set_transform(trans)
  562. return artists
  563. def _copy_collection_props(self, legend_handle, orig_handle):
  564. """
  565. Copy properties from the `.LineCollection` *orig_handle* to the
  566. `.Line2D` *legend_handle*.
  567. """
  568. legend_handle.set_color(orig_handle.get_color()[0])
  569. legend_handle.set_linestyle(orig_handle.get_linestyle()[0])
  570. class HandlerTuple(HandlerBase):
  571. """
  572. Handler for Tuple.
  573. """
  574. def __init__(self, ndivide=1, pad=None, **kwargs):
  575. """
  576. Parameters
  577. ----------
  578. ndivide : int or None, default: 1
  579. The number of sections to divide the legend area into. If None,
  580. use the length of the input tuple.
  581. pad : float, default: :rc:`legend.borderpad`
  582. Padding in units of fraction of font size.
  583. **kwargs
  584. Keyword arguments forwarded to `.HandlerBase`.
  585. """
  586. self._ndivide = ndivide
  587. self._pad = pad
  588. super().__init__(**kwargs)
  589. def create_artists(self, legend, orig_handle,
  590. xdescent, ydescent, width, height, fontsize,
  591. trans):
  592. # docstring inherited
  593. handler_map = legend.get_legend_handler_map()
  594. if self._ndivide is None:
  595. ndivide = len(orig_handle)
  596. else:
  597. ndivide = self._ndivide
  598. if self._pad is None:
  599. pad = legend.borderpad * fontsize
  600. else:
  601. pad = self._pad * fontsize
  602. if ndivide > 1:
  603. width = (width - pad * (ndivide - 1)) / ndivide
  604. xds_cycle = cycle(xdescent - (width + pad) * np.arange(ndivide))
  605. a_list = []
  606. for handle1 in orig_handle:
  607. handler = legend.get_legend_handler(handler_map, handle1)
  608. _a_list = handler.create_artists(
  609. legend, handle1,
  610. next(xds_cycle), ydescent, width, height, fontsize, trans)
  611. a_list.extend(_a_list)
  612. return a_list
  613. class HandlerPolyCollection(HandlerBase):
  614. """
  615. Handler for `.PolyCollection` used in `~.Axes.fill_between` and
  616. `~.Axes.stackplot`.
  617. """
  618. def _update_prop(self, legend_handle, orig_handle):
  619. def first_color(colors):
  620. if colors.size == 0:
  621. return (0, 0, 0, 0)
  622. return tuple(colors[0])
  623. def get_first(prop_array):
  624. if len(prop_array):
  625. return prop_array[0]
  626. else:
  627. return None
  628. # orig_handle is a PolyCollection and legend_handle is a Patch.
  629. # Directly set Patch color attributes (must be RGBA tuples).
  630. legend_handle._facecolor = first_color(orig_handle.get_facecolor())
  631. legend_handle._edgecolor = first_color(orig_handle.get_edgecolor())
  632. legend_handle._original_facecolor = orig_handle._original_facecolor
  633. legend_handle._original_edgecolor = orig_handle._original_edgecolor
  634. legend_handle._fill = orig_handle.get_fill()
  635. legend_handle._hatch = orig_handle.get_hatch()
  636. # Hatch color is anomalous in having no getters and setters.
  637. legend_handle._hatch_color = orig_handle._hatch_color
  638. # Setters are fine for the remaining attributes.
  639. legend_handle.set_linewidth(get_first(orig_handle.get_linewidths()))
  640. legend_handle.set_linestyle(get_first(orig_handle.get_linestyles()))
  641. legend_handle.set_transform(get_first(orig_handle.get_transforms()))
  642. legend_handle.set_figure(orig_handle.get_figure())
  643. # Alpha is already taken into account by the color attributes.
  644. def create_artists(self, legend, orig_handle,
  645. xdescent, ydescent, width, height, fontsize, trans):
  646. # docstring inherited
  647. p = Rectangle(xy=(-xdescent, -ydescent),
  648. width=width, height=height)
  649. self.update_prop(p, orig_handle, legend)
  650. p.set_transform(trans)
  651. return [p]