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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2This module defines default legend handlers.
4It is strongly encouraged to have read the :doc:`legend guide
5</tutorials/intermediate/legend_guide>` before this documentation.
7Legend handlers are expected to be a callable object with a following
8signature. ::
10 legend_handler(legend, orig_handle, fontsize, handlebox)
12Where *legend* is the legend itself, *orig_handle* is the original
13plot, *fontsize* is the fontsize in pixels, and *handlebox* is a
14OffsetBox instance. Within the call, you should create relevant
15artists (using relevant properties from the *legend* and/or
16*orig_handle*) and add them into the handlebox. The artists needs to
17be scaled according to the fontsize (note that the size is in pixel,
18i.e., this is dpi-scaled value).
20This module includes definition of several legend handler classes
21derived from the base class (HandlerBase) with the following method::
23 def legend_artist(self, legend, orig_handle, fontsize, handlebox)
25"""
27from itertools import cycle
29import numpy as np
31from matplotlib.lines import Line2D
32from matplotlib.patches import Rectangle
33import matplotlib.collections as mcoll
34import matplotlib.colors as mcolors
37def update_from_first_child(tgt, src):
38 first_child = next(iter(src.get_children()), None)
39 if first_child is not None:
40 tgt.update_from(first_child)
43class HandlerBase:
44 """
45 A Base class for default legend handlers.
47 The derived classes are meant to override *create_artists* method, which
48 has a following signature.::
50 def create_artists(self, legend, orig_handle,
51 xdescent, ydescent, width, height, fontsize,
52 trans):
54 The overridden method needs to create artists of the given
55 transform that fits in the given dimension (xdescent, ydescent,
56 width, height) that are scaled by fontsize if necessary.
58 """
59 def __init__(self, xpad=0., ypad=0., update_func=None):
60 self._xpad, self._ypad = xpad, ypad
61 self._update_prop_func = update_func
63 def _update_prop(self, legend_handle, orig_handle):
64 if self._update_prop_func is None:
65 self._default_update_prop(legend_handle, orig_handle)
66 else:
67 self._update_prop_func(legend_handle, orig_handle)
69 def _default_update_prop(self, legend_handle, orig_handle):
70 legend_handle.update_from(orig_handle)
72 def update_prop(self, legend_handle, orig_handle, legend):
74 self._update_prop(legend_handle, orig_handle)
76 legend._set_artist_props(legend_handle)
77 legend_handle.set_clip_box(None)
78 legend_handle.set_clip_path(None)
80 def adjust_drawing_area(self, legend, orig_handle,
81 xdescent, ydescent, width, height, fontsize,
82 ):
83 xdescent = xdescent - self._xpad * fontsize
84 ydescent = ydescent - self._ypad * fontsize
85 width = width - self._xpad * fontsize
86 height = height - self._ypad * fontsize
87 return xdescent, ydescent, width, height
89 def legend_artist(self, legend, orig_handle,
90 fontsize, handlebox):
91 """
92 Return the artist that this HandlerBase generates for the given
93 original artist/handle.
95 Parameters
96 ----------
97 legend : :class:`matplotlib.legend.Legend` instance
98 The legend for which these legend artists are being created.
99 orig_handle : :class:`matplotlib.artist.Artist` or similar
100 The object for which these legend artists are being created.
101 fontsize : float or int
102 The fontsize in pixels. The artists being created should
103 be scaled according to the given fontsize.
104 handlebox : :class:`matplotlib.offsetbox.OffsetBox` instance
105 The box which has been created to hold this legend entry's
106 artists. Artists created in the `legend_artist` method must
107 be added to this handlebox inside this method.
109 """
110 xdescent, ydescent, width, height = self.adjust_drawing_area(
111 legend, orig_handle,
112 handlebox.xdescent, handlebox.ydescent,
113 handlebox.width, handlebox.height,
114 fontsize)
115 artists = self.create_artists(legend, orig_handle,
116 xdescent, ydescent, width, height,
117 fontsize, handlebox.get_transform())
119 # create_artists will return a list of artists.
120 for a in artists:
121 handlebox.add_artist(a)
123 # we only return the first artist
124 return artists[0]
126 def create_artists(self, legend, orig_handle,
127 xdescent, ydescent, width, height, fontsize,
128 trans):
129 raise NotImplementedError('Derived must override')
132class 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, **kw):
137 """
138 Parameters
139 ----------
140 marker_pad : float
141 Padding between points in legend entry.
143 numpoints : int
144 Number of points to show in legend entry.
146 Notes
147 -----
148 Any other keyword arguments are given to `HandlerBase`.
149 """
150 HandlerBase.__init__(self, **kw)
152 self._numpoints = numpoints
153 self._marker_pad = marker_pad
155 def get_numpoints(self, legend):
156 if self._numpoints is None:
157 return legend.numpoints
158 else:
159 return self._numpoints
161 def get_xdata(self, legend, xdescent, ydescent, width, height, fontsize):
162 numpoints = self.get_numpoints(legend)
163 if numpoints > 1:
164 # we put some pad here to compensate the size of the marker
165 pad = self._marker_pad * fontsize
166 xdata = np.linspace(-xdescent + pad,
167 -xdescent + width - pad,
168 numpoints)
169 xdata_marker = xdata
170 else:
171 xdata = [-xdescent, -xdescent + width]
172 xdata_marker = [-xdescent + 0.5 * width]
173 return xdata, xdata_marker
176class HandlerNpointsYoffsets(HandlerNpoints):
177 """
178 A legend handler that shows *numpoints* in the legend, and allows them to
179 be individually offest in the y-direction.
180 """
181 def __init__(self, numpoints=None, yoffsets=None, **kw):
182 """
183 Parameters
184 ----------
185 numpoints : int
186 Number of points to show in legend entry.
188 yoffsets : array of floats
189 Length *numpoints* list of y offsets for each point in
190 legend entry.
192 Notes
193 -----
194 Any other keyword arguments are given to `HandlerNpoints`.
195 """
196 HandlerNpoints.__init__(self, numpoints=numpoints, **kw)
197 self._yoffsets = yoffsets
199 def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize):
200 if self._yoffsets is None:
201 ydata = height * legend._scatteryoffsets
202 else:
203 ydata = height * np.asarray(self._yoffsets)
205 return ydata
208class HandlerLine2D(HandlerNpoints):
209 """
210 Handler for `.Line2D` instances.
211 """
212 def __init__(self, marker_pad=0.3, numpoints=None, **kw):
213 """
214 Parameters
215 ----------
216 marker_pad : float
217 Padding between points in legend entry.
219 numpoints : int
220 Number of points to show in legend entry.
222 Notes
223 -----
224 Any other keyword arguments are given to `HandlerNpoints`.
225 """
226 HandlerNpoints.__init__(self, marker_pad=marker_pad,
227 numpoints=numpoints, **kw)
229 def create_artists(self, legend, orig_handle,
230 xdescent, ydescent, width, height, fontsize,
231 trans):
233 xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
234 width, height, fontsize)
236 ydata = np.full_like(xdata, ((height - ydescent) / 2))
237 legline = Line2D(xdata, ydata)
239 self.update_prop(legline, orig_handle, legend)
240 legline.set_drawstyle('default')
241 legline.set_marker("")
243 legline_marker = Line2D(xdata_marker, ydata[:len(xdata_marker)])
244 self.update_prop(legline_marker, orig_handle, legend)
245 legline_marker.set_linestyle('None')
246 if legend.markerscale != 1:
247 newsz = legline_marker.get_markersize() * legend.markerscale
248 legline_marker.set_markersize(newsz)
249 # we don't want to add this to the return list because
250 # the texts and handles are assumed to be in one-to-one
251 # correspondence.
252 legline._legmarker = legline_marker
254 legline.set_transform(trans)
255 legline_marker.set_transform(trans)
257 return [legline, legline_marker]
260class HandlerPatch(HandlerBase):
261 """
262 Handler for `.Patch` instances.
263 """
264 def __init__(self, patch_func=None, **kw):
265 """
266 Parameters
267 ----------
268 patch_func : callable, optional
269 The function that creates the legend key artist.
270 *patch_func* should have the signature::
272 def patch_func(legend=legend, orig_handle=orig_handle,
273 xdescent=xdescent, ydescent=ydescent,
274 width=width, height=height, fontsize=fontsize)
276 Subsequently the created artist will have its ``update_prop``
277 method called and the appropriate transform will be applied.
279 Notes
280 -----
281 Any other keyword arguments are given to `HandlerBase`.
282 """
283 HandlerBase.__init__(self, **kw)
284 self._patch_func = patch_func
286 def _create_patch(self, legend, orig_handle,
287 xdescent, ydescent, width, height, fontsize):
288 if self._patch_func is None:
289 p = Rectangle(xy=(-xdescent, -ydescent),
290 width=width, height=height)
291 else:
292 p = self._patch_func(legend=legend, orig_handle=orig_handle,
293 xdescent=xdescent, ydescent=ydescent,
294 width=width, height=height, fontsize=fontsize)
295 return p
297 def create_artists(self, legend, orig_handle,
298 xdescent, ydescent, width, height, fontsize, trans):
299 p = self._create_patch(legend, orig_handle,
300 xdescent, ydescent, width, height, fontsize)
301 self.update_prop(p, orig_handle, legend)
302 p.set_transform(trans)
303 return [p]
306class HandlerLineCollection(HandlerLine2D):
307 """
308 Handler for `.LineCollection` instances.
309 """
310 def get_numpoints(self, legend):
311 if self._numpoints is None:
312 return legend.scatterpoints
313 else:
314 return self._numpoints
316 def _default_update_prop(self, legend_handle, orig_handle):
317 lw = orig_handle.get_linewidths()[0]
318 dashes = orig_handle._us_linestyles[0]
319 color = orig_handle.get_colors()[0]
320 legend_handle.set_color(color)
321 legend_handle.set_linestyle(dashes)
322 legend_handle.set_linewidth(lw)
324 def create_artists(self, legend, orig_handle,
325 xdescent, ydescent, width, height, fontsize, trans):
327 xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
328 width, height, fontsize)
329 ydata = np.full_like(xdata, (height - ydescent) / 2)
330 legline = Line2D(xdata, ydata)
332 self.update_prop(legline, orig_handle, legend)
333 legline.set_transform(trans)
335 return [legline]
338class HandlerRegularPolyCollection(HandlerNpointsYoffsets):
339 """
340 Handler for `.RegularPolyCollections`.
341 """
342 def __init__(self, yoffsets=None, sizes=None, **kw):
343 HandlerNpointsYoffsets.__init__(self, yoffsets=yoffsets, **kw)
345 self._sizes = sizes
347 def get_numpoints(self, legend):
348 if self._numpoints is None:
349 return legend.scatterpoints
350 else:
351 return self._numpoints
353 def get_sizes(self, legend, orig_handle,
354 xdescent, ydescent, width, height, fontsize):
355 if self._sizes is None:
356 handle_sizes = orig_handle.get_sizes()
357 if not len(handle_sizes):
358 handle_sizes = [1]
359 size_max = max(handle_sizes) * legend.markerscale ** 2
360 size_min = min(handle_sizes) * legend.markerscale ** 2
362 numpoints = self.get_numpoints(legend)
363 if numpoints < 4:
364 sizes = [.5 * (size_max + size_min), size_max,
365 size_min][:numpoints]
366 else:
367 rng = (size_max - size_min)
368 sizes = rng * np.linspace(0, 1, numpoints) + size_min
369 else:
370 sizes = self._sizes
372 return sizes
374 def update_prop(self, legend_handle, orig_handle, legend):
376 self._update_prop(legend_handle, orig_handle)
378 legend_handle.set_figure(legend.figure)
379 # legend._set_artist_props(legend_handle)
380 legend_handle.set_clip_box(None)
381 legend_handle.set_clip_path(None)
383 def create_collection(self, orig_handle, sizes, offsets, transOffset):
384 p = type(orig_handle)(orig_handle.get_numsides(),
385 rotation=orig_handle.get_rotation(),
386 sizes=sizes,
387 offsets=offsets,
388 transOffset=transOffset,
389 )
390 return p
392 def create_artists(self, legend, orig_handle,
393 xdescent, ydescent, width, height, fontsize,
394 trans):
395 xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
396 width, height, fontsize)
398 ydata = self.get_ydata(legend, xdescent, ydescent,
399 width, height, fontsize)
401 sizes = self.get_sizes(legend, orig_handle, xdescent, ydescent,
402 width, height, fontsize)
404 p = self.create_collection(orig_handle, sizes,
405 offsets=list(zip(xdata_marker, ydata)),
406 transOffset=trans)
408 self.update_prop(p, orig_handle, legend)
409 p._transOffset = trans
410 return [p]
413class HandlerPathCollection(HandlerRegularPolyCollection):
414 """
415 Handler for `.PathCollections`, which are used by `~.Axes.scatter`.
416 """
417 def create_collection(self, orig_handle, sizes, offsets, transOffset):
418 p = type(orig_handle)([orig_handle.get_paths()[0]],
419 sizes=sizes,
420 offsets=offsets,
421 transOffset=transOffset,
422 )
423 return p
426class HandlerCircleCollection(HandlerRegularPolyCollection):
427 """
428 Handler for `.CircleCollections`.
429 """
430 def create_collection(self, orig_handle, sizes, offsets, transOffset):
431 p = type(orig_handle)(sizes,
432 offsets=offsets,
433 transOffset=transOffset,
434 )
435 return p
438class HandlerErrorbar(HandlerLine2D):
439 """
440 Handler for Errorbars.
441 """
442 def __init__(self, xerr_size=0.5, yerr_size=None,
443 marker_pad=0.3, numpoints=None, **kw):
445 self._xerr_size = xerr_size
446 self._yerr_size = yerr_size
448 HandlerLine2D.__init__(self, marker_pad=marker_pad,
449 numpoints=numpoints, **kw)
451 def get_err_size(self, legend, xdescent, ydescent,
452 width, height, fontsize):
453 xerr_size = self._xerr_size * fontsize
455 if self._yerr_size is None:
456 yerr_size = xerr_size
457 else:
458 yerr_size = self._yerr_size * fontsize
460 return xerr_size, yerr_size
462 def create_artists(self, legend, orig_handle,
463 xdescent, ydescent, width, height, fontsize,
464 trans):
466 plotlines, caplines, barlinecols = orig_handle
468 xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
469 width, height, fontsize)
471 ydata = np.full_like(xdata, (height - ydescent) / 2)
472 legline = Line2D(xdata, ydata)
474 xdata_marker = np.asarray(xdata_marker)
475 ydata_marker = np.asarray(ydata[:len(xdata_marker)])
477 xerr_size, yerr_size = self.get_err_size(legend, xdescent, ydescent,
478 width, height, fontsize)
480 legline_marker = Line2D(xdata_marker, ydata_marker)
482 # when plotlines are None (only errorbars are drawn), we just
483 # make legline invisible.
484 if plotlines is None:
485 legline.set_visible(False)
486 legline_marker.set_visible(False)
487 else:
488 self.update_prop(legline, plotlines, legend)
490 legline.set_drawstyle('default')
491 legline.set_marker('None')
493 self.update_prop(legline_marker, plotlines, legend)
494 legline_marker.set_linestyle('None')
496 if legend.markerscale != 1:
497 newsz = legline_marker.get_markersize() * legend.markerscale
498 legline_marker.set_markersize(newsz)
500 handle_barlinecols = []
501 handle_caplines = []
503 if orig_handle.has_xerr:
504 verts = [((x - xerr_size, y), (x + xerr_size, y))
505 for x, y in zip(xdata_marker, ydata_marker)]
506 coll = mcoll.LineCollection(verts)
507 self.update_prop(coll, barlinecols[0], legend)
508 handle_barlinecols.append(coll)
510 if caplines:
511 capline_left = Line2D(xdata_marker - xerr_size, ydata_marker)
512 capline_right = Line2D(xdata_marker + xerr_size, ydata_marker)
513 self.update_prop(capline_left, caplines[0], legend)
514 self.update_prop(capline_right, caplines[0], legend)
515 capline_left.set_marker("|")
516 capline_right.set_marker("|")
518 handle_caplines.append(capline_left)
519 handle_caplines.append(capline_right)
521 if orig_handle.has_yerr:
522 verts = [((x, y - yerr_size), (x, y + yerr_size))
523 for x, y in zip(xdata_marker, ydata_marker)]
524 coll = mcoll.LineCollection(verts)
525 self.update_prop(coll, barlinecols[0], legend)
526 handle_barlinecols.append(coll)
528 if caplines:
529 capline_left = Line2D(xdata_marker, ydata_marker - yerr_size)
530 capline_right = Line2D(xdata_marker, ydata_marker + yerr_size)
531 self.update_prop(capline_left, caplines[0], legend)
532 self.update_prop(capline_right, caplines[0], legend)
533 capline_left.set_marker("_")
534 capline_right.set_marker("_")
536 handle_caplines.append(capline_left)
537 handle_caplines.append(capline_right)
539 artists = [
540 *handle_barlinecols, *handle_caplines, legline, legline_marker,
541 ]
542 for artist in artists:
543 artist.set_transform(trans)
544 return artists
547class HandlerStem(HandlerNpointsYoffsets):
548 """
549 Handler for plots produced by `~.Axes.stem`.
550 """
551 def __init__(self, marker_pad=0.3, numpoints=None,
552 bottom=None, yoffsets=None, **kw):
553 """
554 Parameters
555 ----------
556 marker_pad : float
557 Padding between points in legend entry. Default is 0.3.
559 numpoints : int, optional
560 Number of points to show in legend entry.
562 bottom : float, optional
564 yoffsets : array of floats, optional
565 Length *numpoints* list of y offsets for each point in
566 legend entry.
568 Notes
569 -----
570 Any other keyword arguments are given to `HandlerNpointsYoffsets`.
571 """
573 HandlerNpointsYoffsets.__init__(self, marker_pad=marker_pad,
574 numpoints=numpoints,
575 yoffsets=yoffsets,
576 **kw)
577 self._bottom = bottom
579 def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize):
580 if self._yoffsets is None:
581 ydata = height * (0.5 * legend._scatteryoffsets + 0.5)
582 else:
583 ydata = height * np.asarray(self._yoffsets)
585 return ydata
587 def create_artists(self, legend, orig_handle,
588 xdescent, ydescent, width, height, fontsize,
589 trans):
590 markerline, stemlines, baseline = orig_handle
591 # Check to see if the stemcontainer is storing lines as a list or a
592 # LineCollection. Eventually using a list will be removed, and this
593 # logic can also be removed.
594 using_linecoll = isinstance(stemlines, mcoll.LineCollection)
596 xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
597 width, height, fontsize)
599 ydata = self.get_ydata(legend, xdescent, ydescent,
600 width, height, fontsize)
602 if self._bottom is None:
603 bottom = 0.
604 else:
605 bottom = self._bottom
607 leg_markerline = Line2D(xdata_marker, ydata[:len(xdata_marker)])
608 self.update_prop(leg_markerline, markerline, legend)
610 leg_stemlines = [Line2D([x, x], [bottom, y])
611 for x, y in zip(xdata_marker, ydata)]
613 if using_linecoll:
614 # change the function used by update_prop() from the default
615 # to one that handles LineCollection
616 orig_update_func = self._update_prop_func
617 self._update_prop_func = self._copy_collection_props
619 for line in leg_stemlines:
620 self.update_prop(line, stemlines, legend)
622 else:
623 for lm, m in zip(leg_stemlines, stemlines):
624 self.update_prop(lm, m, legend)
626 if using_linecoll:
627 self._update_prop_func = orig_update_func
629 leg_baseline = Line2D([np.min(xdata), np.max(xdata)],
630 [bottom, bottom])
631 self.update_prop(leg_baseline, baseline, legend)
633 artists = [*leg_stemlines, leg_baseline, leg_markerline]
634 for artist in artists:
635 artist.set_transform(trans)
636 return artists
638 def _copy_collection_props(self, legend_handle, orig_handle):
639 """
640 Method to copy properties from a LineCollection (orig_handle) to a
641 Line2D (legend_handle).
642 """
643 legend_handle.set_color(orig_handle.get_color()[0])
644 legend_handle.set_linestyle(orig_handle.get_linestyle()[0])
647class HandlerTuple(HandlerBase):
648 """
649 Handler for Tuple.
651 Additional kwargs are passed through to `HandlerBase`.
653 Parameters
654 ----------
655 ndivide : int, optional
656 The number of sections to divide the legend area into. If None,
657 use the length of the input tuple. Default is 1.
660 pad : float, optional
661 If None, fall back to ``legend.borderpad`` as the default.
662 In units of fraction of font size. Default is None.
663 """
664 def __init__(self, ndivide=1, pad=None, **kwargs):
666 self._ndivide = ndivide
667 self._pad = pad
668 HandlerBase.__init__(self, **kwargs)
670 def create_artists(self, legend, orig_handle,
671 xdescent, ydescent, width, height, fontsize,
672 trans):
674 handler_map = legend.get_legend_handler_map()
676 if self._ndivide is None:
677 ndivide = len(orig_handle)
678 else:
679 ndivide = self._ndivide
681 if self._pad is None:
682 pad = legend.borderpad * fontsize
683 else:
684 pad = self._pad * fontsize
686 if ndivide > 1:
687 width = (width - pad * (ndivide - 1)) / ndivide
689 xds_cycle = cycle(xdescent - (width + pad) * np.arange(ndivide))
691 a_list = []
692 for handle1 in orig_handle:
693 handler = legend.get_legend_handler(handler_map, handle1)
694 _a_list = handler.create_artists(
695 legend, handle1,
696 next(xds_cycle), ydescent, width, height, fontsize, trans)
697 a_list.extend(_a_list)
699 return a_list
702class HandlerPolyCollection(HandlerBase):
703 """
704 Handler for `.PolyCollection` used in `~.Axes.fill_between` and
705 `~.Axes.stackplot`.
706 """
707 def _update_prop(self, legend_handle, orig_handle):
708 def first_color(colors):
709 if colors is None:
710 return None
711 colors = mcolors.to_rgba_array(colors)
712 if len(colors):
713 return colors[0]
714 else:
715 return "none"
717 def get_first(prop_array):
718 if len(prop_array):
719 return prop_array[0]
720 else:
721 return None
722 edgecolor = getattr(orig_handle, '_original_edgecolor',
723 orig_handle.get_edgecolor())
724 legend_handle.set_edgecolor(first_color(edgecolor))
725 facecolor = getattr(orig_handle, '_original_facecolor',
726 orig_handle.get_facecolor())
727 legend_handle.set_facecolor(first_color(facecolor))
728 legend_handle.set_fill(orig_handle.get_fill())
729 legend_handle.set_hatch(orig_handle.get_hatch())
730 legend_handle.set_linewidth(get_first(orig_handle.get_linewidths()))
731 legend_handle.set_linestyle(get_first(orig_handle.get_linestyles()))
732 legend_handle.set_transform(get_first(orig_handle.get_transforms()))
733 legend_handle.set_figure(orig_handle.get_figure())
734 legend_handle.set_alpha(orig_handle.get_alpha())
736 def create_artists(self, legend, orig_handle,
737 xdescent, ydescent, width, height, fontsize, trans):
738 p = Rectangle(xy=(-xdescent, -ydescent),
739 width=width, height=height)
740 self.update_prop(p, orig_handle, legend)
741 p.set_transform(trans)
742 return [p]