Coverage for /usr/lib/python3/dist-packages/matplotlib/collections.py: 44%
859 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
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 inspect
13import math
14from numbers import Number
15import warnings
17import numpy as np
19import matplotlib as mpl
20from . import (_api, _path, artist, cbook, cm, colors as mcolors, _docstring,
21 hatch as mhatch, lines as mlines, path as mpath, transforms)
22from ._enums import JoinStyle, CapStyle
25# "color" is excluded; it is a compound setter, and its docstring differs
26# in LineCollection.
27@_api.define_aliases({
28 "antialiased": ["antialiaseds", "aa"],
29 "edgecolor": ["edgecolors", "ec"],
30 "facecolor": ["facecolors", "fc"],
31 "linestyle": ["linestyles", "dashes", "ls"],
32 "linewidth": ["linewidths", "lw"],
33 "offset_transform": ["transOffset"],
34})
35class Collection(artist.Artist, cm.ScalarMappable):
36 r"""
37 Base class for Collections. Must be subclassed to be usable.
39 A Collection represents a sequence of `.Patch`\es that can be drawn
40 more efficiently together than individually. For example, when a single
41 path is being drawn repeatedly at different offsets, the renderer can
42 typically execute a ``draw_marker()`` call much more efficiently than a
43 series of repeated calls to ``draw_path()`` with the offsets put in
44 one-by-one.
46 Most properties of a collection can be configured per-element. Therefore,
47 Collections have "plural" versions of many of the properties of a `.Patch`
48 (e.g. `.Collection.get_paths` instead of `.Patch.get_path`). Exceptions are
49 the *zorder*, *hatch*, *pickradius*, *capstyle* and *joinstyle* properties,
50 which can only be set globally for the whole collection.
52 Besides these exceptions, all properties can be specified as single values
53 (applying to all elements) or sequences of values. The property of the
54 ``i``\th element of the collection is::
56 prop[i % len(prop)]
58 Each Collection can optionally be used as its own `.ScalarMappable` by
59 passing the *norm* and *cmap* parameters to its constructor. If the
60 Collection's `.ScalarMappable` matrix ``_A`` has been set (via a call
61 to `.Collection.set_array`), then at draw time this internal scalar
62 mappable will be used to set the ``facecolors`` and ``edgecolors``,
63 ignoring those that were manually passed in.
64 """
65 #: Either a list of 3x3 arrays or an Nx3x3 array (representing N
66 #: transforms), suitable for the `all_transforms` argument to
67 #: `~matplotlib.backend_bases.RendererBase.draw_path_collection`;
68 #: each 3x3 array is used to initialize an
69 #: `~matplotlib.transforms.Affine2D` object.
70 #: Each kind of collection defines this based on its arguments.
71 _transforms = np.empty((0, 3, 3))
73 # Whether to draw an edge by default. Set on a
74 # subclass-by-subclass basis.
75 _edge_default = False
77 @_docstring.interpd
78 @_api.make_keyword_only("3.6", name="edgecolors")
79 def __init__(self,
80 edgecolors=None,
81 facecolors=None,
82 linewidths=None,
83 linestyles='solid',
84 capstyle=None,
85 joinstyle=None,
86 antialiaseds=None,
87 offsets=None,
88 offset_transform=None,
89 norm=None, # optional for ScalarMappable
90 cmap=None, # ditto
91 pickradius=5.0,
92 hatch=None,
93 urls=None,
94 *,
95 zorder=1,
96 **kwargs
97 ):
98 """
99 Parameters
100 ----------
101 edgecolors : color or list of colors, default: :rc:`patch.edgecolor`
102 Edge color for each patch making up the collection. The special
103 value 'face' can be passed to make the edgecolor match the
104 facecolor.
105 facecolors : color or list of colors, default: :rc:`patch.facecolor`
106 Face color for each patch making up the collection.
107 linewidths : float or list of floats, default: :rc:`patch.linewidth`
108 Line width for each patch making up the collection.
109 linestyles : str or tuple or list thereof, default: 'solid'
110 Valid strings are ['solid', 'dashed', 'dashdot', 'dotted', '-',
111 '--', '-.', ':']. Dash tuples should be of the form::
113 (offset, onoffseq),
115 where *onoffseq* is an even length tuple of on and off ink lengths
116 in points. For examples, see
117 :doc:`/gallery/lines_bars_and_markers/linestyles`.
118 capstyle : `.CapStyle`-like, default: :rc:`patch.capstyle`
119 Style to use for capping lines for all paths in the collection.
120 Allowed values are %(CapStyle)s.
121 joinstyle : `.JoinStyle`-like, default: :rc:`patch.joinstyle`
122 Style to use for joining lines for all paths in the collection.
123 Allowed values are %(JoinStyle)s.
124 antialiaseds : bool or list of bool, default: :rc:`patch.antialiased`
125 Whether each patch in the collection should be drawn with
126 antialiasing.
127 offsets : (float, float) or list thereof, default: (0, 0)
128 A vector by which to translate each patch after rendering (default
129 is no translation). The translation is performed in screen (pixel)
130 coordinates (i.e. after the Artist's transform is applied).
131 offset_transform : `~.Transform`, default: `.IdentityTransform`
132 A single transform which will be applied to each *offsets* vector
133 before it is used.
134 cmap, norm
135 Data normalization and colormapping parameters. See
136 `.ScalarMappable` for a detailed description.
137 hatch : str, optional
138 Hatching pattern to use in filled paths, if any. Valid strings are
139 ['/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*']. See
140 :doc:`/gallery/shapes_and_collections/hatch_style_reference` for
141 the meaning of each hatch type.
142 pickradius : float, default: 5.0
143 If ``pickradius <= 0``, then `.Collection.contains` will return
144 ``True`` whenever the test point is inside of one of the polygons
145 formed by the control points of a Path in the Collection. On the
146 other hand, if it is greater than 0, then we instead check if the
147 test point is contained in a stroke of width ``2*pickradius``
148 following any of the Paths in the Collection.
149 urls : list of str, default: None
150 A URL for each patch to link to once drawn. Currently only works
151 for the SVG backend. See :doc:`/gallery/misc/hyperlinks_sgskip` for
152 examples.
153 zorder : float, default: 1
154 The drawing order, shared by all Patches in the Collection. See
155 :doc:`/gallery/misc/zorder_demo` for all defaults and examples.
156 """
157 artist.Artist.__init__(self)
158 cm.ScalarMappable.__init__(self, norm, cmap)
159 # list of un-scaled dash patterns
160 # this is needed scaling the dash pattern by linewidth
161 self._us_linestyles = [(0, None)]
162 # list of dash patterns
163 self._linestyles = [(0, None)]
164 # list of unbroadcast/scaled linewidths
165 self._us_lw = [0]
166 self._linewidths = [0]
167 # Flags set by _set_mappable_flags: are colors from mapping an array?
168 self._face_is_mapped = None
169 self._edge_is_mapped = None
170 self._mapped_colors = None # calculated in update_scalarmappable
171 self._hatch_color = mcolors.to_rgba(mpl.rcParams['hatch.color'])
172 self.set_facecolor(facecolors)
173 self.set_edgecolor(edgecolors)
174 self.set_linewidth(linewidths)
175 self.set_linestyle(linestyles)
176 self.set_antialiased(antialiaseds)
177 self.set_pickradius(pickradius)
178 self.set_urls(urls)
179 self.set_hatch(hatch)
180 self.set_zorder(zorder)
182 if capstyle:
183 self.set_capstyle(capstyle)
184 else:
185 self._capstyle = None
187 if joinstyle:
188 self.set_joinstyle(joinstyle)
189 else:
190 self._joinstyle = None
192 if offsets is not None:
193 offsets = np.asanyarray(offsets, float)
194 # Broadcast (2,) -> (1, 2) but nothing else.
195 if offsets.shape == (2,):
196 offsets = offsets[None, :]
198 self._offsets = offsets
199 self._offset_transform = offset_transform
201 self._path_effects = None
202 self._internal_update(kwargs)
203 self._paths = None
205 def get_paths(self):
206 return self._paths
208 def set_paths(self, paths):
209 raise NotImplementedError
211 def get_transforms(self):
212 return self._transforms
214 def get_offset_transform(self):
215 """Return the `.Transform` instance used by this artist offset."""
216 if self._offset_transform is None:
217 self._offset_transform = transforms.IdentityTransform()
218 elif (not isinstance(self._offset_transform, transforms.Transform)
219 and hasattr(self._offset_transform, '_as_mpl_transform')):
220 self._offset_transform = \
221 self._offset_transform._as_mpl_transform(self.axes)
222 return self._offset_transform
224 @_api.rename_parameter("3.6", "transOffset", "offset_transform")
225 def set_offset_transform(self, offset_transform):
226 """
227 Set the artist offset transform.
229 Parameters
230 ----------
231 offset_transform : `.Transform`
232 """
233 self._offset_transform = offset_transform
235 def get_datalim(self, transData):
236 # Calculate the data limits and return them as a `.Bbox`.
237 #
238 # This operation depends on the transforms for the data in the
239 # collection and whether the collection has offsets:
240 #
241 # 1. offsets = None, transform child of transData: use the paths for
242 # the automatic limits (i.e. for LineCollection in streamline).
243 # 2. offsets != None: offset_transform is child of transData:
244 #
245 # a. transform is child of transData: use the path + offset for
246 # limits (i.e for bar).
247 # b. transform is not a child of transData: just use the offsets
248 # for the limits (i.e. for scatter)
249 #
250 # 3. otherwise return a null Bbox.
252 transform = self.get_transform()
253 offset_trf = self.get_offset_transform()
254 if not (isinstance(offset_trf, transforms.IdentityTransform)
255 or offset_trf.contains_branch(transData)):
256 # if the offsets are in some coords other than data,
257 # then don't use them for autoscaling.
258 return transforms.Bbox.null()
259 offsets = self.get_offsets()
261 paths = self.get_paths()
262 if not len(paths):
263 # No paths to transform
264 return transforms.Bbox.null()
266 if not transform.is_affine:
267 paths = [transform.transform_path_non_affine(p) for p in paths]
268 # Don't convert transform to transform.get_affine() here because
269 # we may have transform.contains_branch(transData) but not
270 # transforms.get_affine().contains_branch(transData). But later,
271 # be careful to only apply the affine part that remains.
273 if any(transform.contains_branch_seperately(transData)):
274 # collections that are just in data units (like quiver)
275 # can properly have the axes limits set by their shape +
276 # offset. LineCollections that have no offsets can
277 # also use this algorithm (like streamplot).
278 if isinstance(offsets, np.ma.MaskedArray):
279 offsets = offsets.filled(np.nan)
280 # get_path_collection_extents handles nan but not masked arrays
281 return mpath.get_path_collection_extents(
282 transform.get_affine() - transData, paths,
283 self.get_transforms(),
284 offset_trf.transform_non_affine(offsets),
285 offset_trf.get_affine().frozen())
287 # NOTE: None is the default case where no offsets were passed in
288 if self._offsets is not None:
289 # this is for collections that have their paths (shapes)
290 # in physical, axes-relative, or figure-relative units
291 # (i.e. like scatter). We can't uniquely set limits based on
292 # those shapes, so we just set the limits based on their
293 # location.
294 offsets = (offset_trf - transData).transform(offsets)
295 # note A-B means A B^{-1}
296 offsets = np.ma.masked_invalid(offsets)
297 if not offsets.mask.all():
298 bbox = transforms.Bbox.null()
299 bbox.update_from_data_xy(offsets)
300 return bbox
301 return transforms.Bbox.null()
303 def get_window_extent(self, renderer=None):
304 # TODO: check to ensure that this does not fail for
305 # cases other than scatter plot legend
306 return self.get_datalim(transforms.IdentityTransform())
308 def _prepare_points(self):
309 # Helper for drawing and hit testing.
311 transform = self.get_transform()
312 offset_trf = self.get_offset_transform()
313 offsets = self.get_offsets()
314 paths = self.get_paths()
316 if self.have_units():
317 paths = []
318 for path in self.get_paths():
319 vertices = path.vertices
320 xs, ys = vertices[:, 0], vertices[:, 1]
321 xs = self.convert_xunits(xs)
322 ys = self.convert_yunits(ys)
323 paths.append(mpath.Path(np.column_stack([xs, ys]), path.codes))
324 xs = self.convert_xunits(offsets[:, 0])
325 ys = self.convert_yunits(offsets[:, 1])
326 offsets = np.ma.column_stack([xs, ys])
328 if not transform.is_affine:
329 paths = [transform.transform_path_non_affine(path)
330 for path in paths]
331 transform = transform.get_affine()
332 if not offset_trf.is_affine:
333 offsets = offset_trf.transform_non_affine(offsets)
334 # This might have changed an ndarray into a masked array.
335 offset_trf = offset_trf.get_affine()
337 if isinstance(offsets, np.ma.MaskedArray):
338 offsets = offsets.filled(np.nan)
339 # Changing from a masked array to nan-filled ndarray
340 # is probably most efficient at this point.
342 return transform, offset_trf, offsets, paths
344 @artist.allow_rasterization
345 def draw(self, renderer):
346 if not self.get_visible():
347 return
348 renderer.open_group(self.__class__.__name__, self.get_gid())
350 self.update_scalarmappable()
352 transform, offset_trf, offsets, paths = self._prepare_points()
354 gc = renderer.new_gc()
355 self._set_gc_clip(gc)
356 gc.set_snap(self.get_snap())
358 if self._hatch:
359 gc.set_hatch(self._hatch)
360 gc.set_hatch_color(self._hatch_color)
362 if self.get_sketch_params() is not None:
363 gc.set_sketch_params(*self.get_sketch_params())
365 if self.get_path_effects():
366 from matplotlib.patheffects import PathEffectRenderer
367 renderer = PathEffectRenderer(self.get_path_effects(), renderer)
369 # If the collection is made up of a single shape/color/stroke,
370 # it can be rendered once and blitted multiple times, using
371 # `draw_markers` rather than `draw_path_collection`. This is
372 # *much* faster for Agg, and results in smaller file sizes in
373 # PDF/SVG/PS.
375 trans = self.get_transforms()
376 facecolors = self.get_facecolor()
377 edgecolors = self.get_edgecolor()
378 do_single_path_optimization = False
379 if (len(paths) == 1 and len(trans) <= 1 and
380 len(facecolors) == 1 and len(edgecolors) == 1 and
381 len(self._linewidths) == 1 and
382 all(ls[1] is None for ls in self._linestyles) and
383 len(self._antialiaseds) == 1 and len(self._urls) == 1 and
384 self.get_hatch() is None):
385 if len(trans):
386 combined_transform = transforms.Affine2D(trans[0]) + transform
387 else:
388 combined_transform = transform
389 extents = paths[0].get_extents(combined_transform)
390 if (extents.width < self.figure.bbox.width
391 and extents.height < self.figure.bbox.height):
392 do_single_path_optimization = True
394 if self._joinstyle:
395 gc.set_joinstyle(self._joinstyle)
397 if self._capstyle:
398 gc.set_capstyle(self._capstyle)
400 if do_single_path_optimization:
401 gc.set_foreground(tuple(edgecolors[0]))
402 gc.set_linewidth(self._linewidths[0])
403 gc.set_dashes(*self._linestyles[0])
404 gc.set_antialiased(self._antialiaseds[0])
405 gc.set_url(self._urls[0])
406 renderer.draw_markers(
407 gc, paths[0], combined_transform.frozen(),
408 mpath.Path(offsets), offset_trf, tuple(facecolors[0]))
409 else:
410 renderer.draw_path_collection(
411 gc, transform.frozen(), paths,
412 self.get_transforms(), offsets, offset_trf,
413 self.get_facecolor(), self.get_edgecolor(),
414 self._linewidths, self._linestyles,
415 self._antialiaseds, self._urls,
416 "screen") # offset_position, kept for backcompat.
418 gc.restore()
419 renderer.close_group(self.__class__.__name__)
420 self.stale = False
422 @_api.rename_parameter("3.6", "pr", "pickradius")
423 def set_pickradius(self, pickradius):
424 """
425 Set the pick radius used for containment tests.
427 Parameters
428 ----------
429 pickradius : float
430 Pick radius, in points.
431 """
432 self._pickradius = pickradius
434 def get_pickradius(self):
435 return self._pickradius
437 def contains(self, mouseevent):
438 """
439 Test whether the mouse event occurred in the collection.
441 Returns ``bool, dict(ind=itemlist)``, where every item in itemlist
442 contains the event.
443 """
444 inside, info = self._default_contains(mouseevent)
445 if inside is not None:
446 return inside, info
448 if not self.get_visible():
449 return False, {}
451 pickradius = (
452 float(self._picker)
453 if isinstance(self._picker, Number) and
454 self._picker is not True # the bool, not just nonzero or 1
455 else self._pickradius)
457 if self.axes:
458 self.axes._unstale_viewLim()
460 transform, offset_trf, offsets, paths = self._prepare_points()
462 # Tests if the point is contained on one of the polygons formed
463 # by the control points of each of the paths. A point is considered
464 # "on" a path if it would lie within a stroke of width 2*pickradius
465 # following the path. If pickradius <= 0, then we instead simply check
466 # if the point is *inside* of the path instead.
467 ind = _path.point_in_path_collection(
468 mouseevent.x, mouseevent.y, pickradius,
469 transform.frozen(), paths, self.get_transforms(),
470 offsets, offset_trf, pickradius <= 0)
472 return len(ind) > 0, dict(ind=ind)
474 def set_urls(self, urls):
475 """
476 Parameters
477 ----------
478 urls : list of str or None
480 Notes
481 -----
482 URLs are currently only implemented by the SVG backend. They are
483 ignored by all other backends.
484 """
485 self._urls = urls if urls is not None else [None]
486 self.stale = True
488 def get_urls(self):
489 """
490 Return a list of URLs, one for each element of the collection.
492 The list contains *None* for elements without a URL. See
493 :doc:`/gallery/misc/hyperlinks_sgskip` for an example.
494 """
495 return self._urls
497 def set_hatch(self, hatch):
498 r"""
499 Set the hatching pattern
501 *hatch* can be one of::
503 / - diagonal hatching
504 \ - back diagonal
505 | - vertical
506 - - horizontal
507 + - crossed
508 x - crossed diagonal
509 o - small circle
510 O - large circle
511 . - dots
512 * - stars
514 Letters can be combined, in which case all the specified
515 hatchings are done. If same letter repeats, it increases the
516 density of hatching of that pattern.
518 Hatching is supported in the PostScript, PDF, SVG and Agg
519 backends only.
521 Unlike other properties such as linewidth and colors, hatching
522 can only be specified for the collection as a whole, not separately
523 for each member.
525 Parameters
526 ----------
527 hatch : {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
528 """
529 # Use validate_hatch(list) after deprecation.
530 mhatch._validate_hatch_pattern(hatch)
531 self._hatch = hatch
532 self.stale = True
534 def get_hatch(self):
535 """Return the current hatching pattern."""
536 return self._hatch
538 def set_offsets(self, offsets):
539 """
540 Set the offsets for the collection.
542 Parameters
543 ----------
544 offsets : (N, 2) or (2,) array-like
545 """
546 offsets = np.asanyarray(offsets)
547 if offsets.shape == (2,): # Broadcast (2,) -> (1, 2) but nothing else.
548 offsets = offsets[None, :]
549 self._offsets = np.column_stack(
550 (np.asarray(self.convert_xunits(offsets[:, 0]), 'float'),
551 np.asarray(self.convert_yunits(offsets[:, 1]), 'float')))
552 self.stale = True
554 def get_offsets(self):
555 """Return the offsets for the collection."""
556 # Default to zeros in the no-offset (None) case
557 return np.zeros((1, 2)) if self._offsets is None else self._offsets
559 def _get_default_linewidth(self):
560 # This may be overridden in a subclass.
561 return mpl.rcParams['patch.linewidth'] # validated as float
563 def set_linewidth(self, lw):
564 """
565 Set the linewidth(s) for the collection. *lw* can be a scalar
566 or a sequence; if it is a sequence the patches will cycle
567 through the sequence
569 Parameters
570 ----------
571 lw : float or list of floats
572 """
573 if lw is None:
574 lw = self._get_default_linewidth()
575 # get the un-scaled/broadcast lw
576 self._us_lw = np.atleast_1d(np.asarray(lw))
578 # scale all of the dash patterns.
579 self._linewidths, self._linestyles = self._bcast_lwls(
580 self._us_lw, self._us_linestyles)
581 self.stale = True
583 def set_linestyle(self, ls):
584 """
585 Set the linestyle(s) for the collection.
587 =========================== =================
588 linestyle description
589 =========================== =================
590 ``'-'`` or ``'solid'`` solid line
591 ``'--'`` or ``'dashed'`` dashed line
592 ``'-.'`` or ``'dashdot'`` dash-dotted line
593 ``':'`` or ``'dotted'`` dotted line
594 =========================== =================
596 Alternatively a dash tuple of the following form can be provided::
598 (offset, onoffseq),
600 where ``onoffseq`` is an even length tuple of on and off ink in points.
602 Parameters
603 ----------
604 ls : str or tuple or list thereof
605 Valid values for individual linestyles include {'-', '--', '-.',
606 ':', '', (offset, on-off-seq)}. See `.Line2D.set_linestyle` for a
607 complete description.
608 """
609 try:
610 if isinstance(ls, str):
611 ls = cbook.ls_mapper.get(ls, ls)
612 dashes = [mlines._get_dash_pattern(ls)]
613 else:
614 try:
615 dashes = [mlines._get_dash_pattern(ls)]
616 except ValueError:
617 dashes = [mlines._get_dash_pattern(x) for x in ls]
619 except ValueError as err:
620 raise ValueError('Do not know how to convert {!r} to '
621 'dashes'.format(ls)) from err
623 # get the list of raw 'unscaled' dash patterns
624 self._us_linestyles = dashes
626 # broadcast and scale the lw and dash patterns
627 self._linewidths, self._linestyles = self._bcast_lwls(
628 self._us_lw, self._us_linestyles)
630 @_docstring.interpd
631 def set_capstyle(self, cs):
632 """
633 Set the `.CapStyle` for the collection (for all its elements).
635 Parameters
636 ----------
637 cs : `.CapStyle` or %(CapStyle)s
638 """
639 self._capstyle = CapStyle(cs)
641 def get_capstyle(self):
642 return self._capstyle.name
644 @_docstring.interpd
645 def set_joinstyle(self, js):
646 """
647 Set the `.JoinStyle` for the collection (for all its elements).
649 Parameters
650 ----------
651 js : `.JoinStyle` or %(JoinStyle)s
652 """
653 self._joinstyle = JoinStyle(js)
655 def get_joinstyle(self):
656 return self._joinstyle.name
658 @staticmethod
659 def _bcast_lwls(linewidths, dashes):
660 """
661 Internal helper function to broadcast + scale ls/lw
663 In the collection drawing code, the linewidth and linestyle are cycled
664 through as circular buffers (via ``v[i % len(v)]``). Thus, if we are
665 going to scale the dash pattern at set time (not draw time) we need to
666 do the broadcasting now and expand both lists to be the same length.
668 Parameters
669 ----------
670 linewidths : list
671 line widths of collection
672 dashes : list
673 dash specification (offset, (dash pattern tuple))
675 Returns
676 -------
677 linewidths, dashes : list
678 Will be the same length, dashes are scaled by paired linewidth
679 """
680 if mpl.rcParams['_internal.classic_mode']:
681 return linewidths, dashes
682 # make sure they are the same length so we can zip them
683 if len(dashes) != len(linewidths):
684 l_dashes = len(dashes)
685 l_lw = len(linewidths)
686 gcd = math.gcd(l_dashes, l_lw)
687 dashes = list(dashes) * (l_lw // gcd)
688 linewidths = list(linewidths) * (l_dashes // gcd)
690 # scale the dash patterns
691 dashes = [mlines._scale_dashes(o, d, lw)
692 for (o, d), lw in zip(dashes, linewidths)]
694 return linewidths, dashes
696 def set_antialiased(self, aa):
697 """
698 Set the antialiasing state for rendering.
700 Parameters
701 ----------
702 aa : bool or list of bools
703 """
704 if aa is None:
705 aa = self._get_default_antialiased()
706 self._antialiaseds = np.atleast_1d(np.asarray(aa, bool))
707 self.stale = True
709 def _get_default_antialiased(self):
710 # This may be overridden in a subclass.
711 return mpl.rcParams['patch.antialiased']
713 def set_color(self, c):
714 """
715 Set both the edgecolor and the facecolor.
717 Parameters
718 ----------
719 c : color or list of RGBA tuples
721 See Also
722 --------
723 Collection.set_facecolor, Collection.set_edgecolor
724 For setting the edge or face color individually.
725 """
726 self.set_facecolor(c)
727 self.set_edgecolor(c)
729 def _get_default_facecolor(self):
730 # This may be overridden in a subclass.
731 return mpl.rcParams['patch.facecolor']
733 def _set_facecolor(self, c):
734 if c is None:
735 c = self._get_default_facecolor()
737 self._facecolors = mcolors.to_rgba_array(c, self._alpha)
738 self.stale = True
740 def set_facecolor(self, c):
741 """
742 Set the facecolor(s) of the collection. *c* can be a color (all patches
743 have same color), or a sequence of colors; if it is a sequence the
744 patches will cycle through the sequence.
746 If *c* is 'none', the patch will not be filled.
748 Parameters
749 ----------
750 c : color or list of colors
751 """
752 if isinstance(c, str) and c.lower() in ("none", "face"):
753 c = c.lower()
754 self._original_facecolor = c
755 self._set_facecolor(c)
757 def get_facecolor(self):
758 return self._facecolors
760 def get_edgecolor(self):
761 if cbook._str_equal(self._edgecolors, 'face'):
762 return self.get_facecolor()
763 else:
764 return self._edgecolors
766 def _get_default_edgecolor(self):
767 # This may be overridden in a subclass.
768 return mpl.rcParams['patch.edgecolor']
770 def _set_edgecolor(self, c):
771 set_hatch_color = True
772 if c is None:
773 if (mpl.rcParams['patch.force_edgecolor']
774 or self._edge_default
775 or cbook._str_equal(self._original_facecolor, 'none')):
776 c = self._get_default_edgecolor()
777 else:
778 c = 'none'
779 set_hatch_color = False
780 if cbook._str_lower_equal(c, 'face'):
781 self._edgecolors = 'face'
782 self.stale = True
783 return
784 self._edgecolors = mcolors.to_rgba_array(c, self._alpha)
785 if set_hatch_color and len(self._edgecolors):
786 self._hatch_color = tuple(self._edgecolors[0])
787 self.stale = True
789 def set_edgecolor(self, c):
790 """
791 Set the edgecolor(s) of the collection.
793 Parameters
794 ----------
795 c : color or list of colors or 'face'
796 The collection edgecolor(s). If a sequence, the patches cycle
797 through it. If 'face', match the facecolor.
798 """
799 # We pass through a default value for use in LineCollection.
800 # This allows us to maintain None as the default indicator in
801 # _original_edgecolor.
802 if isinstance(c, str) and c.lower() in ("none", "face"):
803 c = c.lower()
804 self._original_edgecolor = c
805 self._set_edgecolor(c)
807 def set_alpha(self, alpha):
808 """
809 Set the transparency of the collection.
811 Parameters
812 ----------
813 alpha : float or array of float or None
814 If not None, *alpha* values must be between 0 and 1, inclusive.
815 If an array is provided, its length must match the number of
816 elements in the collection. Masked values and nans are not
817 supported.
818 """
819 artist.Artist._set_alpha_for_array(self, alpha)
820 self._set_facecolor(self._original_facecolor)
821 self._set_edgecolor(self._original_edgecolor)
823 set_alpha.__doc__ = artist.Artist._set_alpha_for_array.__doc__
825 def get_linewidth(self):
826 return self._linewidths
828 def get_linestyle(self):
829 return self._linestyles
831 def _set_mappable_flags(self):
832 """
833 Determine whether edges and/or faces are color-mapped.
835 This is a helper for update_scalarmappable.
836 It sets Boolean flags '_edge_is_mapped' and '_face_is_mapped'.
838 Returns
839 -------
840 mapping_change : bool
841 True if either flag is True, or if a flag has changed.
842 """
843 # The flags are initialized to None to ensure this returns True
844 # the first time it is called.
845 edge0 = self._edge_is_mapped
846 face0 = self._face_is_mapped
847 # After returning, the flags must be Booleans, not None.
848 self._edge_is_mapped = False
849 self._face_is_mapped = False
850 if self._A is not None:
851 if not cbook._str_equal(self._original_facecolor, 'none'):
852 self._face_is_mapped = True
853 if cbook._str_equal(self._original_edgecolor, 'face'):
854 self._edge_is_mapped = True
855 else:
856 if self._original_edgecolor is None:
857 self._edge_is_mapped = True
859 mapped = self._face_is_mapped or self._edge_is_mapped
860 changed = (edge0 is None or face0 is None
861 or self._edge_is_mapped != edge0
862 or self._face_is_mapped != face0)
863 return mapped or changed
865 def update_scalarmappable(self):
866 """
867 Update colors from the scalar mappable array, if any.
869 Assign colors to edges and faces based on the array and/or
870 colors that were directly set, as appropriate.
871 """
872 if not self._set_mappable_flags():
873 return
874 # Allow possibility to call 'self.set_array(None)'.
875 if self._A is not None:
876 # QuadMesh can map 2d arrays (but pcolormesh supplies 1d array)
877 if self._A.ndim > 1 and not isinstance(self, QuadMesh):
878 raise ValueError('Collections can only map rank 1 arrays')
879 if np.iterable(self._alpha):
880 if self._alpha.size != self._A.size:
881 raise ValueError(
882 f'Data array shape, {self._A.shape} '
883 'is incompatible with alpha array shape, '
884 f'{self._alpha.shape}. '
885 'This can occur with the deprecated '
886 'behavior of the "flat" shading option, '
887 'in which a row and/or column of the data '
888 'array is dropped.')
889 # pcolormesh, scatter, maybe others flatten their _A
890 self._alpha = self._alpha.reshape(self._A.shape)
891 self._mapped_colors = self.to_rgba(self._A, self._alpha)
893 if self._face_is_mapped:
894 self._facecolors = self._mapped_colors
895 else:
896 self._set_facecolor(self._original_facecolor)
897 if self._edge_is_mapped:
898 self._edgecolors = self._mapped_colors
899 else:
900 self._set_edgecolor(self._original_edgecolor)
901 self.stale = True
903 def get_fill(self):
904 """Return whether face is colored."""
905 return not cbook._str_lower_equal(self._original_facecolor, "none")
907 def update_from(self, other):
908 """Copy properties from other to self."""
910 artist.Artist.update_from(self, other)
911 self._antialiaseds = other._antialiaseds
912 self._mapped_colors = other._mapped_colors
913 self._edge_is_mapped = other._edge_is_mapped
914 self._original_edgecolor = other._original_edgecolor
915 self._edgecolors = other._edgecolors
916 self._face_is_mapped = other._face_is_mapped
917 self._original_facecolor = other._original_facecolor
918 self._facecolors = other._facecolors
919 self._linewidths = other._linewidths
920 self._linestyles = other._linestyles
921 self._us_linestyles = other._us_linestyles
922 self._pickradius = other._pickradius
923 self._hatch = other._hatch
925 # update_from for scalarmappable
926 self._A = other._A
927 self.norm = other.norm
928 self.cmap = other.cmap
929 self.stale = True
932class _CollectionWithSizes(Collection):
933 """
934 Base class for collections that have an array of sizes.
935 """
936 _factor = 1.0
938 def get_sizes(self):
939 """
940 Return the sizes ('areas') of the elements in the collection.
942 Returns
943 -------
944 array
945 The 'area' of each element.
946 """
947 return self._sizes
949 def set_sizes(self, sizes, dpi=72.0):
950 """
951 Set the sizes of each member of the collection.
953 Parameters
954 ----------
955 sizes : ndarray or None
956 The size to set for each element of the collection. The
957 value is the 'area' of the element.
958 dpi : float, default: 72
959 The dpi of the canvas.
960 """
961 if sizes is None:
962 self._sizes = np.array([])
963 self._transforms = np.empty((0, 3, 3))
964 else:
965 self._sizes = np.asarray(sizes)
966 self._transforms = np.zeros((len(self._sizes), 3, 3))
967 scale = np.sqrt(self._sizes) * dpi / 72.0 * self._factor
968 self._transforms[:, 0, 0] = scale
969 self._transforms[:, 1, 1] = scale
970 self._transforms[:, 2, 2] = 1.0
971 self.stale = True
973 @artist.allow_rasterization
974 def draw(self, renderer):
975 self.set_sizes(self._sizes, self.figure.dpi)
976 super().draw(renderer)
979class PathCollection(_CollectionWithSizes):
980 r"""
981 A collection of `~.path.Path`\s, as created by e.g. `~.Axes.scatter`.
982 """
984 def __init__(self, paths, sizes=None, **kwargs):
985 """
986 Parameters
987 ----------
988 paths : list of `.path.Path`
989 The paths that will make up the `.Collection`.
990 sizes : array-like
991 The factor by which to scale each drawn `~.path.Path`. One unit
992 squared in the Path's data space is scaled to be ``sizes**2``
993 points when rendered.
994 **kwargs
995 Forwarded to `.Collection`.
996 """
998 super().__init__(**kwargs)
999 self.set_paths(paths)
1000 self.set_sizes(sizes)
1001 self.stale = True
1003 def set_paths(self, paths):
1004 self._paths = paths
1005 self.stale = True
1007 def get_paths(self):
1008 return self._paths
1010 def legend_elements(self, prop="colors", num="auto",
1011 fmt=None, func=lambda x: x, **kwargs):
1012 """
1013 Create legend handles and labels for a PathCollection.
1015 Each legend handle is a `.Line2D` representing the Path that was drawn,
1016 and each label is a string what each Path represents.
1018 This is useful for obtaining a legend for a `~.Axes.scatter` plot;
1019 e.g.::
1021 scatter = plt.scatter([1, 2, 3], [4, 5, 6], c=[7, 2, 3])
1022 plt.legend(*scatter.legend_elements())
1024 creates three legend elements, one for each color with the numerical
1025 values passed to *c* as the labels.
1027 Also see the :ref:`automatedlegendcreation` example.
1029 Parameters
1030 ----------
1031 prop : {"colors", "sizes"}, default: "colors"
1032 If "colors", the legend handles will show the different colors of
1033 the collection. If "sizes", the legend will show the different
1034 sizes. To set both, use *kwargs* to directly edit the `.Line2D`
1035 properties.
1036 num : int, None, "auto" (default), array-like, or `~.ticker.Locator`
1037 Target number of elements to create.
1038 If None, use all unique elements of the mappable array. If an
1039 integer, target to use *num* elements in the normed range.
1040 If *"auto"*, try to determine which option better suits the nature
1041 of the data.
1042 The number of created elements may slightly deviate from *num* due
1043 to a `~.ticker.Locator` being used to find useful locations.
1044 If a list or array, use exactly those elements for the legend.
1045 Finally, a `~.ticker.Locator` can be provided.
1046 fmt : str, `~matplotlib.ticker.Formatter`, or None (default)
1047 The format or formatter to use for the labels. If a string must be
1048 a valid input for a `.StrMethodFormatter`. If None (the default),
1049 use a `.ScalarFormatter`.
1050 func : function, default: ``lambda x: x``
1051 Function to calculate the labels. Often the size (or color)
1052 argument to `~.Axes.scatter` will have been pre-processed by the
1053 user using a function ``s = f(x)`` to make the markers visible;
1054 e.g. ``size = np.log10(x)``. Providing the inverse of this
1055 function here allows that pre-processing to be inverted, so that
1056 the legend labels have the correct values; e.g. ``func = lambda
1057 x: 10**x``.
1058 **kwargs
1059 Allowed keyword arguments are *color* and *size*. E.g. it may be
1060 useful to set the color of the markers if *prop="sizes"* is used;
1061 similarly to set the size of the markers if *prop="colors"* is
1062 used. Any further parameters are passed onto the `.Line2D`
1063 instance. This may be useful to e.g. specify a different
1064 *markeredgecolor* or *alpha* for the legend handles.
1066 Returns
1067 -------
1068 handles : list of `.Line2D`
1069 Visual representation of each element of the legend.
1070 labels : list of str
1071 The string labels for elements of the legend.
1072 """
1073 handles = []
1074 labels = []
1075 hasarray = self.get_array() is not None
1076 if fmt is None:
1077 fmt = mpl.ticker.ScalarFormatter(useOffset=False, useMathText=True)
1078 elif isinstance(fmt, str):
1079 fmt = mpl.ticker.StrMethodFormatter(fmt)
1080 fmt.create_dummy_axis()
1082 if prop == "colors":
1083 if not hasarray:
1084 warnings.warn("Collection without array used. Make sure to "
1085 "specify the values to be colormapped via the "
1086 "`c` argument.")
1087 return handles, labels
1088 u = np.unique(self.get_array())
1089 size = kwargs.pop("size", mpl.rcParams["lines.markersize"])
1090 elif prop == "sizes":
1091 u = np.unique(self.get_sizes())
1092 color = kwargs.pop("color", "k")
1093 else:
1094 raise ValueError("Valid values for `prop` are 'colors' or "
1095 f"'sizes'. You supplied '{prop}' instead.")
1097 fu = func(u)
1098 fmt.axis.set_view_interval(fu.min(), fu.max())
1099 fmt.axis.set_data_interval(fu.min(), fu.max())
1100 if num == "auto":
1101 num = 9
1102 if len(u) <= num:
1103 num = None
1104 if num is None:
1105 values = u
1106 label_values = func(values)
1107 else:
1108 if prop == "colors":
1109 arr = self.get_array()
1110 elif prop == "sizes":
1111 arr = self.get_sizes()
1112 if isinstance(num, mpl.ticker.Locator):
1113 loc = num
1114 elif np.iterable(num):
1115 loc = mpl.ticker.FixedLocator(num)
1116 else:
1117 num = int(num)
1118 loc = mpl.ticker.MaxNLocator(nbins=num, min_n_ticks=num-1,
1119 steps=[1, 2, 2.5, 3, 5, 6, 8, 10])
1120 label_values = loc.tick_values(func(arr).min(), func(arr).max())
1121 cond = ((label_values >= func(arr).min()) &
1122 (label_values <= func(arr).max()))
1123 label_values = label_values[cond]
1124 yarr = np.linspace(arr.min(), arr.max(), 256)
1125 xarr = func(yarr)
1126 ix = np.argsort(xarr)
1127 values = np.interp(label_values, xarr[ix], yarr[ix])
1129 kw = {"markeredgewidth": self.get_linewidths()[0],
1130 "alpha": self.get_alpha(),
1131 **kwargs}
1133 for val, lab in zip(values, label_values):
1134 if prop == "colors":
1135 color = self.cmap(self.norm(val))
1136 elif prop == "sizes":
1137 size = np.sqrt(val)
1138 if np.isclose(size, 0.0):
1139 continue
1140 h = mlines.Line2D([0], [0], ls="", color=color, ms=size,
1141 marker=self.get_paths()[0], **kw)
1142 handles.append(h)
1143 if hasattr(fmt, "set_locs"):
1144 fmt.set_locs(label_values)
1145 l = fmt(lab)
1146 labels.append(l)
1148 return handles, labels
1151class PolyCollection(_CollectionWithSizes):
1153 @_api.make_keyword_only("3.6", name="closed")
1154 def __init__(self, verts, sizes=None, closed=True, **kwargs):
1155 """
1156 Parameters
1157 ----------
1158 verts : list of array-like
1159 The sequence of polygons [*verts0*, *verts1*, ...] where each
1160 element *verts_i* defines the vertices of polygon *i* as a 2D
1161 array-like of shape (M, 2).
1162 sizes : array-like, default: None
1163 Squared scaling factors for the polygons. The coordinates of each
1164 polygon *verts_i* are multiplied by the square-root of the
1165 corresponding entry in *sizes* (i.e., *sizes* specify the scaling
1166 of areas). The scaling is applied before the Artist master
1167 transform.
1168 closed : bool, default: True
1169 Whether the polygon should be closed by adding a CLOSEPOLY
1170 connection at the end.
1171 **kwargs
1172 Forwarded to `.Collection`.
1173 """
1174 super().__init__(**kwargs)
1175 self.set_sizes(sizes)
1176 self.set_verts(verts, closed)
1177 self.stale = True
1179 def set_verts(self, verts, closed=True):
1180 """
1181 Set the vertices of the polygons.
1183 Parameters
1184 ----------
1185 verts : list of array-like
1186 The sequence of polygons [*verts0*, *verts1*, ...] where each
1187 element *verts_i* defines the vertices of polygon *i* as a 2D
1188 array-like of shape (M, 2).
1189 closed : bool, default: True
1190 Whether the polygon should be closed by adding a CLOSEPOLY
1191 connection at the end.
1192 """
1193 self.stale = True
1194 if isinstance(verts, np.ma.MaskedArray):
1195 verts = verts.astype(float).filled(np.nan)
1197 # No need to do anything fancy if the path isn't closed.
1198 if not closed:
1199 self._paths = [mpath.Path(xy) for xy in verts]
1200 return
1202 # Fast path for arrays
1203 if isinstance(verts, np.ndarray) and len(verts.shape) == 3:
1204 verts_pad = np.concatenate((verts, verts[:, :1]), axis=1)
1205 # Creating the codes once is much faster than having Path do it
1206 # separately each time by passing closed=True.
1207 codes = np.empty(verts_pad.shape[1], dtype=mpath.Path.code_type)
1208 codes[:] = mpath.Path.LINETO
1209 codes[0] = mpath.Path.MOVETO
1210 codes[-1] = mpath.Path.CLOSEPOLY
1211 self._paths = [mpath.Path(xy, codes) for xy in verts_pad]
1212 return
1214 self._paths = []
1215 for xy in verts:
1216 if len(xy):
1217 self._paths.append(mpath.Path._create_closed(xy))
1218 else:
1219 self._paths.append(mpath.Path(xy))
1221 set_paths = set_verts
1223 def set_verts_and_codes(self, verts, codes):
1224 """Initialize vertices with path codes."""
1225 if len(verts) != len(codes):
1226 raise ValueError("'codes' must be a 1D list or array "
1227 "with the same length of 'verts'")
1228 self._paths = [mpath.Path(xy, cds) if len(xy) else mpath.Path(xy)
1229 for xy, cds in zip(verts, codes)]
1230 self.stale = True
1233class BrokenBarHCollection(PolyCollection):
1234 """
1235 A collection of horizontal bars spanning *yrange* with a sequence of
1236 *xranges*.
1237 """
1238 def __init__(self, xranges, yrange, **kwargs):
1239 """
1240 Parameters
1241 ----------
1242 xranges : list of (float, float)
1243 The sequence of (left-edge-position, width) pairs for each bar.
1244 yrange : (float, float)
1245 The (lower-edge, height) common to all bars.
1246 **kwargs
1247 Forwarded to `.Collection`.
1248 """
1249 ymin, ywidth = yrange
1250 ymax = ymin + ywidth
1251 verts = [[(xmin, ymin),
1252 (xmin, ymax),
1253 (xmin + xwidth, ymax),
1254 (xmin + xwidth, ymin),
1255 (xmin, ymin)] for xmin, xwidth in xranges]
1256 super().__init__(verts, **kwargs)
1258 @classmethod
1259 def span_where(cls, x, ymin, ymax, where, **kwargs):
1260 """
1261 Return a `.BrokenBarHCollection` that plots horizontal bars from
1262 over the regions in *x* where *where* is True. The bars range
1263 on the y-axis from *ymin* to *ymax*
1265 *kwargs* are passed on to the collection.
1266 """
1267 xranges = []
1268 for ind0, ind1 in cbook.contiguous_regions(where):
1269 xslice = x[ind0:ind1]
1270 if not len(xslice):
1271 continue
1272 xranges.append((xslice[0], xslice[-1] - xslice[0]))
1273 return cls(xranges, [ymin, ymax - ymin], **kwargs)
1276class RegularPolyCollection(_CollectionWithSizes):
1277 """A collection of n-sided regular polygons."""
1279 _path_generator = mpath.Path.unit_regular_polygon
1280 _factor = np.pi ** (-1/2)
1282 @_api.make_keyword_only("3.6", name="rotation")
1283 def __init__(self,
1284 numsides,
1285 rotation=0,
1286 sizes=(1,),
1287 **kwargs):
1288 """
1289 Parameters
1290 ----------
1291 numsides : int
1292 The number of sides of the polygon.
1293 rotation : float
1294 The rotation of the polygon in radians.
1295 sizes : tuple of float
1296 The area of the circle circumscribing the polygon in points^2.
1297 **kwargs
1298 Forwarded to `.Collection`.
1300 Examples
1301 --------
1302 See :doc:`/gallery/event_handling/lasso_demo` for a complete example::
1304 offsets = np.random.rand(20, 2)
1305 facecolors = [cm.jet(x) for x in np.random.rand(20)]
1307 collection = RegularPolyCollection(
1308 numsides=5, # a pentagon
1309 rotation=0, sizes=(50,),
1310 facecolors=facecolors,
1311 edgecolors=("black",),
1312 linewidths=(1,),
1313 offsets=offsets,
1314 offset_transform=ax.transData,
1315 )
1316 """
1317 super().__init__(**kwargs)
1318 self.set_sizes(sizes)
1319 self._numsides = numsides
1320 self._paths = [self._path_generator(numsides)]
1321 self._rotation = rotation
1322 self.set_transform(transforms.IdentityTransform())
1324 def get_numsides(self):
1325 return self._numsides
1327 def get_rotation(self):
1328 return self._rotation
1330 @artist.allow_rasterization
1331 def draw(self, renderer):
1332 self.set_sizes(self._sizes, self.figure.dpi)
1333 self._transforms = [
1334 transforms.Affine2D(x).rotate(-self._rotation).get_matrix()
1335 for x in self._transforms
1336 ]
1337 # Explicitly not super().draw, because set_sizes must be called before
1338 # updating self._transforms.
1339 Collection.draw(self, renderer)
1342class StarPolygonCollection(RegularPolyCollection):
1343 """Draw a collection of regular stars with *numsides* points."""
1344 _path_generator = mpath.Path.unit_regular_star
1347class AsteriskPolygonCollection(RegularPolyCollection):
1348 """Draw a collection of regular asterisks with *numsides* points."""
1349 _path_generator = mpath.Path.unit_regular_asterisk
1352class LineCollection(Collection):
1353 r"""
1354 Represents a sequence of `.Line2D`\s that should be drawn together.
1356 This class extends `.Collection` to represent a sequence of
1357 `.Line2D`\s instead of just a sequence of `.Patch`\s.
1358 Just as in `.Collection`, each property of a *LineCollection* may be either
1359 a single value or a list of values. This list is then used cyclically for
1360 each element of the LineCollection, so the property of the ``i``\th element
1361 of the collection is::
1363 prop[i % len(prop)]
1365 The properties of each member of a *LineCollection* default to their values
1366 in :rc:`lines.*` instead of :rc:`patch.*`, and the property *colors* is
1367 added in place of *edgecolors*.
1368 """
1370 _edge_default = True
1372 def __init__(self, segments, # Can be None.
1373 *,
1374 zorder=2, # Collection.zorder is 1
1375 **kwargs
1376 ):
1377 """
1378 Parameters
1379 ----------
1380 segments : list of array-like
1381 A sequence of (*line0*, *line1*, *line2*), where::
1383 linen = (x0, y0), (x1, y1), ... (xm, ym)
1385 or the equivalent numpy array with two columns. Each line
1386 can have a different number of segments.
1387 linewidths : float or list of float, default: :rc:`lines.linewidth`
1388 The width of each line in points.
1389 colors : color or list of color, default: :rc:`lines.color`
1390 A sequence of RGBA tuples (e.g., arbitrary color strings, etc, not
1391 allowed).
1392 antialiaseds : bool or list of bool, default: :rc:`lines.antialiased`
1393 Whether to use antialiasing for each line.
1394 zorder : int, default: 2
1395 zorder of the lines once drawn.
1397 facecolors : color or list of color, default: 'none'
1398 When setting *facecolors*, each line is interpreted as a boundary
1399 for an area, implicitly closing the path from the last point to the
1400 first point. The enclosed area is filled with *facecolor*.
1401 In order to manually specify what should count as the "interior" of
1402 each line, please use `.PathCollection` instead, where the
1403 "interior" can be specified by appropriate usage of
1404 `~.path.Path.CLOSEPOLY`.
1406 **kwargs
1407 Forwarded to `.Collection`.
1408 """
1409 # Unfortunately, mplot3d needs this explicit setting of 'facecolors'.
1410 kwargs.setdefault('facecolors', 'none')
1411 super().__init__(
1412 zorder=zorder,
1413 **kwargs)
1414 self.set_segments(segments)
1416 def set_segments(self, segments):
1417 if segments is None:
1418 return
1420 self._paths = [mpath.Path(seg) if isinstance(seg, np.ma.MaskedArray)
1421 else mpath.Path(np.asarray(seg, float))
1422 for seg in segments]
1423 self.stale = True
1425 set_verts = set_segments # for compatibility with PolyCollection
1426 set_paths = set_segments
1428 def get_segments(self):
1429 """
1430 Returns
1431 -------
1432 list
1433 List of segments in the LineCollection. Each list item contains an
1434 array of vertices.
1435 """
1436 segments = []
1438 for path in self._paths:
1439 vertices = [
1440 vertex
1441 for vertex, _
1442 # Never simplify here, we want to get the data-space values
1443 # back and there in no way to know the "right" simplification
1444 # threshold so never try.
1445 in path.iter_segments(simplify=False)
1446 ]
1447 vertices = np.asarray(vertices)
1448 segments.append(vertices)
1450 return segments
1452 def _get_default_linewidth(self):
1453 return mpl.rcParams['lines.linewidth']
1455 def _get_default_antialiased(self):
1456 return mpl.rcParams['lines.antialiased']
1458 def _get_default_edgecolor(self):
1459 return mpl.rcParams['lines.color']
1461 def _get_default_facecolor(self):
1462 return 'none'
1464 def set_color(self, c):
1465 """
1466 Set the edgecolor(s) of the LineCollection.
1468 Parameters
1469 ----------
1470 c : color or list of colors
1471 Single color (all lines have same color), or a
1472 sequence of RGBA tuples; if it is a sequence the lines will
1473 cycle through the sequence.
1474 """
1475 self.set_edgecolor(c)
1477 set_colors = set_color
1479 def get_color(self):
1480 return self._edgecolors
1482 get_colors = get_color # for compatibility with old versions
1485class EventCollection(LineCollection):
1486 """
1487 A collection of locations along a single axis at which an "event" occurred.
1489 The events are given by a 1-dimensional array. They do not have an
1490 amplitude and are displayed as parallel lines.
1491 """
1493 _edge_default = True
1495 @_api.make_keyword_only("3.6", name="lineoffset")
1496 def __init__(self,
1497 positions, # Cannot be None.
1498 orientation='horizontal',
1499 lineoffset=0,
1500 linelength=1,
1501 linewidth=None,
1502 color=None,
1503 linestyle='solid',
1504 antialiased=None,
1505 **kwargs
1506 ):
1507 """
1508 Parameters
1509 ----------
1510 positions : 1D array-like
1511 Each value is an event.
1512 orientation : {'horizontal', 'vertical'}, default: 'horizontal'
1513 The sequence of events is plotted along this direction.
1514 The marker lines of the single events are along the orthogonal
1515 direction.
1516 lineoffset : float, default: 0
1517 The offset of the center of the markers from the origin, in the
1518 direction orthogonal to *orientation*.
1519 linelength : float, default: 1
1520 The total height of the marker (i.e. the marker stretches from
1521 ``lineoffset - linelength/2`` to ``lineoffset + linelength/2``).
1522 linewidth : float or list thereof, default: :rc:`lines.linewidth`
1523 The line width of the event lines, in points.
1524 color : color or list of colors, default: :rc:`lines.color`
1525 The color of the event lines.
1526 linestyle : str or tuple or list thereof, default: 'solid'
1527 Valid strings are ['solid', 'dashed', 'dashdot', 'dotted',
1528 '-', '--', '-.', ':']. Dash tuples should be of the form::
1530 (offset, onoffseq),
1532 where *onoffseq* is an even length tuple of on and off ink
1533 in points.
1534 antialiased : bool or list thereof, default: :rc:`lines.antialiased`
1535 Whether to use antialiasing for drawing the lines.
1536 **kwargs
1537 Forwarded to `.LineCollection`.
1539 Examples
1540 --------
1541 .. plot:: gallery/lines_bars_and_markers/eventcollection_demo.py
1542 """
1543 super().__init__([],
1544 linewidths=linewidth, linestyles=linestyle,
1545 colors=color, antialiaseds=antialiased,
1546 **kwargs)
1547 self._is_horizontal = True # Initial value, may be switched below.
1548 self._linelength = linelength
1549 self._lineoffset = lineoffset
1550 self.set_orientation(orientation)
1551 self.set_positions(positions)
1553 def get_positions(self):
1554 """
1555 Return an array containing the floating-point values of the positions.
1556 """
1557 pos = 0 if self.is_horizontal() else 1
1558 return [segment[0, pos] for segment in self.get_segments()]
1560 def set_positions(self, positions):
1561 """Set the positions of the events."""
1562 if positions is None:
1563 positions = []
1564 if np.ndim(positions) != 1:
1565 raise ValueError('positions must be one-dimensional')
1566 lineoffset = self.get_lineoffset()
1567 linelength = self.get_linelength()
1568 pos_idx = 0 if self.is_horizontal() else 1
1569 segments = np.empty((len(positions), 2, 2))
1570 segments[:, :, pos_idx] = np.sort(positions)[:, None]
1571 segments[:, 0, 1 - pos_idx] = lineoffset + linelength / 2
1572 segments[:, 1, 1 - pos_idx] = lineoffset - linelength / 2
1573 self.set_segments(segments)
1575 def add_positions(self, position):
1576 """Add one or more events at the specified positions."""
1577 if position is None or (hasattr(position, 'len') and
1578 len(position) == 0):
1579 return
1580 positions = self.get_positions()
1581 positions = np.hstack([positions, np.asanyarray(position)])
1582 self.set_positions(positions)
1583 extend_positions = append_positions = add_positions
1585 def is_horizontal(self):
1586 """True if the eventcollection is horizontal, False if vertical."""
1587 return self._is_horizontal
1589 def get_orientation(self):
1590 """
1591 Return the orientation of the event line ('horizontal' or 'vertical').
1592 """
1593 return 'horizontal' if self.is_horizontal() else 'vertical'
1595 def switch_orientation(self):
1596 """
1597 Switch the orientation of the event line, either from vertical to
1598 horizontal or vice versus.
1599 """
1600 segments = self.get_segments()
1601 for i, segment in enumerate(segments):
1602 segments[i] = np.fliplr(segment)
1603 self.set_segments(segments)
1604 self._is_horizontal = not self.is_horizontal()
1605 self.stale = True
1607 def set_orientation(self, orientation):
1608 """
1609 Set the orientation of the event line.
1611 Parameters
1612 ----------
1613 orientation : {'horizontal', 'vertical'}
1614 """
1615 is_horizontal = _api.check_getitem(
1616 {"horizontal": True, "vertical": False},
1617 orientation=orientation)
1618 if is_horizontal == self.is_horizontal():
1619 return
1620 self.switch_orientation()
1622 def get_linelength(self):
1623 """Return the length of the lines used to mark each event."""
1624 return self._linelength
1626 def set_linelength(self, linelength):
1627 """Set the length of the lines used to mark each event."""
1628 if linelength == self.get_linelength():
1629 return
1630 lineoffset = self.get_lineoffset()
1631 segments = self.get_segments()
1632 pos = 1 if self.is_horizontal() else 0
1633 for segment in segments:
1634 segment[0, pos] = lineoffset + linelength / 2.
1635 segment[1, pos] = lineoffset - linelength / 2.
1636 self.set_segments(segments)
1637 self._linelength = linelength
1639 def get_lineoffset(self):
1640 """Return the offset of the lines used to mark each event."""
1641 return self._lineoffset
1643 def set_lineoffset(self, lineoffset):
1644 """Set the offset of the lines used to mark each event."""
1645 if lineoffset == self.get_lineoffset():
1646 return
1647 linelength = self.get_linelength()
1648 segments = self.get_segments()
1649 pos = 1 if self.is_horizontal() else 0
1650 for segment in segments:
1651 segment[0, pos] = lineoffset + linelength / 2.
1652 segment[1, pos] = lineoffset - linelength / 2.
1653 self.set_segments(segments)
1654 self._lineoffset = lineoffset
1656 def get_linewidth(self):
1657 """Get the width of the lines used to mark each event."""
1658 return super().get_linewidth()[0]
1660 def get_linewidths(self):
1661 return super().get_linewidth()
1663 def get_color(self):
1664 """Return the color of the lines used to mark each event."""
1665 return self.get_colors()[0]
1668class CircleCollection(_CollectionWithSizes):
1669 """A collection of circles, drawn using splines."""
1671 _factor = np.pi ** (-1/2)
1673 def __init__(self, sizes, **kwargs):
1674 """
1675 Parameters
1676 ----------
1677 sizes : float or array-like
1678 The area of each circle in points^2.
1679 **kwargs
1680 Forwarded to `.Collection`.
1681 """
1682 super().__init__(**kwargs)
1683 self.set_sizes(sizes)
1684 self.set_transform(transforms.IdentityTransform())
1685 self._paths = [mpath.Path.unit_circle()]
1688class EllipseCollection(Collection):
1689 """A collection of ellipses, drawn using splines."""
1691 @_api.make_keyword_only("3.6", name="units")
1692 def __init__(self, widths, heights, angles, units='points', **kwargs):
1693 """
1694 Parameters
1695 ----------
1696 widths : array-like
1697 The lengths of the first axes (e.g., major axis lengths).
1698 heights : array-like
1699 The lengths of second axes.
1700 angles : array-like
1701 The angles of the first axes, degrees CCW from the x-axis.
1702 units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'}
1703 The units in which majors and minors are given; 'width' and
1704 'height' refer to the dimensions of the axes, while 'x' and 'y'
1705 refer to the *offsets* data units. 'xy' differs from all others in
1706 that the angle as plotted varies with the aspect ratio, and equals
1707 the specified angle only when the aspect ratio is unity. Hence
1708 it behaves the same as the `~.patches.Ellipse` with
1709 ``axes.transData`` as its transform.
1710 **kwargs
1711 Forwarded to `Collection`.
1712 """
1713 super().__init__(**kwargs)
1714 self._widths = 0.5 * np.asarray(widths).ravel()
1715 self._heights = 0.5 * np.asarray(heights).ravel()
1716 self._angles = np.deg2rad(angles).ravel()
1717 self._units = units
1718 self.set_transform(transforms.IdentityTransform())
1719 self._transforms = np.empty((0, 3, 3))
1720 self._paths = [mpath.Path.unit_circle()]
1722 def _set_transforms(self):
1723 """Calculate transforms immediately before drawing."""
1725 ax = self.axes
1726 fig = self.figure
1728 if self._units == 'xy':
1729 sc = 1
1730 elif self._units == 'x':
1731 sc = ax.bbox.width / ax.viewLim.width
1732 elif self._units == 'y':
1733 sc = ax.bbox.height / ax.viewLim.height
1734 elif self._units == 'inches':
1735 sc = fig.dpi
1736 elif self._units == 'points':
1737 sc = fig.dpi / 72.0
1738 elif self._units == 'width':
1739 sc = ax.bbox.width
1740 elif self._units == 'height':
1741 sc = ax.bbox.height
1742 elif self._units == 'dots':
1743 sc = 1.0
1744 else:
1745 raise ValueError(f'Unrecognized units: {self._units!r}')
1747 self._transforms = np.zeros((len(self._widths), 3, 3))
1748 widths = self._widths * sc
1749 heights = self._heights * sc
1750 sin_angle = np.sin(self._angles)
1751 cos_angle = np.cos(self._angles)
1752 self._transforms[:, 0, 0] = widths * cos_angle
1753 self._transforms[:, 0, 1] = heights * -sin_angle
1754 self._transforms[:, 1, 0] = widths * sin_angle
1755 self._transforms[:, 1, 1] = heights * cos_angle
1756 self._transforms[:, 2, 2] = 1.0
1758 _affine = transforms.Affine2D
1759 if self._units == 'xy':
1760 m = ax.transData.get_affine().get_matrix().copy()
1761 m[:2, 2:] = 0
1762 self.set_transform(_affine(m))
1764 @artist.allow_rasterization
1765 def draw(self, renderer):
1766 self._set_transforms()
1767 super().draw(renderer)
1770class PatchCollection(Collection):
1771 """
1772 A generic collection of patches.
1774 PatchCollection draws faster than a large number of equivalent individual
1775 Patches. It also makes it easier to assign a colormap to a heterogeneous
1776 collection of patches.
1777 """
1779 @_api.make_keyword_only("3.6", name="match_original")
1780 def __init__(self, patches, match_original=False, **kwargs):
1781 """
1782 Parameters
1783 ----------
1784 patches : list of `.Patch`
1785 A sequence of Patch objects. This list may include
1786 a heterogeneous assortment of different patch types.
1788 match_original : bool, default: False
1789 If True, use the colors and linewidths of the original
1790 patches. If False, new colors may be assigned by
1791 providing the standard collection arguments, facecolor,
1792 edgecolor, linewidths, norm or cmap.
1794 **kwargs
1795 All other parameters are forwarded to `.Collection`.
1797 If any of *edgecolors*, *facecolors*, *linewidths*, *antialiaseds*
1798 are None, they default to their `.rcParams` patch setting, in
1799 sequence form.
1801 Notes
1802 -----
1803 The use of `~matplotlib.cm.ScalarMappable` functionality is optional.
1804 If the `~matplotlib.cm.ScalarMappable` matrix ``_A`` has been set (via
1805 a call to `~.ScalarMappable.set_array`), at draw time a call to scalar
1806 mappable will be made to set the face colors.
1807 """
1809 if match_original:
1810 def determine_facecolor(patch):
1811 if patch.get_fill():
1812 return patch.get_facecolor()
1813 return [0, 0, 0, 0]
1815 kwargs['facecolors'] = [determine_facecolor(p) for p in patches]
1816 kwargs['edgecolors'] = [p.get_edgecolor() for p in patches]
1817 kwargs['linewidths'] = [p.get_linewidth() for p in patches]
1818 kwargs['linestyles'] = [p.get_linestyle() for p in patches]
1819 kwargs['antialiaseds'] = [p.get_antialiased() for p in patches]
1821 super().__init__(**kwargs)
1823 self.set_paths(patches)
1825 def set_paths(self, patches):
1826 paths = [p.get_transform().transform_path(p.get_path())
1827 for p in patches]
1828 self._paths = paths
1831class TriMesh(Collection):
1832 """
1833 Class for the efficient drawing of a triangular mesh using Gouraud shading.
1835 A triangular mesh is a `~matplotlib.tri.Triangulation` object.
1836 """
1837 def __init__(self, triangulation, **kwargs):
1838 super().__init__(**kwargs)
1839 self._triangulation = triangulation
1840 self._shading = 'gouraud'
1842 self._bbox = transforms.Bbox.unit()
1844 # Unfortunately this requires a copy, unless Triangulation
1845 # was rewritten.
1846 xy = np.hstack((triangulation.x.reshape(-1, 1),
1847 triangulation.y.reshape(-1, 1)))
1848 self._bbox.update_from_data_xy(xy)
1850 def get_paths(self):
1851 if self._paths is None:
1852 self.set_paths()
1853 return self._paths
1855 def set_paths(self):
1856 self._paths = self.convert_mesh_to_paths(self._triangulation)
1858 @staticmethod
1859 def convert_mesh_to_paths(tri):
1860 """
1861 Convert a given mesh into a sequence of `.Path` objects.
1863 This function is primarily of use to implementers of backends that do
1864 not directly support meshes.
1865 """
1866 triangles = tri.get_masked_triangles()
1867 verts = np.stack((tri.x[triangles], tri.y[triangles]), axis=-1)
1868 return [mpath.Path(x) for x in verts]
1870 @artist.allow_rasterization
1871 def draw(self, renderer):
1872 if not self.get_visible():
1873 return
1874 renderer.open_group(self.__class__.__name__, gid=self.get_gid())
1875 transform = self.get_transform()
1877 # Get a list of triangles and the color at each vertex.
1878 tri = self._triangulation
1879 triangles = tri.get_masked_triangles()
1881 verts = np.stack((tri.x[triangles], tri.y[triangles]), axis=-1)
1883 self.update_scalarmappable()
1884 colors = self._facecolors[triangles]
1886 gc = renderer.new_gc()
1887 self._set_gc_clip(gc)
1888 gc.set_linewidth(self.get_linewidth()[0])
1889 renderer.draw_gouraud_triangles(gc, verts, colors, transform.frozen())
1890 gc.restore()
1891 renderer.close_group(self.__class__.__name__)
1894class QuadMesh(Collection):
1895 r"""
1896 Class for the efficient drawing of a quadrilateral mesh.
1898 A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are
1899 defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is
1900 defined by the vertices ::
1902 (m+1, n) ----------- (m+1, n+1)
1903 / /
1904 / /
1905 / /
1906 (m, n) -------- (m, n+1)
1908 The mesh need not be regular and the polygons need not be convex.
1910 Parameters
1911 ----------
1912 coordinates : (M+1, N+1, 2) array-like
1913 The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates
1914 of vertex (m, n).
1916 antialiased : bool, default: True
1918 shading : {'flat', 'gouraud'}, default: 'flat'
1920 Notes
1921 -----
1922 Unlike other `.Collection`\s, the default *pickradius* of `.QuadMesh` is 0,
1923 i.e. `~.Artist.contains` checks whether the test point is within any of the
1924 mesh quadrilaterals.
1926 There exists a deprecated API version ``QuadMesh(M, N, coords)``, where
1927 the dimensions are given explicitly and ``coords`` is a (M*N, 2)
1928 array-like. This has been deprecated in Matplotlib 3.5. The following
1929 describes the semantics of this deprecated API.
1931 A quadrilateral mesh consists of a grid of vertices.
1932 The dimensions of this array are (*meshWidth* + 1, *meshHeight* + 1).
1933 Each vertex in the mesh has a different set of "mesh coordinates"
1934 representing its position in the topology of the mesh.
1935 For any values (*m*, *n*) such that 0 <= *m* <= *meshWidth*
1936 and 0 <= *n* <= *meshHeight*, the vertices at mesh coordinates
1937 (*m*, *n*), (*m*, *n* + 1), (*m* + 1, *n* + 1), and (*m* + 1, *n*)
1938 form one of the quadrilaterals in the mesh. There are thus
1939 (*meshWidth* * *meshHeight*) quadrilaterals in the mesh. The mesh
1940 need not be regular and the polygons need not be convex.
1942 A quadrilateral mesh is represented by a (2 x ((*meshWidth* + 1) *
1943 (*meshHeight* + 1))) numpy array *coordinates*, where each row is
1944 the *x* and *y* coordinates of one of the vertices. To define the
1945 function that maps from a data point to its corresponding color,
1946 use the :meth:`set_cmap` method. Each of these arrays is indexed in
1947 row-major order by the mesh coordinates of the vertex (or the mesh
1948 coordinates of the lower left vertex, in the case of the colors).
1950 For example, the first entry in *coordinates* is the coordinates of the
1951 vertex at mesh coordinates (0, 0), then the one at (0, 1), then at (0, 2)
1952 .. (0, meshWidth), (1, 0), (1, 1), and so on.
1953 """
1955 def __init__(self, *args, **kwargs):
1956 # signature deprecation since="3.5": Change to new signature after the
1957 # deprecation has expired. Also remove setting __init__.__signature__,
1958 # and remove the Notes from the docstring.
1959 params = _api.select_matching_signature(
1960 [
1961 lambda meshWidth, meshHeight, coordinates, antialiased=True,
1962 shading='flat', **kwargs: locals(),
1963 lambda coordinates, antialiased=True, shading='flat', **kwargs:
1964 locals()
1965 ],
1966 *args, **kwargs).values()
1967 *old_w_h, coords, antialiased, shading, kwargs = params
1968 if old_w_h: # The old signature matched.
1969 _api.warn_deprecated(
1970 "3.5",
1971 message="This usage of Quadmesh is deprecated: Parameters "
1972 "meshWidth and meshHeights will be removed; "
1973 "coordinates must be 2D; all parameters except "
1974 "coordinates will be keyword-only.")
1975 w, h = old_w_h
1976 coords = np.asarray(coords, np.float64).reshape((h + 1, w + 1, 2))
1977 kwargs.setdefault("pickradius", 0)
1978 # end of signature deprecation code
1980 _api.check_shape((None, None, 2), coordinates=coords)
1981 self._coordinates = coords
1982 self._antialiased = antialiased
1983 self._shading = shading
1984 self._bbox = transforms.Bbox.unit()
1985 self._bbox.update_from_data_xy(self._coordinates.reshape(-1, 2))
1986 # super init delayed after own init because array kwarg requires
1987 # self._coordinates and self._shading
1988 super().__init__(**kwargs)
1989 self.set_mouseover(False)
1991 # Only needed during signature deprecation
1992 __init__.__signature__ = inspect.signature(
1993 lambda self, coordinates, *,
1994 antialiased=True, shading='flat', pickradius=0, **kwargs: None)
1996 def get_paths(self):
1997 if self._paths is None:
1998 self.set_paths()
1999 return self._paths
2001 def set_paths(self):
2002 self._paths = self._convert_mesh_to_paths(self._coordinates)
2003 self.stale = True
2005 def set_array(self, A):
2006 """
2007 Set the data values.
2009 Parameters
2010 ----------
2011 A : (M, N) array-like or M*N array-like
2012 If the values are provided as a 2D grid, the shape must match the
2013 coordinates grid. If the values are 1D, they are reshaped to 2D.
2014 M, N follow from the coordinates grid, where the coordinates grid
2015 shape is (M, N) for 'gouraud' *shading* and (M+1, N+1) for 'flat'
2016 shading.
2017 """
2018 height, width = self._coordinates.shape[0:-1]
2019 misshapen_data = False
2020 faulty_data = False
2022 if self._shading == 'flat':
2023 h, w = height-1, width-1
2024 else:
2025 h, w = height, width
2027 if A is not None:
2028 shape = np.shape(A)
2029 if len(shape) == 1:
2030 if shape[0] != (h*w):
2031 faulty_data = True
2032 elif shape != (h, w):
2033 if np.prod(shape) == (h * w):
2034 misshapen_data = True
2035 else:
2036 faulty_data = True
2038 if misshapen_data:
2039 _api.warn_deprecated(
2040 "3.5", message=f"For X ({width}) and Y ({height}) "
2041 f"with {self._shading} shading, the expected shape of "
2042 f"A is ({h}, {w}). Passing A ({A.shape}) is deprecated "
2043 "since %(since)s and will become an error %(removal)s.")
2045 if faulty_data:
2046 raise TypeError(
2047 f"Dimensions of A {A.shape} are incompatible with "
2048 f"X ({width}) and/or Y ({height})")
2050 return super().set_array(A)
2052 def get_datalim(self, transData):
2053 return (self.get_transform() - transData).transform_bbox(self._bbox)
2055 def get_coordinates(self):
2056 """
2057 Return the vertices of the mesh as an (M+1, N+1, 2) array.
2059 M, N are the number of quadrilaterals in the rows / columns of the
2060 mesh, corresponding to (M+1, N+1) vertices.
2061 The last dimension specifies the components (x, y).
2062 """
2063 return self._coordinates
2065 @staticmethod
2066 @_api.deprecated("3.5", alternative="`QuadMesh(coordinates).get_paths()"
2067 "<.QuadMesh.get_paths>`")
2068 def convert_mesh_to_paths(meshWidth, meshHeight, coordinates):
2069 return QuadMesh._convert_mesh_to_paths(coordinates)
2071 @staticmethod
2072 def _convert_mesh_to_paths(coordinates):
2073 """
2074 Convert a given mesh into a sequence of `.Path` objects.
2076 This function is primarily of use to implementers of backends that do
2077 not directly support quadmeshes.
2078 """
2079 if isinstance(coordinates, np.ma.MaskedArray):
2080 c = coordinates.data
2081 else:
2082 c = coordinates
2083 points = np.concatenate([
2084 c[:-1, :-1],
2085 c[:-1, 1:],
2086 c[1:, 1:],
2087 c[1:, :-1],
2088 c[:-1, :-1]
2089 ], axis=2).reshape((-1, 5, 2))
2090 return [mpath.Path(x) for x in points]
2092 @_api.deprecated("3.5")
2093 def convert_mesh_to_triangles(self, meshWidth, meshHeight, coordinates):
2094 return self._convert_mesh_to_triangles(coordinates)
2096 def _convert_mesh_to_triangles(self, coordinates):
2097 """
2098 Convert a given mesh into a sequence of triangles, each point
2099 with its own color. The result can be used to construct a call to
2100 `~.RendererBase.draw_gouraud_triangle`.
2101 """
2102 if isinstance(coordinates, np.ma.MaskedArray):
2103 p = coordinates.data
2104 else:
2105 p = coordinates
2107 p_a = p[:-1, :-1]
2108 p_b = p[:-1, 1:]
2109 p_c = p[1:, 1:]
2110 p_d = p[1:, :-1]
2111 p_center = (p_a + p_b + p_c + p_d) / 4.0
2112 triangles = np.concatenate([
2113 p_a, p_b, p_center,
2114 p_b, p_c, p_center,
2115 p_c, p_d, p_center,
2116 p_d, p_a, p_center,
2117 ], axis=2).reshape((-1, 3, 2))
2119 c = self.get_facecolor().reshape((*coordinates.shape[:2], 4))
2120 c_a = c[:-1, :-1]
2121 c_b = c[:-1, 1:]
2122 c_c = c[1:, 1:]
2123 c_d = c[1:, :-1]
2124 c_center = (c_a + c_b + c_c + c_d) / 4.0
2125 colors = np.concatenate([
2126 c_a, c_b, c_center,
2127 c_b, c_c, c_center,
2128 c_c, c_d, c_center,
2129 c_d, c_a, c_center,
2130 ], axis=2).reshape((-1, 3, 4))
2132 return triangles, colors
2134 @artist.allow_rasterization
2135 def draw(self, renderer):
2136 if not self.get_visible():
2137 return
2138 renderer.open_group(self.__class__.__name__, self.get_gid())
2139 transform = self.get_transform()
2140 offset_trf = self.get_offset_transform()
2141 offsets = self.get_offsets()
2143 if self.have_units():
2144 xs = self.convert_xunits(offsets[:, 0])
2145 ys = self.convert_yunits(offsets[:, 1])
2146 offsets = np.column_stack([xs, ys])
2148 self.update_scalarmappable()
2150 if not transform.is_affine:
2151 coordinates = self._coordinates.reshape((-1, 2))
2152 coordinates = transform.transform(coordinates)
2153 coordinates = coordinates.reshape(self._coordinates.shape)
2154 transform = transforms.IdentityTransform()
2155 else:
2156 coordinates = self._coordinates
2158 if not offset_trf.is_affine:
2159 offsets = offset_trf.transform_non_affine(offsets)
2160 offset_trf = offset_trf.get_affine()
2162 gc = renderer.new_gc()
2163 gc.set_snap(self.get_snap())
2164 self._set_gc_clip(gc)
2165 gc.set_linewidth(self.get_linewidth()[0])
2167 if self._shading == 'gouraud':
2168 triangles, colors = self._convert_mesh_to_triangles(coordinates)
2169 renderer.draw_gouraud_triangles(
2170 gc, triangles, colors, transform.frozen())
2171 else:
2172 renderer.draw_quad_mesh(
2173 gc, transform.frozen(),
2174 coordinates.shape[1] - 1, coordinates.shape[0] - 1,
2175 coordinates, offsets, offset_trf,
2176 # Backends expect flattened rgba arrays (n*m, 4) for fc and ec
2177 self.get_facecolor().reshape((-1, 4)),
2178 self._antialiased, self.get_edgecolors().reshape((-1, 4)))
2179 gc.restore()
2180 renderer.close_group(self.__class__.__name__)
2181 self.stale = False
2183 def get_cursor_data(self, event):
2184 contained, info = self.contains(event)
2185 if contained and self.get_array() is not None:
2186 return self.get_array().ravel()[info["ind"]]
2187 return None