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

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"""
2Classes for the efficient drawing of large collections of objects that
3share most properties, e.g., a large number of line segments or
4polygons.
6The classes are not meant to be as flexible as their single element
7counterparts (e.g., you may not be able to select all line styles) but
8they are meant to be fast for common use cases (e.g., a large set of solid
9line segments).
10"""
12import math
13from numbers import Number
14import numpy as np
16import matplotlib as mpl
17from . import (_path, artist, cbook, cm, colors as mcolors, docstring,
18 lines as mlines, path as mpath, transforms)
19import warnings
22@cbook._define_aliases({
23 "antialiased": ["antialiaseds", "aa"],
24 "edgecolor": ["edgecolors", "ec"],
25 "facecolor": ["facecolors", "fc"],
26 "linestyle": ["linestyles", "dashes", "ls"],
27 "linewidth": ["linewidths", "lw"],
28})
29class Collection(artist.Artist, cm.ScalarMappable):
30 """
31 Base class for Collections. Must be subclassed to be usable.
33 All properties in a collection must be sequences or scalars;
34 if scalars, they will be converted to sequences. The
35 property of the ith element of the collection is::
37 prop[i % len(props)]
39 Exceptions are *capstyle* and *joinstyle* properties, these can
40 only be set globally for the whole collection.
42 Keyword arguments and default values:
44 * *edgecolors*: None
45 * *facecolors*: None
46 * *linewidths*: None
47 * *capstyle*: None
48 * *joinstyle*: None
49 * *antialiaseds*: None
50 * *offsets*: None
51 * *transOffset*: transforms.IdentityTransform()
52 * *offset_position*: 'screen' (default) or 'data'
53 * *norm*: None (optional for
54 :class:`matplotlib.cm.ScalarMappable`)
55 * *cmap*: None (optional for
56 :class:`matplotlib.cm.ScalarMappable`)
57 * *hatch*: None
58 * *zorder*: 1
60 *offsets* and *transOffset* are used to translate the patch after
61 rendering (default no offsets). If offset_position is 'screen'
62 (default) the offset is applied after the master transform has
63 been applied, that is, the offsets are in screen coordinates. If
64 offset_position is 'data', the offset is applied before the master
65 transform, i.e., the offsets are in data coordinates.
67 If any of *edgecolors*, *facecolors*, *linewidths*, *antialiaseds*
68 are None, they default to their :data:`matplotlib.rcParams` patch
69 setting, in sequence form.
71 The use of :class:`~matplotlib.cm.ScalarMappable` is optional. If
72 the :class:`~matplotlib.cm.ScalarMappable` matrix _A is not None
73 (i.e., a call to set_array has been made), at draw time a call to
74 scalar mappable will be made to set the face colors.
75 """
76 _offsets = np.zeros((0, 2))
77 _transOffset = transforms.IdentityTransform()
78 #: Either a list of 3x3 arrays or an Nx3x3 array of transforms, suitable
79 #: for the `all_transforms` argument to
80 #: :meth:`~matplotlib.backend_bases.RendererBase.draw_path_collection`;
81 #: each 3x3 array is used to initialize an
82 #: :class:`~matplotlib.transforms.Affine2D` object.
83 #: Each kind of collection defines this based on its arguments.
84 _transforms = np.empty((0, 3, 3))
86 # Whether to draw an edge by default. Set on a
87 # subclass-by-subclass basis.
88 _edge_default = False
90 def __init__(self,
91 edgecolors=None,
92 facecolors=None,
93 linewidths=None,
94 linestyles='solid',
95 capstyle=None,
96 joinstyle=None,
97 antialiaseds=None,
98 offsets=None,
99 transOffset=None,
100 norm=None, # optional for ScalarMappable
101 cmap=None, # ditto
102 pickradius=5.0,
103 hatch=None,
104 urls=None,
105 offset_position='screen',
106 zorder=1,
107 **kwargs
108 ):
109 """
110 Create a Collection
112 %(Collection)s
113 """
114 artist.Artist.__init__(self)
115 cm.ScalarMappable.__init__(self, norm, cmap)
116 # list of un-scaled dash patterns
117 # this is needed scaling the dash pattern by linewidth
118 self._us_linestyles = [(None, None)]
119 # list of dash patterns
120 self._linestyles = [(None, None)]
121 # list of unbroadcast/scaled linewidths
122 self._us_lw = [0]
123 self._linewidths = [0]
124 self._is_filled = True # May be modified by set_facecolor().
126 self._hatch_color = mcolors.to_rgba(mpl.rcParams['hatch.color'])
127 self.set_facecolor(facecolors)
128 self.set_edgecolor(edgecolors)
129 self.set_linewidth(linewidths)
130 self.set_linestyle(linestyles)
131 self.set_antialiased(antialiaseds)
132 self.set_pickradius(pickradius)
133 self.set_urls(urls)
134 self.set_hatch(hatch)
135 self.set_offset_position(offset_position)
136 self.set_zorder(zorder)
138 if capstyle:
139 self.set_capstyle(capstyle)
140 else:
141 self._capstyle = None
143 if joinstyle:
144 self.set_joinstyle(joinstyle)
145 else:
146 self._joinstyle = None
148 self._offsets = np.zeros((1, 2))
149 # save if offsets passed in were none...
150 self._offsetsNone = offsets is None
151 self._uniform_offsets = None
152 if offsets is not None:
153 offsets = np.asanyarray(offsets, float)
154 # Broadcast (2,) -> (1, 2) but nothing else.
155 if offsets.shape == (2,):
156 offsets = offsets[None, :]
157 if transOffset is not None:
158 self._offsets = offsets
159 self._transOffset = transOffset
160 else:
161 self._uniform_offsets = offsets
163 self._path_effects = None
164 self.update(kwargs)
165 self._paths = None
167 def get_paths(self):
168 return self._paths
170 def set_paths(self):
171 raise NotImplementedError
173 def get_transforms(self):
174 return self._transforms
176 def get_offset_transform(self):
177 t = self._transOffset
178 if (not isinstance(t, transforms.Transform)
179 and hasattr(t, '_as_mpl_transform')):
180 t = t._as_mpl_transform(self.axes)
181 return t
183 def get_datalim(self, transData):
185 # Get the automatic datalim of the collection.
186 #
187 # This operation depends on the transforms for the data in the
188 # collection and whether the collection has offsets.
189 #
190 # 1) offsets = None, transform child of transData: use the paths for
191 # the automatic limits (i.e. for LineCollection in streamline).
192 # 2) offsets != None: offset_transform is child of transData:
193 # a) transform is child of transData: use the path + offset for
194 # limits (i.e for bar).
195 # b) transform is not a child of transData: just use the offsets
196 # for the limits (i.e. for scatter)
197 # 3) otherwise return a null Bbox.
199 transform = self.get_transform()
200 transOffset = self.get_offset_transform()
201 if (not self._offsetsNone and
202 not transOffset.contains_branch(transData)):
203 # if there are offsets but in some co-ords other than data,
204 # then don't use them for autoscaling.
205 return transforms.Bbox.null()
206 offsets = self._offsets
208 paths = self.get_paths()
210 if not transform.is_affine:
211 paths = [transform.transform_path_non_affine(p) for p in paths]
212 # Don't convert transform to transform.get_affine() here because
213 # we may have transform.contains_branch(transData) but not
214 # transforms.get_affine().contains_branch(transData). But later,
215 # be careful to only apply the affine part that remains.
217 if isinstance(offsets, np.ma.MaskedArray):
218 offsets = offsets.filled(np.nan)
219 # get_path_collection_extents handles nan but not masked arrays
221 if len(paths) and len(offsets):
222 if any(transform.contains_branch_seperately(transData)):
223 # collections that are just in data units (like quiver)
224 # can properly have the axes limits set by their shape +
225 # offset. LineCollections that have no offsets can
226 # also use this algorithm (like streamplot).
227 result = mpath.get_path_collection_extents(
228 transform.get_affine(), paths, self.get_transforms(),
229 transOffset.transform_non_affine(offsets),
230 transOffset.get_affine().frozen())
231 return result.transformed(transData.inverted())
232 if not self._offsetsNone:
233 # this is for collections that have their paths (shapes)
234 # in physical, axes-relative, or figure-relative units
235 # (i.e. like scatter). We can't uniquely set limits based on
236 # those shapes, so we just set the limits based on their
237 # location.
239 offsets = (transOffset - transData).transform(offsets)
240 # note A-B means A B^{-1}
241 offsets = np.ma.masked_invalid(offsets)
242 if not offsets.mask.all():
243 points = np.row_stack((offsets.min(axis=0),
244 offsets.max(axis=0)))
245 return transforms.Bbox(points)
246 return transforms.Bbox.null()
248 def get_window_extent(self, renderer):
249 # TODO: check to ensure that this does not fail for
250 # cases other than scatter plot legend
251 return self.get_datalim(transforms.IdentityTransform())
253 def _prepare_points(self):
254 # Helper for drawing and hit testing.
256 transform = self.get_transform()
257 transOffset = self.get_offset_transform()
258 offsets = self._offsets
259 paths = self.get_paths()
261 if self.have_units():
262 paths = []
263 for path in self.get_paths():
264 vertices = path.vertices
265 xs, ys = vertices[:, 0], vertices[:, 1]
266 xs = self.convert_xunits(xs)
267 ys = self.convert_yunits(ys)
268 paths.append(mpath.Path(np.column_stack([xs, ys]), path.codes))
269 if offsets.size:
270 xs = self.convert_xunits(offsets[:, 0])
271 ys = self.convert_yunits(offsets[:, 1])
272 offsets = np.column_stack([xs, ys])
274 if not transform.is_affine:
275 paths = [transform.transform_path_non_affine(path)
276 for path in paths]
277 transform = transform.get_affine()
278 if not transOffset.is_affine:
279 offsets = transOffset.transform_non_affine(offsets)
280 # This might have changed an ndarray into a masked array.
281 transOffset = transOffset.get_affine()
283 if isinstance(offsets, np.ma.MaskedArray):
284 offsets = offsets.filled(np.nan)
285 # Changing from a masked array to nan-filled ndarray
286 # is probably most efficient at this point.
288 return transform, transOffset, offsets, paths
290 @artist.allow_rasterization
291 def draw(self, renderer):
292 if not self.get_visible():
293 return
294 renderer.open_group(self.__class__.__name__, self.get_gid())
296 self.update_scalarmappable()
298 transform, transOffset, offsets, paths = self._prepare_points()
300 gc = renderer.new_gc()
301 self._set_gc_clip(gc)
302 gc.set_snap(self.get_snap())
304 if self._hatch:
305 gc.set_hatch(self._hatch)
306 try:
307 gc.set_hatch_color(self._hatch_color)
308 except AttributeError:
309 # if we end up with a GC that does not have this method
310 cbook.warn_deprecated(
311 "3.1", message="Your backend does not support setting the "
312 "hatch color; such backends will become unsupported in "
313 "Matplotlib 3.3.")
315 if self.get_sketch_params() is not None:
316 gc.set_sketch_params(*self.get_sketch_params())
318 if self.get_path_effects():
319 from matplotlib.patheffects import PathEffectRenderer
320 renderer = PathEffectRenderer(self.get_path_effects(), renderer)
322 # If the collection is made up of a single shape/color/stroke,
323 # it can be rendered once and blitted multiple times, using
324 # `draw_markers` rather than `draw_path_collection`. This is
325 # *much* faster for Agg, and results in smaller file sizes in
326 # PDF/SVG/PS.
328 trans = self.get_transforms()
329 facecolors = self.get_facecolor()
330 edgecolors = self.get_edgecolor()
331 do_single_path_optimization = False
332 if (len(paths) == 1 and len(trans) <= 1 and
333 len(facecolors) == 1 and len(edgecolors) == 1 and
334 len(self._linewidths) == 1 and
335 self._linestyles == [(None, None)] and
336 len(self._antialiaseds) == 1 and len(self._urls) == 1 and
337 self.get_hatch() is None):
338 if len(trans):
339 combined_transform = transforms.Affine2D(trans[0]) + transform
340 else:
341 combined_transform = transform
342 extents = paths[0].get_extents(combined_transform)
343 if (extents.width < self.figure.bbox.width
344 and extents.height < self.figure.bbox.height):
345 do_single_path_optimization = True
347 if self._joinstyle:
348 gc.set_joinstyle(self._joinstyle)
350 if self._capstyle:
351 gc.set_capstyle(self._capstyle)
353 if do_single_path_optimization:
354 gc.set_foreground(tuple(edgecolors[0]))
355 gc.set_linewidth(self._linewidths[0])
356 gc.set_dashes(*self._linestyles[0])
357 gc.set_antialiased(self._antialiaseds[0])
358 gc.set_url(self._urls[0])
359 renderer.draw_markers(
360 gc, paths[0], combined_transform.frozen(),
361 mpath.Path(offsets), transOffset, tuple(facecolors[0]))
362 else:
363 renderer.draw_path_collection(
364 gc, transform.frozen(), paths,
365 self.get_transforms(), offsets, transOffset,
366 self.get_facecolor(), self.get_edgecolor(),
367 self._linewidths, self._linestyles,
368 self._antialiaseds, self._urls,
369 self._offset_position)
371 gc.restore()
372 renderer.close_group(self.__class__.__name__)
373 self.stale = False
375 def set_pickradius(self, pr):
376 """
377 Set the pick radius used for containment tests.
379 Parameters
380 ----------
381 d : float
382 Pick radius, in points.
383 """
384 self._pickradius = pr
386 def get_pickradius(self):
387 return self._pickradius
389 def contains(self, mouseevent):
390 """
391 Test whether the mouse event occurred in the collection.
393 Returns ``bool, dict(ind=itemlist)``, where every item in itemlist
394 contains the event.
395 """
396 inside, info = self._default_contains(mouseevent)
397 if inside is not None:
398 return inside, info
400 if not self.get_visible():
401 return False, {}
403 pickradius = (
404 float(self._picker)
405 if isinstance(self._picker, Number) and
406 self._picker is not True # the bool, not just nonzero or 1
407 else self._pickradius)
409 if self.axes and self.get_offset_position() == "data":
410 self.axes._unstale_viewLim()
412 transform, transOffset, offsets, paths = self._prepare_points()
414 ind = _path.point_in_path_collection(
415 mouseevent.x, mouseevent.y, pickradius,
416 transform.frozen(), paths, self.get_transforms(),
417 offsets, transOffset, pickradius <= 0,
418 self.get_offset_position())
420 return len(ind) > 0, dict(ind=ind)
422 def set_urls(self, urls):
423 """
424 Parameters
425 ----------
426 urls : List[str] or None
427 """
428 self._urls = urls if urls is not None else [None]
429 self.stale = True
431 def get_urls(self):
432 return self._urls
434 def set_hatch(self, hatch):
435 r"""
436 Set the hatching pattern
438 *hatch* can be one of::
440 / - diagonal hatching
441 \ - back diagonal
442 | - vertical
443 - - horizontal
444 + - crossed
445 x - crossed diagonal
446 o - small circle
447 O - large circle
448 . - dots
449 * - stars
451 Letters can be combined, in which case all the specified
452 hatchings are done. If same letter repeats, it increases the
453 density of hatching of that pattern.
455 Hatching is supported in the PostScript, PDF, SVG and Agg
456 backends only.
458 Unlike other properties such as linewidth and colors, hatching
459 can only be specified for the collection as a whole, not separately
460 for each member.
462 Parameters
463 ----------
464 hatch : {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
465 """
466 self._hatch = hatch
467 self.stale = True
469 def get_hatch(self):
470 """Return the current hatching pattern."""
471 return self._hatch
473 def set_offsets(self, offsets):
474 """
475 Set the offsets for the collection.
477 Parameters
478 ----------
479 offsets : array-like (N, 2) or (2,)
480 """
481 offsets = np.asanyarray(offsets, float)
482 if offsets.shape == (2,): # Broadcast (2,) -> (1, 2) but nothing else.
483 offsets = offsets[None, :]
484 # This decision is based on how they are initialized above in __init__.
485 if self._uniform_offsets is None:
486 self._offsets = offsets
487 else:
488 self._uniform_offsets = offsets
489 self.stale = True
491 def get_offsets(self):
492 """Return the offsets for the collection."""
493 # This decision is based on how they are initialized above in __init__.
494 if self._uniform_offsets is None:
495 return self._offsets
496 else:
497 return self._uniform_offsets
499 def set_offset_position(self, offset_position):
500 """
501 Set how offsets are applied. If *offset_position* is 'screen'
502 (default) the offset is applied after the master transform has
503 been applied, that is, the offsets are in screen coordinates.
504 If offset_position is 'data', the offset is applied before the
505 master transform, i.e., the offsets are in data coordinates.
507 Parameters
508 ----------
509 offset_position : {'screen', 'data'}
510 """
511 cbook._check_in_list(['screen', 'data'],
512 offset_position=offset_position)
513 self._offset_position = offset_position
514 self.stale = True
516 def get_offset_position(self):
517 """
518 Returns how offsets are applied for the collection. If
519 *offset_position* is 'screen', the offset is applied after the
520 master transform has been applied, that is, the offsets are in
521 screen coordinates. If offset_position is 'data', the offset
522 is applied before the master transform, i.e., the offsets are
523 in data coordinates.
524 """
525 return self._offset_position
527 def set_linewidth(self, lw):
528 """
529 Set the linewidth(s) for the collection. *lw* can be a scalar
530 or a sequence; if it is a sequence the patches will cycle
531 through the sequence
533 Parameters
534 ----------
535 lw : float or sequence of floats
536 """
537 if lw is None:
538 lw = mpl.rcParams['patch.linewidth']
539 if lw is None:
540 lw = mpl.rcParams['lines.linewidth']
541 # get the un-scaled/broadcast lw
542 self._us_lw = np.atleast_1d(np.asarray(lw))
544 # scale all of the dash patterns.
545 self._linewidths, self._linestyles = self._bcast_lwls(
546 self._us_lw, self._us_linestyles)
547 self.stale = True
549 def set_linestyle(self, ls):
550 """
551 Set the linestyle(s) for the collection.
553 =========================== =================
554 linestyle description
555 =========================== =================
556 ``'-'`` or ``'solid'`` solid line
557 ``'--'`` or ``'dashed'`` dashed line
558 ``'-.'`` or ``'dashdot'`` dash-dotted line
559 ``':'`` or ``'dotted'`` dotted line
560 =========================== =================
562 Alternatively a dash tuple of the following form can be provided::
564 (offset, onoffseq),
566 where ``onoffseq`` is an even length tuple of on and off ink in points.
568 Parameters
569 ----------
570 ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
571 The line style.
572 """
573 try:
574 if isinstance(ls, str):
575 ls = cbook.ls_mapper.get(ls, ls)
576 dashes = [mlines._get_dash_pattern(ls)]
577 else:
578 try:
579 dashes = [mlines._get_dash_pattern(ls)]
580 except ValueError:
581 dashes = [mlines._get_dash_pattern(x) for x in ls]
583 except ValueError:
584 raise ValueError(
585 'Do not know how to convert {!r} to dashes'.format(ls))
587 # get the list of raw 'unscaled' dash patterns
588 self._us_linestyles = dashes
590 # broadcast and scale the lw and dash patterns
591 self._linewidths, self._linestyles = self._bcast_lwls(
592 self._us_lw, self._us_linestyles)
594 def set_capstyle(self, cs):
595 """
596 Set the capstyle for the collection (for all its elements).
598 Parameters
599 ----------
600 cs : {'butt', 'round', 'projecting'}
601 The capstyle
602 """
603 cbook._check_in_list(('butt', 'round', 'projecting'), capstyle=cs)
604 self._capstyle = cs
606 def get_capstyle(self):
607 return self._capstyle
609 def set_joinstyle(self, js):
610 """
611 Set the joinstyle for the collection (for all its elements).
613 Parameters
614 ----------
615 js : {'miter', 'round', 'bevel'}
616 The joinstyle
617 """
618 cbook._check_in_list(('miter', 'round', 'bevel'), joinstyle=js)
619 self._joinstyle = js
621 def get_joinstyle(self):
622 return self._joinstyle
624 @staticmethod
625 def _bcast_lwls(linewidths, dashes):
626 """
627 Internal helper function to broadcast + scale ls/lw
629 In the collection drawing code, the linewidth and linestyle are cycled
630 through as circular buffers (via ``v[i % len(v)]``). Thus, if we are
631 going to scale the dash pattern at set time (not draw time) we need to
632 do the broadcasting now and expand both lists to be the same length.
634 Parameters
635 ----------
636 linewidths : list
637 line widths of collection
638 dashes : list
639 dash specification (offset, (dash pattern tuple))
641 Returns
642 -------
643 linewidths, dashes : list
644 Will be the same length, dashes are scaled by paired linewidth
645 """
646 if mpl.rcParams['_internal.classic_mode']:
647 return linewidths, dashes
648 # make sure they are the same length so we can zip them
649 if len(dashes) != len(linewidths):
650 l_dashes = len(dashes)
651 l_lw = len(linewidths)
652 gcd = math.gcd(l_dashes, l_lw)
653 dashes = list(dashes) * (l_lw // gcd)
654 linewidths = list(linewidths) * (l_dashes // gcd)
656 # scale the dash patters
657 dashes = [mlines._scale_dashes(o, d, lw)
658 for (o, d), lw in zip(dashes, linewidths)]
660 return linewidths, dashes
662 def set_antialiased(self, aa):
663 """
664 Set the antialiasing state for rendering.
666 Parameters
667 ----------
668 aa : bool or sequence of bools
669 """
670 if aa is None:
671 aa = mpl.rcParams['patch.antialiased']
672 self._antialiaseds = np.atleast_1d(np.asarray(aa, bool))
673 self.stale = True
675 def set_color(self, c):
676 """
677 Set both the edgecolor and the facecolor.
679 Parameters
680 ----------
681 c : color or sequence of rgba tuples
683 See Also
684 --------
685 Collection.set_facecolor, Collection.set_edgecolor
686 For setting the edge or face color individually.
687 """
688 self.set_facecolor(c)
689 self.set_edgecolor(c)
691 def _set_facecolor(self, c):
692 if c is None:
693 c = mpl.rcParams['patch.facecolor']
695 self._is_filled = True
696 try:
697 if c.lower() == 'none':
698 self._is_filled = False
699 except AttributeError:
700 pass
701 self._facecolors = mcolors.to_rgba_array(c, self._alpha)
702 self.stale = True
704 def set_facecolor(self, c):
705 """
706 Set the facecolor(s) of the collection. *c* can be a color (all patches
707 have same color), or a sequence of colors; if it is a sequence the
708 patches will cycle through the sequence.
710 If *c* is 'none', the patch will not be filled.
712 Parameters
713 ----------
714 c : color or sequence of colors
715 """
716 self._original_facecolor = c
717 self._set_facecolor(c)
719 def get_facecolor(self):
720 return self._facecolors
722 def get_edgecolor(self):
723 if cbook._str_equal(self._edgecolors, 'face'):
724 return self.get_facecolor()
725 else:
726 return self._edgecolors
728 def _set_edgecolor(self, c):
729 set_hatch_color = True
730 if c is None:
731 if (mpl.rcParams['patch.force_edgecolor'] or
732 not self._is_filled or self._edge_default):
733 c = mpl.rcParams['patch.edgecolor']
734 else:
735 c = 'none'
736 set_hatch_color = False
738 self._is_stroked = True
739 try:
740 if c.lower() == 'none':
741 self._is_stroked = False
742 except AttributeError:
743 pass
745 try:
746 if c.lower() == 'face': # Special case: lookup in "get" method.
747 self._edgecolors = 'face'
748 return
749 except AttributeError:
750 pass
751 self._edgecolors = mcolors.to_rgba_array(c, self._alpha)
752 if set_hatch_color and len(self._edgecolors):
753 self._hatch_color = tuple(self._edgecolors[0])
754 self.stale = True
756 def set_edgecolor(self, c):
757 """
758 Set the edgecolor(s) of the collection.
760 Parameters
761 ----------
762 c : color or sequence of colors or 'face'
763 The collection edgecolor(s). If a sequence, the patches cycle
764 through it. If 'face', match the facecolor.
765 """
766 self._original_edgecolor = c
767 self._set_edgecolor(c)
769 def set_alpha(self, alpha):
770 # docstring inherited
771 super().set_alpha(alpha)
772 self.update_dict['array'] = True
773 self._set_facecolor(self._original_facecolor)
774 self._set_edgecolor(self._original_edgecolor)
776 def get_linewidth(self):
777 return self._linewidths
779 def get_linestyle(self):
780 return self._linestyles
782 def update_scalarmappable(self):
783 """Update colors from the scalar mappable array, if it is not None."""
784 if self._A is None:
785 return
786 if self._A.ndim > 1:
787 raise ValueError('Collections can only map rank 1 arrays')
788 if not self.check_update("array"):
789 return
790 if self._is_filled:
791 self._facecolors = self.to_rgba(self._A, self._alpha)
792 elif self._is_stroked:
793 self._edgecolors = self.to_rgba(self._A, self._alpha)
794 self.stale = True
796 def get_fill(self):
797 'return whether fill is set'
798 return self._is_filled
800 def update_from(self, other):
801 'copy properties from other to self'
803 artist.Artist.update_from(self, other)
804 self._antialiaseds = other._antialiaseds
805 self._original_edgecolor = other._original_edgecolor
806 self._edgecolors = other._edgecolors
807 self._original_facecolor = other._original_facecolor
808 self._facecolors = other._facecolors
809 self._linewidths = other._linewidths
810 self._linestyles = other._linestyles
811 self._us_linestyles = other._us_linestyles
812 self._pickradius = other._pickradius
813 self._hatch = other._hatch
815 # update_from for scalarmappable
816 self._A = other._A
817 self.norm = other.norm
818 self.cmap = other.cmap
819 # self.update_dict = other.update_dict # do we need to copy this? -JJL
820 self.stale = True
823# these are not available for the object inspector until after the
824# class is built so we define an initial set here for the init
825# function and they will be overridden after object defn
826docstring.interpd.update(Collection="""\
827 Valid Collection keyword arguments:
829 * *edgecolors*: None
830 * *facecolors*: None
831 * *linewidths*: None
832 * *antialiaseds*: None
833 * *offsets*: None
834 * *transOffset*: transforms.IdentityTransform()
835 * *norm*: None (optional for
836 :class:`matplotlib.cm.ScalarMappable`)
837 * *cmap*: None (optional for
838 :class:`matplotlib.cm.ScalarMappable`)
840 *offsets* and *transOffset* are used to translate the patch after
841 rendering (default no offsets)
843 If any of *edgecolors*, *facecolors*, *linewidths*, *antialiaseds*
844 are None, they default to their :data:`matplotlib.rcParams` patch
845 setting, in sequence form.
846""")
849class _CollectionWithSizes(Collection):
850 """
851 Base class for collections that have an array of sizes.
852 """
853 _factor = 1.0
855 def get_sizes(self):
856 """
857 Returns the sizes of the elements in the collection. The
858 value represents the 'area' of the element.
860 Returns
861 -------
862 sizes : array
863 The 'area' of each element.
864 """
865 return self._sizes
867 def set_sizes(self, sizes, dpi=72.0):
868 """
869 Set the sizes of each member of the collection.
871 Parameters
872 ----------
873 sizes : ndarray or None
874 The size to set for each element of the collection. The
875 value is the 'area' of the element.
876 dpi : float
877 The dpi of the canvas. Defaults to 72.0.
878 """
879 if sizes is None:
880 self._sizes = np.array([])
881 self._transforms = np.empty((0, 3, 3))
882 else:
883 self._sizes = np.asarray(sizes)
884 self._transforms = np.zeros((len(self._sizes), 3, 3))
885 scale = np.sqrt(self._sizes) * dpi / 72.0 * self._factor
886 self._transforms[:, 0, 0] = scale
887 self._transforms[:, 1, 1] = scale
888 self._transforms[:, 2, 2] = 1.0
889 self.stale = True
891 @artist.allow_rasterization
892 def draw(self, renderer):
893 self.set_sizes(self._sizes, self.figure.dpi)
894 Collection.draw(self, renderer)
897class PathCollection(_CollectionWithSizes):
898 """
899 This is the most basic :class:`Collection` subclass.
900 A :class:`PathCollection` is e.g. created by a :meth:`~.Axes.scatter` plot.
901 """
902 @docstring.dedent_interpd
903 def __init__(self, paths, sizes=None, **kwargs):
904 """
905 *paths* is a sequence of :class:`matplotlib.path.Path`
906 instances.
908 %(Collection)s
909 """
911 Collection.__init__(self, **kwargs)
912 self.set_paths(paths)
913 self.set_sizes(sizes)
914 self.stale = True
916 def set_paths(self, paths):
917 self._paths = paths
918 self.stale = True
920 def get_paths(self):
921 return self._paths
923 def legend_elements(self, prop="colors", num="auto",
924 fmt=None, func=lambda x: x, **kwargs):
925 """
926 Creates legend handles and labels for a PathCollection. This is useful
927 for obtaining a legend for a :meth:`~.Axes.scatter` plot. E.g.::
929 scatter = plt.scatter([1, 2, 3], [4, 5, 6], c=[7, 2, 3])
930 plt.legend(*scatter.legend_elements())
932 Also see the :ref:`automatedlegendcreation` example.
934 Parameters
935 ----------
936 prop : string, optional, default *"colors"*
937 Can be *"colors"* or *"sizes"*. In case of *"colors"*, the legend
938 handles will show the different colors of the collection. In case
939 of "sizes", the legend will show the different sizes.
940 num : int, None, "auto" (default), array-like, or `~.ticker.Locator`,
941 optional
942 Target number of elements to create.
943 If None, use all unique elements of the mappable array. If an
944 integer, target to use *num* elements in the normed range.
945 If *"auto"*, try to determine which option better suits the nature
946 of the data.
947 The number of created elements may slightly deviate from *num* due
948 to a `~.ticker.Locator` being used to find useful locations.
949 If a list or array, use exactly those elements for the legend.
950 Finally, a `~.ticker.Locator` can be provided.
951 fmt : str, `~matplotlib.ticker.Formatter`, or None (default)
952 The format or formatter to use for the labels. If a string must be
953 a valid input for a `~.StrMethodFormatter`. If None (the default),
954 use a `~.ScalarFormatter`.
955 func : function, default *lambda x: x*
956 Function to calculate the labels. Often the size (or color)
957 argument to :meth:`~.Axes.scatter` will have been pre-processed
958 by the user using a function *s = f(x)* to make the markers
959 visible; e.g. *size = np.log10(x)*. Providing the inverse of this
960 function here allows that pre-processing to be inverted, so that
961 the legend labels have the correct values;
962 e.g. *func = np.exp(x, 10)*.
963 kwargs : further parameters
964 Allowed keyword arguments are *color* and *size*. E.g. it may be
965 useful to set the color of the markers if *prop="sizes"* is used;
966 similarly to set the size of the markers if *prop="colors"* is
967 used. Any further parameters are passed onto the `.Line2D`
968 instance. This may be useful to e.g. specify a different
969 *markeredgecolor* or *alpha* for the legend handles.
971 Returns
972 -------
973 tuple (handles, labels)
974 with *handles* being a list of `.Line2D` objects
975 and *labels* a matching list of strings.
976 """
977 handles = []
978 labels = []
979 hasarray = self.get_array() is not None
980 if fmt is None:
981 fmt = mpl.ticker.ScalarFormatter(useOffset=False, useMathText=True)
982 elif isinstance(fmt, str):
983 fmt = mpl.ticker.StrMethodFormatter(fmt)
984 fmt.create_dummy_axis()
986 if prop == "colors":
987 if not hasarray:
988 warnings.warn("Collection without array used. Make sure to "
989 "specify the values to be colormapped via the "
990 "`c` argument.")
991 return handles, labels
992 u = np.unique(self.get_array())
993 size = kwargs.pop("size", mpl.rcParams["lines.markersize"])
994 elif prop == "sizes":
995 u = np.unique(self.get_sizes())
996 color = kwargs.pop("color", "k")
997 else:
998 raise ValueError("Valid values for `prop` are 'colors' or "
999 f"'sizes'. You supplied '{prop}' instead.")
1001 fmt.set_bounds(func(u).min(), func(u).max())
1002 if num == "auto":
1003 num = 9
1004 if len(u) <= num:
1005 num = None
1006 if num is None:
1007 values = u
1008 label_values = func(values)
1009 else:
1010 if prop == "colors":
1011 arr = self.get_array()
1012 elif prop == "sizes":
1013 arr = self.get_sizes()
1014 if isinstance(num, mpl.ticker.Locator):
1015 loc = num
1016 elif np.iterable(num):
1017 loc = mpl.ticker.FixedLocator(num)
1018 else:
1019 num = int(num)
1020 loc = mpl.ticker.MaxNLocator(nbins=num, min_n_ticks=num-1,
1021 steps=[1, 2, 2.5, 3, 5, 6, 8, 10])
1022 label_values = loc.tick_values(func(arr).min(), func(arr).max())
1023 cond = ((label_values >= func(arr).min()) &
1024 (label_values <= func(arr).max()))
1025 label_values = label_values[cond]
1026 xarr = np.linspace(arr.min(), arr.max(), 256)
1027 values = np.interp(label_values, func(xarr), xarr)
1029 kw = dict(markeredgewidth=self.get_linewidths()[0],
1030 alpha=self.get_alpha())
1031 kw.update(kwargs)
1033 for val, lab in zip(values, label_values):
1034 if prop == "colors":
1035 color = self.cmap(self.norm(val))
1036 elif prop == "sizes":
1037 size = np.sqrt(val)
1038 if np.isclose(size, 0.0):
1039 continue
1040 h = mlines.Line2D([0], [0], ls="", color=color, ms=size,
1041 marker=self.get_paths()[0], **kw)
1042 handles.append(h)
1043 if hasattr(fmt, "set_locs"):
1044 fmt.set_locs(label_values)
1045 l = fmt(lab)
1046 labels.append(l)
1048 return handles, labels
1051class PolyCollection(_CollectionWithSizes):
1052 @docstring.dedent_interpd
1053 def __init__(self, verts, sizes=None, closed=True, **kwargs):
1054 """
1055 *verts* is a sequence of ( *verts0*, *verts1*, ...) where
1056 *verts_i* is a sequence of *xy* tuples of vertices, or an
1057 equivalent :mod:`numpy` array of shape (*nv*, 2).
1059 *sizes* is *None* (default) or a sequence of floats that
1060 scale the corresponding *verts_i*. The scaling is applied
1061 before the Artist master transform; if the latter is an identity
1062 transform, then the overall scaling is such that if
1063 *verts_i* specify a unit square, then *sizes_i* is the area
1064 of that square in points^2.
1065 If len(*sizes*) < *nv*, the additional values will be
1066 taken cyclically from the array.
1068 *closed*, when *True*, will explicitly close the polygon.
1070 %(Collection)s
1071 """
1072 Collection.__init__(self, **kwargs)
1073 self.set_sizes(sizes)
1074 self.set_verts(verts, closed)
1075 self.stale = True
1077 def set_verts(self, verts, closed=True):
1078 '''This allows one to delay initialization of the vertices.'''
1079 if isinstance(verts, np.ma.MaskedArray):
1080 verts = verts.astype(float).filled(np.nan)
1081 # This is much faster than having Path do it one at a time.
1082 if closed:
1083 self._paths = []
1084 for xy in verts:
1085 if len(xy):
1086 if isinstance(xy, np.ma.MaskedArray):
1087 xy = np.ma.concatenate([xy, xy[0:1]])
1088 else:
1089 xy = np.asarray(xy)
1090 xy = np.concatenate([xy, xy[0:1]])
1091 codes = np.empty(xy.shape[0], dtype=mpath.Path.code_type)
1092 codes[:] = mpath.Path.LINETO
1093 codes[0] = mpath.Path.MOVETO
1094 codes[-1] = mpath.Path.CLOSEPOLY
1095 self._paths.append(mpath.Path(xy, codes))
1096 else:
1097 self._paths.append(mpath.Path(xy))
1098 else:
1099 self._paths = [mpath.Path(xy) for xy in verts]
1100 self.stale = True
1102 set_paths = set_verts
1104 def set_verts_and_codes(self, verts, codes):
1105 """This allows one to initialize vertices with path codes."""
1106 if len(verts) != len(codes):
1107 raise ValueError("'codes' must be a 1D list or array "
1108 "with the same length of 'verts'")
1109 self._paths = []
1110 for xy, cds in zip(verts, codes):
1111 if len(xy):
1112 self._paths.append(mpath.Path(xy, cds))
1113 else:
1114 self._paths.append(mpath.Path(xy))
1115 self.stale = True
1118class BrokenBarHCollection(PolyCollection):
1119 """
1120 A collection of horizontal bars spanning *yrange* with a sequence of
1121 *xranges*.
1122 """
1123 @docstring.dedent_interpd
1124 def __init__(self, xranges, yrange, **kwargs):
1125 """
1126 *xranges*
1127 sequence of (*xmin*, *xwidth*)
1129 *yrange*
1130 *ymin*, *ywidth*
1132 %(Collection)s
1133 """
1134 ymin, ywidth = yrange
1135 ymax = ymin + ywidth
1136 verts = [[(xmin, ymin),
1137 (xmin, ymax),
1138 (xmin + xwidth, ymax),
1139 (xmin + xwidth, ymin),
1140 (xmin, ymin)] for xmin, xwidth in xranges]
1141 PolyCollection.__init__(self, verts, **kwargs)
1143 @staticmethod
1144 def span_where(x, ymin, ymax, where, **kwargs):
1145 """
1146 Create a BrokenBarHCollection to plot horizontal bars from
1147 over the regions in *x* where *where* is True. The bars range
1148 on the y-axis from *ymin* to *ymax*
1150 A :class:`BrokenBarHCollection` is returned. *kwargs* are
1151 passed on to the collection.
1152 """
1153 xranges = []
1154 for ind0, ind1 in cbook.contiguous_regions(where):
1155 xslice = x[ind0:ind1]
1156 if not len(xslice):
1157 continue
1158 xranges.append((xslice[0], xslice[-1] - xslice[0]))
1160 collection = BrokenBarHCollection(
1161 xranges, [ymin, ymax - ymin], **kwargs)
1162 return collection
1165class RegularPolyCollection(_CollectionWithSizes):
1166 """Draw a collection of regular polygons with *numsides*."""
1168 _path_generator = mpath.Path.unit_regular_polygon
1169 _factor = np.pi ** (-1/2)
1171 @docstring.dedent_interpd
1172 def __init__(self,
1173 numsides,
1174 rotation=0,
1175 sizes=(1,),
1176 **kwargs):
1177 """
1178 *numsides*
1179 the number of sides of the polygon
1181 *rotation*
1182 the rotation of the polygon in radians
1184 *sizes*
1185 gives the area of the circle circumscribing the
1186 regular polygon in points^2
1188 %(Collection)s
1190 Example: see :doc:`/gallery/event_handling/lasso_demo` for a
1191 complete example::
1193 offsets = np.random.rand(20, 2)
1194 facecolors = [cm.jet(x) for x in np.random.rand(20)]
1196 collection = RegularPolyCollection(
1197 numsides=5, # a pentagon
1198 rotation=0, sizes=(50,),
1199 facecolors=facecolors,
1200 edgecolors=("black",),
1201 linewidths=(1,),
1202 offsets=offsets,
1203 transOffset=ax.transData,
1204 )
1205 """
1206 Collection.__init__(self, **kwargs)
1207 self.set_sizes(sizes)
1208 self._numsides = numsides
1209 self._paths = [self._path_generator(numsides)]
1210 self._rotation = rotation
1211 self.set_transform(transforms.IdentityTransform())
1213 def get_numsides(self):
1214 return self._numsides
1216 def get_rotation(self):
1217 return self._rotation
1219 @artist.allow_rasterization
1220 def draw(self, renderer):
1221 self.set_sizes(self._sizes, self.figure.dpi)
1222 self._transforms = [
1223 transforms.Affine2D(x).rotate(-self._rotation).get_matrix()
1224 for x in self._transforms
1225 ]
1226 Collection.draw(self, renderer)
1229class StarPolygonCollection(RegularPolyCollection):
1230 """Draw a collection of regular stars with *numsides* points."""
1231 _path_generator = mpath.Path.unit_regular_star
1234class AsteriskPolygonCollection(RegularPolyCollection):
1235 """Draw a collection of regular asterisks with *numsides* points."""
1236 _path_generator = mpath.Path.unit_regular_asterisk
1239class LineCollection(Collection):
1240 """
1241 All parameters must be sequences or scalars; if scalars, they will
1242 be converted to sequences. The property of the ith line
1243 segment is::
1245 prop[i % len(props)]
1247 i.e., the properties cycle if the ``len`` of props is less than the
1248 number of segments.
1249 """
1251 _edge_default = True
1253 def __init__(self, segments, # Can be None.
1254 linewidths=None,
1255 colors=None,
1256 antialiaseds=None,
1257 linestyles='solid',
1258 offsets=None,
1259 transOffset=None,
1260 norm=None,
1261 cmap=None,
1262 pickradius=5,
1263 zorder=2,
1264 facecolors='none',
1265 **kwargs
1266 ):
1267 """
1268 Parameters
1269 ----------
1270 segments
1271 A sequence of (*line0*, *line1*, *line2*), where::
1273 linen = (x0, y0), (x1, y1), ... (xm, ym)
1275 or the equivalent numpy array with two columns. Each line
1276 can be a different length.
1278 colors : sequence, optional
1279 A sequence of RGBA tuples (e.g., arbitrary color
1280 strings, etc, not allowed).
1282 antialiaseds : sequence, optional
1283 A sequence of ones or zeros.
1285 linestyles : str or tuple, optional
1286 Either one of {'solid', 'dashed', 'dashdot', 'dotted'}, or
1287 a dash tuple. The dash tuple is::
1289 (offset, onoffseq)
1291 where ``onoffseq`` is an even length tuple of on and off ink
1292 in points.
1294 norm : Normalize, optional
1295 `~.colors.Normalize` instance.
1297 cmap : str or Colormap, optional
1298 Colormap name or `~.colors.Colormap` instance.
1300 pickradius : float, optional
1301 The tolerance in points for mouse clicks picking a line.
1302 Default is 5 pt.
1304 zorder : int, optional
1305 zorder of the LineCollection. Default is 2.
1307 facecolors : optional
1308 The facecolors of the LineCollection. Default is 'none'.
1309 Setting to a value other than 'none' will lead to a filled
1310 polygon being drawn between points on each line.
1312 Notes
1313 -----
1314 If *linewidths*, *colors*, or *antialiaseds* is None, they
1315 default to their rcParams setting, in sequence form.
1317 If *offsets* and *transOffset* are not None, then
1318 *offsets* are transformed by *transOffset* and applied after
1319 the segments have been transformed to display coordinates.
1321 If *offsets* is not None but *transOffset* is None, then the
1322 *offsets* are added to the segments before any transformation.
1323 In this case, a single offset can be specified as::
1325 offsets=(xo, yo)
1327 and this value will be added cumulatively to each successive
1328 segment, so as to produce a set of successively offset curves.
1330 The use of :class:`~matplotlib.cm.ScalarMappable` is optional.
1331 If the :class:`~matplotlib.cm.ScalarMappable` array
1332 :attr:`~matplotlib.cm.ScalarMappable._A` is not None (i.e., a call to
1333 :meth:`~matplotlib.cm.ScalarMappable.set_array` has been made), at
1334 draw time a call to scalar mappable will be made to set the colors.
1335 """
1336 if colors is None:
1337 colors = mpl.rcParams['lines.color']
1338 if linewidths is None:
1339 linewidths = (mpl.rcParams['lines.linewidth'],)
1340 if antialiaseds is None:
1341 antialiaseds = (mpl.rcParams['lines.antialiased'],)
1343 colors = mcolors.to_rgba_array(colors)
1344 Collection.__init__(
1345 self,
1346 edgecolors=colors,
1347 facecolors=facecolors,
1348 linewidths=linewidths,
1349 linestyles=linestyles,
1350 antialiaseds=antialiaseds,
1351 offsets=offsets,
1352 transOffset=transOffset,
1353 norm=norm,
1354 cmap=cmap,
1355 pickradius=pickradius,
1356 zorder=zorder,
1357 **kwargs)
1359 self.set_segments(segments)
1361 def set_segments(self, segments):
1362 if segments is None:
1363 return
1364 _segments = []
1366 for seg in segments:
1367 if not isinstance(seg, np.ma.MaskedArray):
1368 seg = np.asarray(seg, float)
1369 _segments.append(seg)
1371 if self._uniform_offsets is not None:
1372 _segments = self._add_offsets(_segments)
1374 self._paths = [mpath.Path(_seg) for _seg in _segments]
1375 self.stale = True
1377 set_verts = set_segments # for compatibility with PolyCollection
1378 set_paths = set_segments
1380 def get_segments(self):
1381 """
1382 Returns
1383 -------
1384 segments : list
1385 List of segments in the LineCollection. Each list item contains an
1386 array of vertices.
1387 """
1388 segments = []
1390 for path in self._paths:
1391 vertices = [vertex for vertex, _ in path.iter_segments()]
1392 vertices = np.asarray(vertices)
1393 segments.append(vertices)
1395 return segments
1397 def _add_offsets(self, segs):
1398 offsets = self._uniform_offsets
1399 Nsegs = len(segs)
1400 Noffs = offsets.shape[0]
1401 if Noffs == 1:
1402 for i in range(Nsegs):
1403 segs[i] = segs[i] + i * offsets
1404 else:
1405 for i in range(Nsegs):
1406 io = i % Noffs
1407 segs[i] = segs[i] + offsets[io:io + 1]
1408 return segs
1410 def set_color(self, c):
1411 """
1412 Set the color(s) of the LineCollection.
1414 Parameters
1415 ----------
1416 c : color or list of colors
1417 Matplotlib color argument (all patches have same color), or a
1418 sequence or rgba tuples; if it is a sequence the patches will
1419 cycle through the sequence.
1420 """
1421 self.set_edgecolor(c)
1422 self.stale = True
1424 def get_color(self):
1425 return self._edgecolors
1427 get_colors = get_color # for compatibility with old versions
1430class EventCollection(LineCollection):
1431 """
1432 A collection of discrete events.
1434 The events are given by a 1-dimensional array, usually the position of
1435 something along an axis, such as time or length. They do not have an
1436 amplitude and are displayed as vertical or horizontal parallel bars.
1437 """
1439 _edge_default = True
1441 def __init__(self,
1442 positions, # Cannot be None.
1443 orientation=None,
1444 lineoffset=0,
1445 linelength=1,
1446 linewidth=None,
1447 color=None,
1448 linestyle='solid',
1449 antialiased=None,
1450 **kwargs
1451 ):
1452 """
1453 Parameters
1454 ----------
1455 positions : 1D array-like object
1456 Each value is an event.
1458 orientation : {None, 'horizontal', 'vertical'}, optional
1459 The orientation of the **collection** (the event bars are along
1460 the orthogonal direction). Defaults to 'horizontal' if not
1461 specified or None.
1463 lineoffset : scalar, optional, default: 0
1464 The offset of the center of the markers from the origin, in the
1465 direction orthogonal to *orientation*.
1467 linelength : scalar, optional, default: 1
1468 The total height of the marker (i.e. the marker stretches from
1469 ``lineoffset - linelength/2`` to ``lineoffset + linelength/2``).
1471 linewidth : scalar or None, optional, default: None
1472 If it is None, defaults to its rcParams setting, in sequence form.
1474 color : color, sequence of colors or None, optional, default: None
1475 If it is None, defaults to its rcParams setting, in sequence form.
1477 linestyle : str or tuple, optional, default: 'solid'
1478 Valid strings are ['solid', 'dashed', 'dashdot', 'dotted',
1479 '-', '--', '-.', ':']. Dash tuples should be of the form::
1481 (offset, onoffseq),
1483 where *onoffseq* is an even length tuple of on and off ink
1484 in points.
1486 antialiased : {None, 1, 2}, optional
1487 If it is None, defaults to its rcParams setting, in sequence form.
1489 **kwargs : optional
1490 Other keyword arguments are line collection properties. See
1491 :class:`~matplotlib.collections.LineCollection` for a list of
1492 the valid properties.
1494 Examples
1495 --------
1496 .. plot:: gallery/lines_bars_and_markers/eventcollection_demo.py
1497 """
1498 if positions is None:
1499 raise ValueError('positions must be an array-like object')
1500 # Force a copy of positions
1501 positions = np.array(positions, copy=True)
1502 segment = (lineoffset + linelength / 2.,
1503 lineoffset - linelength / 2.)
1504 if positions.size == 0:
1505 segments = []
1506 elif positions.ndim > 1:
1507 raise ValueError('positions cannot be an array with more than '
1508 'one dimension.')
1509 elif (orientation is None or orientation.lower() == 'none' or
1510 orientation.lower() == 'horizontal'):
1511 positions.sort()
1512 segments = [[(coord1, coord2) for coord2 in segment] for
1513 coord1 in positions]
1514 self._is_horizontal = True
1515 elif orientation.lower() == 'vertical':
1516 positions.sort()
1517 segments = [[(coord2, coord1) for coord2 in segment] for
1518 coord1 in positions]
1519 self._is_horizontal = False
1520 else:
1521 cbook._check_in_list(['horizontal', 'vertical'],
1522 orientation=orientation)
1524 LineCollection.__init__(self,
1525 segments,
1526 linewidths=linewidth,
1527 colors=color,
1528 antialiaseds=antialiased,
1529 linestyles=linestyle,
1530 **kwargs)
1532 self._linelength = linelength
1533 self._lineoffset = lineoffset
1535 def get_positions(self):
1536 '''
1537 return an array containing the floating-point values of the positions
1538 '''
1539 pos = 0 if self.is_horizontal() else 1
1540 return [segment[0, pos] for segment in self.get_segments()]
1542 def set_positions(self, positions):
1543 '''
1544 set the positions of the events to the specified value
1545 '''
1546 if positions is None or (hasattr(positions, 'len') and
1547 len(positions) == 0):
1548 self.set_segments([])
1549 return
1551 lineoffset = self.get_lineoffset()
1552 linelength = self.get_linelength()
1553 segment = (lineoffset + linelength / 2.,
1554 lineoffset - linelength / 2.)
1555 positions = np.asanyarray(positions)
1556 positions.sort()
1557 if self.is_horizontal():
1558 segments = [[(coord1, coord2) for coord2 in segment] for
1559 coord1 in positions]
1560 else:
1561 segments = [[(coord2, coord1) for coord2 in segment] for
1562 coord1 in positions]
1563 self.set_segments(segments)
1565 def add_positions(self, position):
1566 '''
1567 add one or more events at the specified positions
1568 '''
1569 if position is None or (hasattr(position, 'len') and
1570 len(position) == 0):
1571 return
1572 positions = self.get_positions()
1573 positions = np.hstack([positions, np.asanyarray(position)])
1574 self.set_positions(positions)
1575 extend_positions = append_positions = add_positions
1577 def is_horizontal(self):
1578 '''
1579 True if the eventcollection is horizontal, False if vertical
1580 '''
1581 return self._is_horizontal
1583 def get_orientation(self):
1584 """
1585 Return the orientation of the event line ('horizontal' or 'vertical').
1586 """
1587 return 'horizontal' if self.is_horizontal() else 'vertical'
1589 def switch_orientation(self):
1590 '''
1591 switch the orientation of the event line, either from vertical to
1592 horizontal or vice versus
1593 '''
1594 segments = self.get_segments()
1595 for i, segment in enumerate(segments):
1596 segments[i] = np.fliplr(segment)
1597 self.set_segments(segments)
1598 self._is_horizontal = not self.is_horizontal()
1599 self.stale = True
1601 def set_orientation(self, orientation=None):
1602 """
1603 Set the orientation of the event line.
1605 Parameters
1606 ----------
1607 orientation: {'horizontal', 'vertical'} or None
1608 Defaults to 'horizontal' if not specified or None.
1609 """
1610 if (orientation is None or orientation.lower() == 'none' or
1611 orientation.lower() == 'horizontal'):
1612 is_horizontal = True
1613 elif orientation.lower() == 'vertical':
1614 is_horizontal = False
1615 else:
1616 cbook._check_in_list(['horizontal', 'vertical'],
1617 orientation=orientation)
1618 if is_horizontal == self.is_horizontal():
1619 return
1620 self.switch_orientation()
1622 def get_linelength(self):
1623 '''
1624 get the length of the lines used to mark each event
1625 '''
1626 return self._linelength
1628 def set_linelength(self, linelength):
1629 '''
1630 set the length of the lines used to mark each event
1631 '''
1632 if linelength == self.get_linelength():
1633 return
1634 lineoffset = self.get_lineoffset()
1635 segments = self.get_segments()
1636 pos = 1 if self.is_horizontal() else 0
1637 for segment in segments:
1638 segment[0, pos] = lineoffset + linelength / 2.
1639 segment[1, pos] = lineoffset - linelength / 2.
1640 self.set_segments(segments)
1641 self._linelength = linelength
1643 def get_lineoffset(self):
1644 '''
1645 get the offset of the lines used to mark each event
1646 '''
1647 return self._lineoffset
1649 def set_lineoffset(self, lineoffset):
1650 '''
1651 set the offset of the lines used to mark each event
1652 '''
1653 if lineoffset == self.get_lineoffset():
1654 return
1655 linelength = self.get_linelength()
1656 segments = self.get_segments()
1657 pos = 1 if self.is_horizontal() else 0
1658 for segment in segments:
1659 segment[0, pos] = lineoffset + linelength / 2.
1660 segment[1, pos] = lineoffset - linelength / 2.
1661 self.set_segments(segments)
1662 self._lineoffset = lineoffset
1664 def get_linewidth(self):
1665 """Get the width of the lines used to mark each event."""
1666 return super(EventCollection, self).get_linewidth()[0]
1668 def get_linewidths(self):
1669 return super(EventCollection, self).get_linewidth()
1671 def get_color(self):
1672 '''
1673 get the color of the lines used to mark each event
1674 '''
1675 return self.get_colors()[0]
1678class CircleCollection(_CollectionWithSizes):
1679 """A collection of circles, drawn using splines."""
1681 _factor = np.pi ** (-1/2)
1683 @docstring.dedent_interpd
1684 def __init__(self, sizes, **kwargs):
1685 """
1686 *sizes*
1687 Gives the area of the circle in points^2
1689 %(Collection)s
1690 """
1691 Collection.__init__(self, **kwargs)
1692 self.set_sizes(sizes)
1693 self.set_transform(transforms.IdentityTransform())
1694 self._paths = [mpath.Path.unit_circle()]
1697class EllipseCollection(Collection):
1698 """A collection of ellipses, drawn using splines."""
1700 @docstring.dedent_interpd
1701 def __init__(self, widths, heights, angles, units='points', **kwargs):
1702 """
1703 Parameters
1704 ----------
1705 widths : array-like
1706 The lengths of the first axes (e.g., major axis lengths).
1708 heights : array-like
1709 The lengths of second axes.
1711 angles : array-like
1712 The angles of the first axes, degrees CCW from the x-axis.
1714 units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'}
1716 The units in which majors and minors are given; 'width' and
1717 'height' refer to the dimensions of the axes, while 'x'
1718 and 'y' refer to the *offsets* data units. 'xy' differs
1719 from all others in that the angle as plotted varies with
1720 the aspect ratio, and equals the specified angle only when
1721 the aspect ratio is unity. Hence it behaves the same as
1722 the :class:`~matplotlib.patches.Ellipse` with
1723 ``axes.transData`` as its transform.
1725 Other Parameters
1726 ----------------
1727 **kwargs
1728 Additional kwargs inherited from the base :class:`Collection`.
1730 %(Collection)s
1731 """
1732 Collection.__init__(self, **kwargs)
1733 self._widths = 0.5 * np.asarray(widths).ravel()
1734 self._heights = 0.5 * np.asarray(heights).ravel()
1735 self._angles = np.deg2rad(angles).ravel()
1736 self._units = units
1737 self.set_transform(transforms.IdentityTransform())
1738 self._transforms = np.empty((0, 3, 3))
1739 self._paths = [mpath.Path.unit_circle()]
1741 def _set_transforms(self):
1742 """Calculate transforms immediately before drawing."""
1744 ax = self.axes
1745 fig = self.figure
1747 if self._units == 'xy':
1748 sc = 1
1749 elif self._units == 'x':
1750 sc = ax.bbox.width / ax.viewLim.width
1751 elif self._units == 'y':
1752 sc = ax.bbox.height / ax.viewLim.height
1753 elif self._units == 'inches':
1754 sc = fig.dpi
1755 elif self._units == 'points':
1756 sc = fig.dpi / 72.0
1757 elif self._units == 'width':
1758 sc = ax.bbox.width
1759 elif self._units == 'height':
1760 sc = ax.bbox.height
1761 elif self._units == 'dots':
1762 sc = 1.0
1763 else:
1764 raise ValueError('unrecognized units: %s' % self._units)
1766 self._transforms = np.zeros((len(self._widths), 3, 3))
1767 widths = self._widths * sc
1768 heights = self._heights * sc
1769 sin_angle = np.sin(self._angles)
1770 cos_angle = np.cos(self._angles)
1771 self._transforms[:, 0, 0] = widths * cos_angle
1772 self._transforms[:, 0, 1] = heights * -sin_angle
1773 self._transforms[:, 1, 0] = widths * sin_angle
1774 self._transforms[:, 1, 1] = heights * cos_angle
1775 self._transforms[:, 2, 2] = 1.0
1777 _affine = transforms.Affine2D
1778 if self._units == 'xy':
1779 m = ax.transData.get_affine().get_matrix().copy()
1780 m[:2, 2:] = 0
1781 self.set_transform(_affine(m))
1783 @artist.allow_rasterization
1784 def draw(self, renderer):
1785 self._set_transforms()
1786 Collection.draw(self, renderer)
1789class PatchCollection(Collection):
1790 """
1791 A generic collection of patches.
1793 This makes it easier to assign a color map to a heterogeneous
1794 collection of patches.
1796 This also may improve plotting speed, since PatchCollection will
1797 draw faster than a large number of patches.
1798 """
1800 def __init__(self, patches, match_original=False, **kwargs):
1801 """
1802 *patches*
1803 a sequence of Patch objects. This list may include
1804 a heterogeneous assortment of different patch types.
1806 *match_original*
1807 If True, use the colors and linewidths of the original
1808 patches. If False, new colors may be assigned by
1809 providing the standard collection arguments, facecolor,
1810 edgecolor, linewidths, norm or cmap.
1812 If any of *edgecolors*, *facecolors*, *linewidths*,
1813 *antialiaseds* are None, they default to their
1814 :data:`matplotlib.rcParams` patch setting, in sequence form.
1816 The use of :class:`~matplotlib.cm.ScalarMappable` is optional.
1817 If the :class:`~matplotlib.cm.ScalarMappable` matrix _A is not
1818 None (i.e., a call to set_array has been made), at draw time a
1819 call to scalar mappable will be made to set the face colors.
1820 """
1822 if match_original:
1823 def determine_facecolor(patch):
1824 if patch.get_fill():
1825 return patch.get_facecolor()
1826 return [0, 0, 0, 0]
1828 kwargs['facecolors'] = [determine_facecolor(p) for p in patches]
1829 kwargs['edgecolors'] = [p.get_edgecolor() for p in patches]
1830 kwargs['linewidths'] = [p.get_linewidth() for p in patches]
1831 kwargs['linestyles'] = [p.get_linestyle() for p in patches]
1832 kwargs['antialiaseds'] = [p.get_antialiased() for p in patches]
1834 Collection.__init__(self, **kwargs)
1836 self.set_paths(patches)
1838 def set_paths(self, patches):
1839 paths = [p.get_transform().transform_path(p.get_path())
1840 for p in patches]
1841 self._paths = paths
1844class TriMesh(Collection):
1845 """
1846 Class for the efficient drawing of a triangular mesh using Gouraud shading.
1848 A triangular mesh is a `~matplotlib.tri.Triangulation` object.
1849 """
1850 def __init__(self, triangulation, **kwargs):
1851 Collection.__init__(self, **kwargs)
1852 self._triangulation = triangulation
1853 self._shading = 'gouraud'
1854 self._is_filled = True
1856 self._bbox = transforms.Bbox.unit()
1858 # Unfortunately this requires a copy, unless Triangulation
1859 # was rewritten.
1860 xy = np.hstack((triangulation.x.reshape(-1, 1),
1861 triangulation.y.reshape(-1, 1)))
1862 self._bbox.update_from_data_xy(xy)
1864 def get_paths(self):
1865 if self._paths is None:
1866 self.set_paths()
1867 return self._paths
1869 def set_paths(self):
1870 self._paths = self.convert_mesh_to_paths(self._triangulation)
1872 @staticmethod
1873 def convert_mesh_to_paths(tri):
1874 """
1875 Converts a given mesh into a sequence of `~.Path` objects.
1877 This function is primarily of use to implementers of backends that do
1878 not directly support meshes.
1879 """
1880 triangles = tri.get_masked_triangles()
1881 verts = np.stack((tri.x[triangles], tri.y[triangles]), axis=-1)
1882 return [mpath.Path(x) for x in verts]
1884 @artist.allow_rasterization
1885 def draw(self, renderer):
1886 if not self.get_visible():
1887 return
1888 renderer.open_group(self.__class__.__name__, gid=self.get_gid())
1889 transform = self.get_transform()
1891 # Get a list of triangles and the color at each vertex.
1892 tri = self._triangulation
1893 triangles = tri.get_masked_triangles()
1895 verts = np.stack((tri.x[triangles], tri.y[triangles]), axis=-1)
1897 self.update_scalarmappable()
1898 colors = self._facecolors[triangles]
1900 gc = renderer.new_gc()
1901 self._set_gc_clip(gc)
1902 gc.set_linewidth(self.get_linewidth()[0])
1903 renderer.draw_gouraud_triangles(gc, verts, colors, transform.frozen())
1904 gc.restore()
1905 renderer.close_group(self.__class__.__name__)
1908class QuadMesh(Collection):
1909 """
1910 Class for the efficient drawing of a quadrilateral mesh.
1912 A quadrilateral mesh consists of a grid of vertices. The
1913 dimensions of this array are (*meshWidth* + 1, *meshHeight* +
1914 1). Each vertex in the mesh has a different set of "mesh
1915 coordinates" representing its position in the topology of the
1916 mesh. For any values (*m*, *n*) such that 0 <= *m* <= *meshWidth*
1917 and 0 <= *n* <= *meshHeight*, the vertices at mesh coordinates
1918 (*m*, *n*), (*m*, *n* + 1), (*m* + 1, *n* + 1), and (*m* + 1, *n*)
1919 form one of the quadrilaterals in the mesh. There are thus
1920 (*meshWidth* * *meshHeight*) quadrilaterals in the mesh. The mesh
1921 need not be regular and the polygons need not be convex.
1923 A quadrilateral mesh is represented by a (2 x ((*meshWidth* + 1) *
1924 (*meshHeight* + 1))) numpy array *coordinates*, where each row is
1925 the *x* and *y* coordinates of one of the vertices. To define the
1926 function that maps from a data point to its corresponding color,
1927 use the :meth:`set_cmap` method. Each of these arrays is indexed in
1928 row-major order by the mesh coordinates of the vertex (or the mesh
1929 coordinates of the lower left vertex, in the case of the
1930 colors).
1932 For example, the first entry in *coordinates* is the
1933 coordinates of the vertex at mesh coordinates (0, 0), then the one
1934 at (0, 1), then at (0, 2) .. (0, meshWidth), (1, 0), (1, 1), and
1935 so on.
1937 *shading* may be 'flat', or 'gouraud'
1938 """
1939 def __init__(self, meshWidth, meshHeight, coordinates,
1940 antialiased=True, shading='flat', **kwargs):
1941 Collection.__init__(self, **kwargs)
1942 self._meshWidth = meshWidth
1943 self._meshHeight = meshHeight
1944 # By converting to floats now, we can avoid that on every draw.
1945 self._coordinates = np.asarray(coordinates, float).reshape(
1946 (meshHeight + 1, meshWidth + 1, 2))
1947 self._antialiased = antialiased
1948 self._shading = shading
1950 self._bbox = transforms.Bbox.unit()
1951 self._bbox.update_from_data_xy(coordinates.reshape(
1952 ((meshWidth + 1) * (meshHeight + 1), 2)))
1954 def get_paths(self):
1955 if self._paths is None:
1956 self.set_paths()
1957 return self._paths
1959 def set_paths(self):
1960 self._paths = self.convert_mesh_to_paths(
1961 self._meshWidth, self._meshHeight, self._coordinates)
1962 self.stale = True
1964 def get_datalim(self, transData):
1965 return (self.get_transform() - transData).transform_bbox(self._bbox)
1967 @staticmethod
1968 def convert_mesh_to_paths(meshWidth, meshHeight, coordinates):
1969 """
1970 Converts a given mesh into a sequence of `~.Path` objects.
1972 This function is primarily of use to implementers of backends that do
1973 not directly support quadmeshes.
1974 """
1975 if isinstance(coordinates, np.ma.MaskedArray):
1976 c = coordinates.data
1977 else:
1978 c = coordinates
1979 points = np.concatenate((
1980 c[:-1, :-1],
1981 c[:-1, 1:],
1982 c[1:, 1:],
1983 c[1:, :-1],
1984 c[:-1, :-1]
1985 ), axis=2)
1986 points = points.reshape((meshWidth * meshHeight, 5, 2))
1987 return [mpath.Path(x) for x in points]
1989 def convert_mesh_to_triangles(self, meshWidth, meshHeight, coordinates):
1990 """
1991 Converts a given mesh into a sequence of triangles, each point
1992 with its own color. This is useful for experiments using
1993 `draw_gouraud_triangle`.
1994 """
1995 if isinstance(coordinates, np.ma.MaskedArray):
1996 p = coordinates.data
1997 else:
1998 p = coordinates
2000 p_a = p[:-1, :-1]
2001 p_b = p[:-1, 1:]
2002 p_c = p[1:, 1:]
2003 p_d = p[1:, :-1]
2004 p_center = (p_a + p_b + p_c + p_d) / 4.0
2006 triangles = np.concatenate((
2007 p_a, p_b, p_center,
2008 p_b, p_c, p_center,
2009 p_c, p_d, p_center,
2010 p_d, p_a, p_center,
2011 ), axis=2)
2012 triangles = triangles.reshape((meshWidth * meshHeight * 4, 3, 2))
2014 c = self.get_facecolor().reshape((meshHeight + 1, meshWidth + 1, 4))
2015 c_a = c[:-1, :-1]
2016 c_b = c[:-1, 1:]
2017 c_c = c[1:, 1:]
2018 c_d = c[1:, :-1]
2019 c_center = (c_a + c_b + c_c + c_d) / 4.0
2021 colors = np.concatenate((
2022 c_a, c_b, c_center,
2023 c_b, c_c, c_center,
2024 c_c, c_d, c_center,
2025 c_d, c_a, c_center,
2026 ), axis=2)
2027 colors = colors.reshape((meshWidth * meshHeight * 4, 3, 4))
2029 return triangles, colors
2031 @artist.allow_rasterization
2032 def draw(self, renderer):
2033 if not self.get_visible():
2034 return
2035 renderer.open_group(self.__class__.__name__, self.get_gid())
2036 transform = self.get_transform()
2037 transOffset = self.get_offset_transform()
2038 offsets = self._offsets
2040 if self.have_units():
2041 if len(self._offsets):
2042 xs = self.convert_xunits(self._offsets[:, 0])
2043 ys = self.convert_yunits(self._offsets[:, 1])
2044 offsets = np.column_stack([xs, ys])
2046 self.update_scalarmappable()
2048 if not transform.is_affine:
2049 coordinates = self._coordinates.reshape((-1, 2))
2050 coordinates = transform.transform(coordinates)
2051 coordinates = coordinates.reshape(self._coordinates.shape)
2052 transform = transforms.IdentityTransform()
2053 else:
2054 coordinates = self._coordinates
2056 if not transOffset.is_affine:
2057 offsets = transOffset.transform_non_affine(offsets)
2058 transOffset = transOffset.get_affine()
2060 gc = renderer.new_gc()
2061 self._set_gc_clip(gc)
2062 gc.set_linewidth(self.get_linewidth()[0])
2064 if self._shading == 'gouraud':
2065 triangles, colors = self.convert_mesh_to_triangles(
2066 self._meshWidth, self._meshHeight, coordinates)
2067 renderer.draw_gouraud_triangles(
2068 gc, triangles, colors, transform.frozen())
2069 else:
2070 renderer.draw_quad_mesh(
2071 gc, transform.frozen(), self._meshWidth, self._meshHeight,
2072 coordinates, offsets, transOffset, self.get_facecolor(),
2073 self._antialiased, self.get_edgecolors())
2074 gc.restore()
2075 renderer.close_group(self.__class__.__name__)
2076 self.stale = False
2079patchstr = artist.kwdoc(Collection)
2080for k in ('QuadMesh', 'TriMesh', 'PolyCollection', 'BrokenBarHCollection',
2081 'RegularPolyCollection', 'PathCollection',
2082 'StarPolygonCollection', 'PatchCollection',
2083 'CircleCollection', 'Collection',):
2084 docstring.interpd.update({k: patchstr})
2085docstring.interpd.update(LineCollection=artist.kwdoc(LineCollection))