| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573 |
- """
- Classes for the efficient drawing of large collections of objects that
- share most properties, e.g., a large number of line segments or
- polygons.
- The classes are not meant to be as flexible as their single element
- counterparts (e.g., you may not be able to select all line styles) but
- they are meant to be fast for common use cases (e.g., a large set of solid
- line segments).
- """
- import itertools
- import functools
- import math
- from numbers import Number, Real
- import warnings
- import numpy as np
- import matplotlib as mpl
- from . import (_api, _path, artist, cbook, colorizer as mcolorizer, colors as mcolors,
- _docstring, hatch as mhatch, lines as mlines, path as mpath, transforms)
- from ._enums import JoinStyle, CapStyle
- # "color" is excluded; it is a compound setter, and its docstring differs
- # in LineCollection.
- @_api.define_aliases({
- "antialiased": ["antialiaseds", "aa"],
- "edgecolor": ["edgecolors", "ec"],
- "facecolor": ["facecolors", "fc"],
- "linestyle": ["linestyles", "dashes", "ls"],
- "linewidth": ["linewidths", "lw"],
- "offset_transform": ["transOffset"],
- })
- class Collection(mcolorizer.ColorizingArtist):
- r"""
- Base class for Collections. Must be subclassed to be usable.
- A Collection represents a sequence of `.Patch`\es that can be drawn
- more efficiently together than individually. For example, when a single
- path is being drawn repeatedly at different offsets, the renderer can
- typically execute a ``draw_marker()`` call much more efficiently than a
- series of repeated calls to ``draw_path()`` with the offsets put in
- one-by-one.
- Most properties of a collection can be configured per-element. Therefore,
- Collections have "plural" versions of many of the properties of a `.Patch`
- (e.g. `.Collection.get_paths` instead of `.Patch.get_path`). Exceptions are
- the *zorder*, *hatch*, *pickradius*, *capstyle* and *joinstyle* properties,
- which can only be set globally for the whole collection.
- Besides these exceptions, all properties can be specified as single values
- (applying to all elements) or sequences of values. The property of the
- ``i``\th element of the collection is::
- prop[i % len(prop)]
- Each Collection can optionally be used as its own `.ScalarMappable` by
- passing the *norm* and *cmap* parameters to its constructor. If the
- Collection's `.ScalarMappable` matrix ``_A`` has been set (via a call
- to `.Collection.set_array`), then at draw time this internal scalar
- mappable will be used to set the ``facecolors`` and ``edgecolors``,
- ignoring those that were manually passed in.
- """
- #: Either a list of 3x3 arrays or an Nx3x3 array (representing N
- #: transforms), suitable for the `all_transforms` argument to
- #: `~matplotlib.backend_bases.RendererBase.draw_path_collection`;
- #: each 3x3 array is used to initialize an
- #: `~matplotlib.transforms.Affine2D` object.
- #: Each kind of collection defines this based on its arguments.
- _transforms = np.empty((0, 3, 3))
- # Whether to draw an edge by default. Set on a
- # subclass-by-subclass basis.
- _edge_default = False
- @_docstring.interpd
- def __init__(self, *,
- edgecolors=None,
- facecolors=None,
- linewidths=None,
- linestyles='solid',
- capstyle=None,
- joinstyle=None,
- antialiaseds=None,
- offsets=None,
- offset_transform=None,
- norm=None, # optional for ScalarMappable
- cmap=None, # ditto
- colorizer=None,
- pickradius=5.0,
- hatch=None,
- urls=None,
- zorder=1,
- **kwargs
- ):
- """
- Parameters
- ----------
- edgecolors : :mpltype:`color` or list of colors, default: :rc:`patch.edgecolor`
- Edge color for each patch making up the collection. The special
- value 'face' can be passed to make the edgecolor match the
- facecolor.
- facecolors : :mpltype:`color` or list of colors, default: :rc:`patch.facecolor`
- Face color for each patch making up the collection.
- linewidths : float or list of floats, default: :rc:`patch.linewidth`
- Line width for each patch making up the collection.
- linestyles : str or tuple or list thereof, default: 'solid'
- Valid strings are ['solid', 'dashed', 'dashdot', 'dotted', '-',
- '--', '-.', ':']. Dash tuples should be of the form::
- (offset, onoffseq),
- where *onoffseq* is an even length tuple of on and off ink lengths
- in points. For examples, see
- :doc:`/gallery/lines_bars_and_markers/linestyles`.
- capstyle : `.CapStyle`-like, default: 'butt'
- Style to use for capping lines for all paths in the collection.
- Allowed values are %(CapStyle)s.
- joinstyle : `.JoinStyle`-like, default: 'round'
- Style to use for joining lines for all paths in the collection.
- Allowed values are %(JoinStyle)s.
- antialiaseds : bool or list of bool, default: :rc:`patch.antialiased`
- Whether each patch in the collection should be drawn with
- antialiasing.
- offsets : (float, float) or list thereof, default: (0, 0)
- A vector by which to translate each patch after rendering (default
- is no translation). The translation is performed in screen (pixel)
- coordinates (i.e. after the Artist's transform is applied).
- offset_transform : `~.Transform`, default: `.IdentityTransform`
- A single transform which will be applied to each *offsets* vector
- before it is used.
- cmap, norm
- Data normalization and colormapping parameters. See
- `.ScalarMappable` for a detailed description.
- hatch : str, optional
- Hatching pattern to use in filled paths, if any. Valid strings are
- ['/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*']. See
- :doc:`/gallery/shapes_and_collections/hatch_style_reference` for
- the meaning of each hatch type.
- pickradius : float, default: 5.0
- If ``pickradius <= 0``, then `.Collection.contains` will return
- ``True`` whenever the test point is inside of one of the polygons
- formed by the control points of a Path in the Collection. On the
- other hand, if it is greater than 0, then we instead check if the
- test point is contained in a stroke of width ``2*pickradius``
- following any of the Paths in the Collection.
- urls : list of str, default: None
- A URL for each patch to link to once drawn. Currently only works
- for the SVG backend. See :doc:`/gallery/misc/hyperlinks_sgskip` for
- examples.
- zorder : float, default: 1
- The drawing order, shared by all Patches in the Collection. See
- :doc:`/gallery/misc/zorder_demo` for all defaults and examples.
- **kwargs
- Remaining keyword arguments will be used to set properties as
- ``Collection.set_{key}(val)`` for each key-value pair in *kwargs*.
- """
- super().__init__(self._get_colorizer(cmap, norm, colorizer))
- # list of un-scaled dash patterns
- # this is needed scaling the dash pattern by linewidth
- self._us_linestyles = [(0, None)]
- # list of dash patterns
- self._linestyles = [(0, None)]
- # list of unbroadcast/scaled linewidths
- self._us_lw = [0]
- self._linewidths = [0]
- self._gapcolor = None # Currently only used by LineCollection.
- # Flags set by _set_mappable_flags: are colors from mapping an array?
- self._face_is_mapped = None
- self._edge_is_mapped = None
- self._mapped_colors = None # calculated in update_scalarmappable
- self._hatch_color = mcolors.to_rgba(mpl.rcParams['hatch.color'])
- self._hatch_linewidth = mpl.rcParams['hatch.linewidth']
- self.set_facecolor(facecolors)
- self.set_edgecolor(edgecolors)
- self.set_linewidth(linewidths)
- self.set_linestyle(linestyles)
- self.set_antialiased(antialiaseds)
- self.set_pickradius(pickradius)
- self.set_urls(urls)
- self.set_hatch(hatch)
- self.set_zorder(zorder)
- if capstyle:
- self.set_capstyle(capstyle)
- else:
- self._capstyle = None
- if joinstyle:
- self.set_joinstyle(joinstyle)
- else:
- self._joinstyle = None
- if offsets is not None:
- offsets = np.asanyarray(offsets, float)
- # Broadcast (2,) -> (1, 2) but nothing else.
- if offsets.shape == (2,):
- offsets = offsets[None, :]
- self._offsets = offsets
- self._offset_transform = offset_transform
- self._path_effects = None
- self._internal_update(kwargs)
- self._paths = None
- def get_paths(self):
- return self._paths
- def set_paths(self, paths):
- self._paths = paths
- self.stale = True
- def get_transforms(self):
- return self._transforms
- def get_offset_transform(self):
- """Return the `.Transform` instance used by this artist offset."""
- if self._offset_transform is None:
- self._offset_transform = transforms.IdentityTransform()
- elif (not isinstance(self._offset_transform, transforms.Transform)
- and hasattr(self._offset_transform, '_as_mpl_transform')):
- self._offset_transform = \
- self._offset_transform._as_mpl_transform(self.axes)
- return self._offset_transform
- def set_offset_transform(self, offset_transform):
- """
- Set the artist offset transform.
- Parameters
- ----------
- offset_transform : `.Transform`
- """
- self._offset_transform = offset_transform
- def get_datalim(self, transData):
- # Calculate the data limits and return them as a `.Bbox`.
- #
- # This operation depends on the transforms for the data in the
- # collection and whether the collection has offsets:
- #
- # 1. offsets = None, transform child of transData: use the paths for
- # the automatic limits (i.e. for LineCollection in streamline).
- # 2. offsets != None: offset_transform is child of transData:
- #
- # a. transform is child of transData: use the path + offset for
- # limits (i.e for bar).
- # b. transform is not a child of transData: just use the offsets
- # for the limits (i.e. for scatter)
- #
- # 3. otherwise return a null Bbox.
- transform = self.get_transform()
- offset_trf = self.get_offset_transform()
- if not (isinstance(offset_trf, transforms.IdentityTransform)
- or offset_trf.contains_branch(transData)):
- # if the offsets are in some coords other than data,
- # then don't use them for autoscaling.
- return transforms.Bbox.null()
- paths = self.get_paths()
- if not len(paths):
- # No paths to transform
- return transforms.Bbox.null()
- if not transform.is_affine:
- paths = [transform.transform_path_non_affine(p) for p in paths]
- # Don't convert transform to transform.get_affine() here because
- # we may have transform.contains_branch(transData) but not
- # transforms.get_affine().contains_branch(transData). But later,
- # be careful to only apply the affine part that remains.
- offsets = self.get_offsets()
- if any(transform.contains_branch_seperately(transData)):
- # collections that are just in data units (like quiver)
- # can properly have the axes limits set by their shape +
- # offset. LineCollections that have no offsets can
- # also use this algorithm (like streamplot).
- if isinstance(offsets, np.ma.MaskedArray):
- offsets = offsets.filled(np.nan)
- # get_path_collection_extents handles nan but not masked arrays
- return mpath.get_path_collection_extents(
- transform.get_affine() - transData, paths,
- self.get_transforms(),
- offset_trf.transform_non_affine(offsets),
- offset_trf.get_affine().frozen())
- # NOTE: None is the default case where no offsets were passed in
- if self._offsets is not None:
- # this is for collections that have their paths (shapes)
- # in physical, axes-relative, or figure-relative units
- # (i.e. like scatter). We can't uniquely set limits based on
- # those shapes, so we just set the limits based on their
- # location.
- offsets = (offset_trf - transData).transform(offsets)
- # note A-B means A B^{-1}
- offsets = np.ma.masked_invalid(offsets)
- if not offsets.mask.all():
- bbox = transforms.Bbox.null()
- bbox.update_from_data_xy(offsets)
- return bbox
- return transforms.Bbox.null()
- def get_window_extent(self, renderer=None):
- # TODO: check to ensure that this does not fail for
- # cases other than scatter plot legend
- return self.get_datalim(transforms.IdentityTransform())
- def _prepare_points(self):
- # Helper for drawing and hit testing.
- transform = self.get_transform()
- offset_trf = self.get_offset_transform()
- offsets = self.get_offsets()
- paths = self.get_paths()
- if self.have_units():
- paths = []
- for path in self.get_paths():
- vertices = path.vertices
- xs, ys = vertices[:, 0], vertices[:, 1]
- xs = self.convert_xunits(xs)
- ys = self.convert_yunits(ys)
- paths.append(mpath.Path(np.column_stack([xs, ys]), path.codes))
- xs = self.convert_xunits(offsets[:, 0])
- ys = self.convert_yunits(offsets[:, 1])
- offsets = np.ma.column_stack([xs, ys])
- if not transform.is_affine:
- paths = [transform.transform_path_non_affine(path)
- for path in paths]
- transform = transform.get_affine()
- if not offset_trf.is_affine:
- offsets = offset_trf.transform_non_affine(offsets)
- # This might have changed an ndarray into a masked array.
- offset_trf = offset_trf.get_affine()
- if isinstance(offsets, np.ma.MaskedArray):
- offsets = offsets.filled(np.nan)
- # Changing from a masked array to nan-filled ndarray
- # is probably most efficient at this point.
- return transform, offset_trf, offsets, paths
- @artist.allow_rasterization
- def draw(self, renderer):
- if not self.get_visible():
- return
- renderer.open_group(self.__class__.__name__, self.get_gid())
- self.update_scalarmappable()
- transform, offset_trf, offsets, paths = self._prepare_points()
- gc = renderer.new_gc()
- self._set_gc_clip(gc)
- gc.set_snap(self.get_snap())
- if self._hatch:
- gc.set_hatch(self._hatch)
- gc.set_hatch_color(self._hatch_color)
- gc.set_hatch_linewidth(self._hatch_linewidth)
- if self.get_sketch_params() is not None:
- gc.set_sketch_params(*self.get_sketch_params())
- if self.get_path_effects():
- from matplotlib.patheffects import PathEffectRenderer
- renderer = PathEffectRenderer(self.get_path_effects(), renderer)
- # If the collection is made up of a single shape/color/stroke,
- # it can be rendered once and blitted multiple times, using
- # `draw_markers` rather than `draw_path_collection`. This is
- # *much* faster for Agg, and results in smaller file sizes in
- # PDF/SVG/PS.
- trans = self.get_transforms()
- facecolors = self.get_facecolor()
- edgecolors = self.get_edgecolor()
- do_single_path_optimization = False
- if (len(paths) == 1 and len(trans) <= 1 and
- len(facecolors) == 1 and len(edgecolors) == 1 and
- len(self._linewidths) == 1 and
- all(ls[1] is None for ls in self._linestyles) and
- len(self._antialiaseds) == 1 and len(self._urls) == 1 and
- self.get_hatch() is None):
- if len(trans):
- combined_transform = transforms.Affine2D(trans[0]) + transform
- else:
- combined_transform = transform
- extents = paths[0].get_extents(combined_transform)
- if (extents.width < self.get_figure(root=True).bbox.width
- and extents.height < self.get_figure(root=True).bbox.height):
- do_single_path_optimization = True
- if self._joinstyle:
- gc.set_joinstyle(self._joinstyle)
- if self._capstyle:
- gc.set_capstyle(self._capstyle)
- if do_single_path_optimization:
- gc.set_foreground(tuple(edgecolors[0]))
- gc.set_linewidth(self._linewidths[0])
- gc.set_dashes(*self._linestyles[0])
- gc.set_antialiased(self._antialiaseds[0])
- gc.set_url(self._urls[0])
- renderer.draw_markers(
- gc, paths[0], combined_transform.frozen(),
- mpath.Path(offsets), offset_trf, tuple(facecolors[0]))
- else:
- if self._gapcolor is not None:
- # First draw paths within the gaps.
- ipaths, ilinestyles = self._get_inverse_paths_linestyles()
- renderer.draw_path_collection(
- gc, transform.frozen(), ipaths,
- self.get_transforms(), offsets, offset_trf,
- [mcolors.to_rgba("none")], self._gapcolor,
- self._linewidths, ilinestyles,
- self._antialiaseds, self._urls,
- "screen")
- renderer.draw_path_collection(
- gc, transform.frozen(), paths,
- self.get_transforms(), offsets, offset_trf,
- self.get_facecolor(), self.get_edgecolor(),
- self._linewidths, self._linestyles,
- self._antialiaseds, self._urls,
- "screen") # offset_position, kept for backcompat.
- gc.restore()
- renderer.close_group(self.__class__.__name__)
- self.stale = False
- def set_pickradius(self, pickradius):
- """
- Set the pick radius used for containment tests.
- Parameters
- ----------
- pickradius : float
- Pick radius, in points.
- """
- if not isinstance(pickradius, Real):
- raise ValueError(
- f"pickradius must be a real-valued number, not {pickradius!r}")
- self._pickradius = pickradius
- def get_pickradius(self):
- return self._pickradius
- def contains(self, mouseevent):
- """
- Test whether the mouse event occurred in the collection.
- Returns ``bool, dict(ind=itemlist)``, where every item in itemlist
- contains the event.
- """
- if self._different_canvas(mouseevent) or not self.get_visible():
- return False, {}
- pickradius = (
- float(self._picker)
- if isinstance(self._picker, Number) and
- self._picker is not True # the bool, not just nonzero or 1
- else self._pickradius)
- if self.axes:
- self.axes._unstale_viewLim()
- transform, offset_trf, offsets, paths = self._prepare_points()
- # Tests if the point is contained on one of the polygons formed
- # by the control points of each of the paths. A point is considered
- # "on" a path if it would lie within a stroke of width 2*pickradius
- # following the path. If pickradius <= 0, then we instead simply check
- # if the point is *inside* of the path instead.
- ind = _path.point_in_path_collection(
- mouseevent.x, mouseevent.y, pickradius,
- transform.frozen(), paths, self.get_transforms(),
- offsets, offset_trf, pickradius <= 0)
- return len(ind) > 0, dict(ind=ind)
- def set_urls(self, urls):
- """
- Parameters
- ----------
- urls : list of str or None
- Notes
- -----
- URLs are currently only implemented by the SVG backend. They are
- ignored by all other backends.
- """
- self._urls = urls if urls is not None else [None]
- self.stale = True
- def get_urls(self):
- """
- Return a list of URLs, one for each element of the collection.
- The list contains *None* for elements without a URL. See
- :doc:`/gallery/misc/hyperlinks_sgskip` for an example.
- """
- return self._urls
- def set_hatch(self, hatch):
- r"""
- Set the hatching pattern
- *hatch* can be one of::
- / - diagonal hatching
- \ - back diagonal
- | - vertical
- - - horizontal
- + - crossed
- x - crossed diagonal
- o - small circle
- O - large circle
- . - dots
- * - stars
- Letters can be combined, in which case all the specified
- hatchings are done. If same letter repeats, it increases the
- density of hatching of that pattern.
- Unlike other properties such as linewidth and colors, hatching
- can only be specified for the collection as a whole, not separately
- for each member.
- Parameters
- ----------
- hatch : {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
- """
- # Use validate_hatch(list) after deprecation.
- mhatch._validate_hatch_pattern(hatch)
- self._hatch = hatch
- self.stale = True
- def get_hatch(self):
- """Return the current hatching pattern."""
- return self._hatch
- def set_hatch_linewidth(self, lw):
- """Set the hatch linewidth."""
- self._hatch_linewidth = lw
- def get_hatch_linewidth(self):
- """Return the hatch linewidth."""
- return self._hatch_linewidth
- def set_offsets(self, offsets):
- """
- Set the offsets for the collection.
- Parameters
- ----------
- offsets : (N, 2) or (2,) array-like
- """
- offsets = np.asanyarray(offsets)
- if offsets.shape == (2,): # Broadcast (2,) -> (1, 2) but nothing else.
- offsets = offsets[None, :]
- cstack = (np.ma.column_stack if isinstance(offsets, np.ma.MaskedArray)
- else np.column_stack)
- self._offsets = cstack(
- (np.asanyarray(self.convert_xunits(offsets[:, 0]), float),
- np.asanyarray(self.convert_yunits(offsets[:, 1]), float)))
- self.stale = True
- def get_offsets(self):
- """Return the offsets for the collection."""
- # Default to zeros in the no-offset (None) case
- return np.zeros((1, 2)) if self._offsets is None else self._offsets
- def _get_default_linewidth(self):
- # This may be overridden in a subclass.
- return mpl.rcParams['patch.linewidth'] # validated as float
- def set_linewidth(self, lw):
- """
- Set the linewidth(s) for the collection. *lw* can be a scalar
- or a sequence; if it is a sequence the patches will cycle
- through the sequence
- Parameters
- ----------
- lw : float or list of floats
- """
- if lw is None:
- lw = self._get_default_linewidth()
- # get the un-scaled/broadcast lw
- self._us_lw = np.atleast_1d(lw)
- # scale all of the dash patterns.
- self._linewidths, self._linestyles = self._bcast_lwls(
- self._us_lw, self._us_linestyles)
- self.stale = True
- def set_linestyle(self, ls):
- """
- Set the linestyle(s) for the collection.
- =========================== =================
- linestyle description
- =========================== =================
- ``'-'`` or ``'solid'`` solid line
- ``'--'`` or ``'dashed'`` dashed line
- ``'-.'`` or ``'dashdot'`` dash-dotted line
- ``':'`` or ``'dotted'`` dotted line
- =========================== =================
- Alternatively a dash tuple of the following form can be provided::
- (offset, onoffseq),
- where ``onoffseq`` is an even length tuple of on and off ink in points.
- Parameters
- ----------
- ls : str or tuple or list thereof
- Valid values for individual linestyles include {'-', '--', '-.',
- ':', '', (offset, on-off-seq)}. See `.Line2D.set_linestyle` for a
- complete description.
- """
- # get the list of raw 'unscaled' dash patterns
- self._us_linestyles = mlines._get_dash_patterns(ls)
- # broadcast and scale the lw and dash patterns
- self._linewidths, self._linestyles = self._bcast_lwls(
- self._us_lw, self._us_linestyles)
- @_docstring.interpd
- def set_capstyle(self, cs):
- """
- Set the `.CapStyle` for the collection (for all its elements).
- Parameters
- ----------
- cs : `.CapStyle` or %(CapStyle)s
- """
- self._capstyle = CapStyle(cs)
- @_docstring.interpd
- def get_capstyle(self):
- """
- Return the cap style for the collection (for all its elements).
- Returns
- -------
- %(CapStyle)s or None
- """
- return self._capstyle.name if self._capstyle else None
- @_docstring.interpd
- def set_joinstyle(self, js):
- """
- Set the `.JoinStyle` for the collection (for all its elements).
- Parameters
- ----------
- js : `.JoinStyle` or %(JoinStyle)s
- """
- self._joinstyle = JoinStyle(js)
- @_docstring.interpd
- def get_joinstyle(self):
- """
- Return the join style for the collection (for all its elements).
- Returns
- -------
- %(JoinStyle)s or None
- """
- return self._joinstyle.name if self._joinstyle else None
- @staticmethod
- def _bcast_lwls(linewidths, dashes):
- """
- Internal helper function to broadcast + scale ls/lw
- In the collection drawing code, the linewidth and linestyle are cycled
- through as circular buffers (via ``v[i % len(v)]``). Thus, if we are
- going to scale the dash pattern at set time (not draw time) we need to
- do the broadcasting now and expand both lists to be the same length.
- Parameters
- ----------
- linewidths : list
- line widths of collection
- dashes : list
- dash specification (offset, (dash pattern tuple))
- Returns
- -------
- linewidths, dashes : list
- Will be the same length, dashes are scaled by paired linewidth
- """
- if mpl.rcParams['_internal.classic_mode']:
- return linewidths, dashes
- # make sure they are the same length so we can zip them
- if len(dashes) != len(linewidths):
- l_dashes = len(dashes)
- l_lw = len(linewidths)
- gcd = math.gcd(l_dashes, l_lw)
- dashes = list(dashes) * (l_lw // gcd)
- linewidths = list(linewidths) * (l_dashes // gcd)
- # scale the dash patterns
- dashes = [mlines._scale_dashes(o, d, lw)
- for (o, d), lw in zip(dashes, linewidths)]
- return linewidths, dashes
- def get_antialiased(self):
- """
- Get the antialiasing state for rendering.
- Returns
- -------
- array of bools
- """
- return self._antialiaseds
- def set_antialiased(self, aa):
- """
- Set the antialiasing state for rendering.
- Parameters
- ----------
- aa : bool or list of bools
- """
- if aa is None:
- aa = self._get_default_antialiased()
- self._antialiaseds = np.atleast_1d(np.asarray(aa, bool))
- self.stale = True
- def _get_default_antialiased(self):
- # This may be overridden in a subclass.
- return mpl.rcParams['patch.antialiased']
- def set_color(self, c):
- """
- Set both the edgecolor and the facecolor.
- Parameters
- ----------
- c : :mpltype:`color` or list of RGBA tuples
- See Also
- --------
- Collection.set_facecolor, Collection.set_edgecolor
- For setting the edge or face color individually.
- """
- self.set_facecolor(c)
- self.set_edgecolor(c)
- def _get_default_facecolor(self):
- # This may be overridden in a subclass.
- return mpl.rcParams['patch.facecolor']
- def _set_facecolor(self, c):
- if c is None:
- c = self._get_default_facecolor()
- self._facecolors = mcolors.to_rgba_array(c, self._alpha)
- self.stale = True
- def set_facecolor(self, c):
- """
- Set the facecolor(s) of the collection. *c* can be a color (all patches
- have same color), or a sequence of colors; if it is a sequence the
- patches will cycle through the sequence.
- If *c* is 'none', the patch will not be filled.
- Parameters
- ----------
- c : :mpltype:`color` or list of :mpltype:`color`
- """
- if isinstance(c, str) and c.lower() in ("none", "face"):
- c = c.lower()
- self._original_facecolor = c
- self._set_facecolor(c)
- def get_facecolor(self):
- return self._facecolors
- def get_edgecolor(self):
- if cbook._str_equal(self._edgecolors, 'face'):
- return self.get_facecolor()
- else:
- return self._edgecolors
- def _get_default_edgecolor(self):
- # This may be overridden in a subclass.
- return mpl.rcParams['patch.edgecolor']
- def _set_edgecolor(self, c):
- set_hatch_color = True
- if c is None:
- if (mpl.rcParams['patch.force_edgecolor']
- or self._edge_default
- or cbook._str_equal(self._original_facecolor, 'none')):
- c = self._get_default_edgecolor()
- else:
- c = 'none'
- set_hatch_color = False
- if cbook._str_lower_equal(c, 'face'):
- self._edgecolors = 'face'
- self.stale = True
- return
- self._edgecolors = mcolors.to_rgba_array(c, self._alpha)
- if set_hatch_color and len(self._edgecolors):
- self._hatch_color = tuple(self._edgecolors[0])
- self.stale = True
- def set_edgecolor(self, c):
- """
- Set the edgecolor(s) of the collection.
- Parameters
- ----------
- c : :mpltype:`color` or list of :mpltype:`color` or 'face'
- The collection edgecolor(s). If a sequence, the patches cycle
- through it. If 'face', match the facecolor.
- """
- # We pass through a default value for use in LineCollection.
- # This allows us to maintain None as the default indicator in
- # _original_edgecolor.
- if isinstance(c, str) and c.lower() in ("none", "face"):
- c = c.lower()
- self._original_edgecolor = c
- self._set_edgecolor(c)
- def set_alpha(self, alpha):
- """
- Set the transparency of the collection.
- Parameters
- ----------
- alpha : float or array of float or None
- If not None, *alpha* values must be between 0 and 1, inclusive.
- If an array is provided, its length must match the number of
- elements in the collection. Masked values and nans are not
- supported.
- """
- artist.Artist._set_alpha_for_array(self, alpha)
- self._set_facecolor(self._original_facecolor)
- self._set_edgecolor(self._original_edgecolor)
- set_alpha.__doc__ = artist.Artist._set_alpha_for_array.__doc__
- def get_linewidth(self):
- return self._linewidths
- def get_linestyle(self):
- return self._linestyles
- def _set_mappable_flags(self):
- """
- Determine whether edges and/or faces are color-mapped.
- This is a helper for update_scalarmappable.
- It sets Boolean flags '_edge_is_mapped' and '_face_is_mapped'.
- Returns
- -------
- mapping_change : bool
- True if either flag is True, or if a flag has changed.
- """
- # The flags are initialized to None to ensure this returns True
- # the first time it is called.
- edge0 = self._edge_is_mapped
- face0 = self._face_is_mapped
- # After returning, the flags must be Booleans, not None.
- self._edge_is_mapped = False
- self._face_is_mapped = False
- if self._A is not None:
- if not cbook._str_equal(self._original_facecolor, 'none'):
- self._face_is_mapped = True
- if cbook._str_equal(self._original_edgecolor, 'face'):
- self._edge_is_mapped = True
- else:
- if self._original_edgecolor is None:
- self._edge_is_mapped = True
- mapped = self._face_is_mapped or self._edge_is_mapped
- changed = (edge0 is None or face0 is None
- or self._edge_is_mapped != edge0
- or self._face_is_mapped != face0)
- return mapped or changed
- def update_scalarmappable(self):
- """
- Update colors from the scalar mappable array, if any.
- Assign colors to edges and faces based on the array and/or
- colors that were directly set, as appropriate.
- """
- if not self._set_mappable_flags():
- return
- # Allow possibility to call 'self.set_array(None)'.
- if self._A is not None:
- # QuadMesh can map 2d arrays (but pcolormesh supplies 1d array)
- if self._A.ndim > 1 and not isinstance(self, _MeshData):
- raise ValueError('Collections can only map rank 1 arrays')
- if np.iterable(self._alpha):
- if self._alpha.size != self._A.size:
- raise ValueError(
- f'Data array shape, {self._A.shape} '
- 'is incompatible with alpha array shape, '
- f'{self._alpha.shape}. '
- 'This can occur with the deprecated '
- 'behavior of the "flat" shading option, '
- 'in which a row and/or column of the data '
- 'array is dropped.')
- # pcolormesh, scatter, maybe others flatten their _A
- self._alpha = self._alpha.reshape(self._A.shape)
- self._mapped_colors = self.to_rgba(self._A, self._alpha)
- if self._face_is_mapped:
- self._facecolors = self._mapped_colors
- else:
- self._set_facecolor(self._original_facecolor)
- if self._edge_is_mapped:
- self._edgecolors = self._mapped_colors
- else:
- self._set_edgecolor(self._original_edgecolor)
- self.stale = True
- def get_fill(self):
- """Return whether face is colored."""
- return not cbook._str_lower_equal(self._original_facecolor, "none")
- def update_from(self, other):
- """Copy properties from other to self."""
- artist.Artist.update_from(self, other)
- self._antialiaseds = other._antialiaseds
- self._mapped_colors = other._mapped_colors
- self._edge_is_mapped = other._edge_is_mapped
- self._original_edgecolor = other._original_edgecolor
- self._edgecolors = other._edgecolors
- self._face_is_mapped = other._face_is_mapped
- self._original_facecolor = other._original_facecolor
- self._facecolors = other._facecolors
- self._linewidths = other._linewidths
- self._linestyles = other._linestyles
- self._us_linestyles = other._us_linestyles
- self._pickradius = other._pickradius
- self._hatch = other._hatch
- # update_from for scalarmappable
- self._A = other._A
- self.norm = other.norm
- self.cmap = other.cmap
- self.stale = True
- class _CollectionWithSizes(Collection):
- """
- Base class for collections that have an array of sizes.
- """
- _factor = 1.0
- def get_sizes(self):
- """
- Return the sizes ('areas') of the elements in the collection.
- Returns
- -------
- array
- The 'area' of each element.
- """
- return self._sizes
- def set_sizes(self, sizes, dpi=72.0):
- """
- Set the sizes of each member of the collection.
- Parameters
- ----------
- sizes : `numpy.ndarray` or None
- The size to set for each element of the collection. The
- value is the 'area' of the element.
- dpi : float, default: 72
- The dpi of the canvas.
- """
- if sizes is None:
- self._sizes = np.array([])
- self._transforms = np.empty((0, 3, 3))
- else:
- self._sizes = np.asarray(sizes)
- self._transforms = np.zeros((len(self._sizes), 3, 3))
- scale = np.sqrt(self._sizes) * dpi / 72.0 * self._factor
- self._transforms[:, 0, 0] = scale
- self._transforms[:, 1, 1] = scale
- self._transforms[:, 2, 2] = 1.0
- self.stale = True
- @artist.allow_rasterization
- def draw(self, renderer):
- self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
- super().draw(renderer)
- class PathCollection(_CollectionWithSizes):
- r"""
- A collection of `~.path.Path`\s, as created by e.g. `~.Axes.scatter`.
- """
- def __init__(self, paths, sizes=None, **kwargs):
- """
- Parameters
- ----------
- paths : list of `.path.Path`
- The paths that will make up the `.Collection`.
- sizes : array-like
- The factor by which to scale each drawn `~.path.Path`. One unit
- squared in the Path's data space is scaled to be ``sizes**2``
- points when rendered.
- **kwargs
- Forwarded to `.Collection`.
- """
- super().__init__(**kwargs)
- self.set_paths(paths)
- self.set_sizes(sizes)
- self.stale = True
- def get_paths(self):
- return self._paths
- def legend_elements(self, prop="colors", num="auto",
- fmt=None, func=lambda x: x, **kwargs):
- """
- Create legend handles and labels for a PathCollection.
- Each legend handle is a `.Line2D` representing the Path that was drawn,
- and each label is a string that represents the Path.
- This is useful for obtaining a legend for a `~.Axes.scatter` plot;
- e.g.::
- scatter = plt.scatter([1, 2, 3], [4, 5, 6], c=[7, 2, 3], num=None)
- plt.legend(*scatter.legend_elements())
- creates three legend elements, one for each color with the numerical
- values passed to *c* as the labels.
- Also see the :ref:`automatedlegendcreation` example.
- Parameters
- ----------
- prop : {"colors", "sizes"}, default: "colors"
- If "colors", the legend handles will show the different colors of
- the collection. If "sizes", the legend will show the different
- sizes. To set both, use *kwargs* to directly edit the `.Line2D`
- properties.
- num : int, None, "auto" (default), array-like, or `~.ticker.Locator`
- Target number of elements to create.
- If None, use all unique elements of the mappable array. If an
- integer, target to use *num* elements in the normed range.
- If *"auto"*, try to determine which option better suits the nature
- of the data.
- The number of created elements may slightly deviate from *num* due
- to a `~.ticker.Locator` being used to find useful locations.
- If a list or array, use exactly those elements for the legend.
- Finally, a `~.ticker.Locator` can be provided.
- fmt : str, `~matplotlib.ticker.Formatter`, or None (default)
- The format or formatter to use for the labels. If a string must be
- a valid input for a `.StrMethodFormatter`. If None (the default),
- use a `.ScalarFormatter`.
- func : function, default: ``lambda x: x``
- Function to calculate the labels. Often the size (or color)
- argument to `~.Axes.scatter` will have been pre-processed by the
- user using a function ``s = f(x)`` to make the markers visible;
- e.g. ``size = np.log10(x)``. Providing the inverse of this
- function here allows that pre-processing to be inverted, so that
- the legend labels have the correct values; e.g. ``func = lambda
- x: 10**x``.
- **kwargs
- Allowed keyword arguments are *color* and *size*. E.g. it may be
- useful to set the color of the markers if *prop="sizes"* is used;
- similarly to set the size of the markers if *prop="colors"* is
- used. Any further parameters are passed onto the `.Line2D`
- instance. This may be useful to e.g. specify a different
- *markeredgecolor* or *alpha* for the legend handles.
- Returns
- -------
- handles : list of `.Line2D`
- Visual representation of each element of the legend.
- labels : list of str
- The string labels for elements of the legend.
- """
- handles = []
- labels = []
- hasarray = self.get_array() is not None
- if fmt is None:
- fmt = mpl.ticker.ScalarFormatter(useOffset=False, useMathText=True)
- elif isinstance(fmt, str):
- fmt = mpl.ticker.StrMethodFormatter(fmt)
- fmt.create_dummy_axis()
- if prop == "colors":
- if not hasarray:
- warnings.warn("Collection without array used. Make sure to "
- "specify the values to be colormapped via the "
- "`c` argument.")
- return handles, labels
- u = np.unique(self.get_array())
- size = kwargs.pop("size", mpl.rcParams["lines.markersize"])
- elif prop == "sizes":
- u = np.unique(self.get_sizes())
- color = kwargs.pop("color", "k")
- else:
- raise ValueError("Valid values for `prop` are 'colors' or "
- f"'sizes'. You supplied '{prop}' instead.")
- fu = func(u)
- fmt.axis.set_view_interval(fu.min(), fu.max())
- fmt.axis.set_data_interval(fu.min(), fu.max())
- if num == "auto":
- num = 9
- if len(u) <= num:
- num = None
- if num is None:
- values = u
- label_values = func(values)
- else:
- if prop == "colors":
- arr = self.get_array()
- elif prop == "sizes":
- arr = self.get_sizes()
- if isinstance(num, mpl.ticker.Locator):
- loc = num
- elif np.iterable(num):
- loc = mpl.ticker.FixedLocator(num)
- else:
- num = int(num)
- loc = mpl.ticker.MaxNLocator(nbins=num, min_n_ticks=num-1,
- steps=[1, 2, 2.5, 3, 5, 6, 8, 10])
- label_values = loc.tick_values(func(arr).min(), func(arr).max())
- cond = ((label_values >= func(arr).min()) &
- (label_values <= func(arr).max()))
- label_values = label_values[cond]
- yarr = np.linspace(arr.min(), arr.max(), 256)
- xarr = func(yarr)
- ix = np.argsort(xarr)
- values = np.interp(label_values, xarr[ix], yarr[ix])
- kw = {"markeredgewidth": self.get_linewidths()[0],
- "alpha": self.get_alpha(),
- **kwargs}
- for val, lab in zip(values, label_values):
- if prop == "colors":
- color = self.cmap(self.norm(val))
- elif prop == "sizes":
- size = np.sqrt(val)
- if np.isclose(size, 0.0):
- continue
- h = mlines.Line2D([0], [0], ls="", color=color, ms=size,
- marker=self.get_paths()[0], **kw)
- handles.append(h)
- if hasattr(fmt, "set_locs"):
- fmt.set_locs(label_values)
- l = fmt(lab)
- labels.append(l)
- return handles, labels
- class PolyCollection(_CollectionWithSizes):
- def __init__(self, verts, sizes=None, *, closed=True, **kwargs):
- """
- Parameters
- ----------
- verts : list of array-like
- The sequence of polygons [*verts0*, *verts1*, ...] where each
- element *verts_i* defines the vertices of polygon *i* as a 2D
- array-like of shape (M, 2).
- sizes : array-like, default: None
- Squared scaling factors for the polygons. The coordinates of each
- polygon *verts_i* are multiplied by the square-root of the
- corresponding entry in *sizes* (i.e., *sizes* specify the scaling
- of areas). The scaling is applied before the Artist master
- transform.
- closed : bool, default: True
- Whether the polygon should be closed by adding a CLOSEPOLY
- connection at the end.
- **kwargs
- Forwarded to `.Collection`.
- """
- super().__init__(**kwargs)
- self.set_sizes(sizes)
- self.set_verts(verts, closed)
- self.stale = True
- def set_verts(self, verts, closed=True):
- """
- Set the vertices of the polygons.
- Parameters
- ----------
- verts : list of array-like
- The sequence of polygons [*verts0*, *verts1*, ...] where each
- element *verts_i* defines the vertices of polygon *i* as a 2D
- array-like of shape (M, 2).
- closed : bool, default: True
- Whether the polygon should be closed by adding a CLOSEPOLY
- connection at the end.
- """
- self.stale = True
- if isinstance(verts, np.ma.MaskedArray):
- verts = verts.astype(float).filled(np.nan)
- # No need to do anything fancy if the path isn't closed.
- if not closed:
- self._paths = [mpath.Path(xy) for xy in verts]
- return
- # Fast path for arrays
- if isinstance(verts, np.ndarray) and len(verts.shape) == 3:
- verts_pad = np.concatenate((verts, verts[:, :1]), axis=1)
- # Creating the codes once is much faster than having Path do it
- # separately each time by passing closed=True.
- codes = np.empty(verts_pad.shape[1], dtype=mpath.Path.code_type)
- codes[:] = mpath.Path.LINETO
- codes[0] = mpath.Path.MOVETO
- codes[-1] = mpath.Path.CLOSEPOLY
- self._paths = [mpath.Path(xy, codes) for xy in verts_pad]
- return
- self._paths = []
- for xy in verts:
- if len(xy):
- self._paths.append(mpath.Path._create_closed(xy))
- else:
- self._paths.append(mpath.Path(xy))
- set_paths = set_verts
- def set_verts_and_codes(self, verts, codes):
- """Initialize vertices with path codes."""
- if len(verts) != len(codes):
- raise ValueError("'codes' must be a 1D list or array "
- "with the same length of 'verts'")
- self._paths = [mpath.Path(xy, cds) if len(xy) else mpath.Path(xy)
- for xy, cds in zip(verts, codes)]
- self.stale = True
- class FillBetweenPolyCollection(PolyCollection):
- """
- `.PolyCollection` that fills the area between two x- or y-curves.
- """
- def __init__(
- self, t_direction, t, f1, f2, *,
- where=None, interpolate=False, step=None, **kwargs):
- """
- Parameters
- ----------
- t_direction : {{'x', 'y'}}
- The axes on which the variable lies.
- - 'x': the curves are ``(t, f1)`` and ``(t, f2)``.
- - 'y': the curves are ``(f1, t)`` and ``(f2, t)``.
- t : array-like
- The ``t_direction`` coordinates of the nodes defining the curves.
- f1 : array-like or float
- The other coordinates of the nodes defining the first curve.
- f2 : array-like or float
- The other coordinates of the nodes defining the second curve.
- where : array-like of bool, optional
- Define *where* to exclude some {dir} regions from being filled.
- The filled regions are defined by the coordinates ``t[where]``.
- More precisely, fill between ``t[i]`` and ``t[i+1]`` if
- ``where[i] and where[i+1]``. Note that this definition implies
- that an isolated *True* value between two *False* values in *where*
- will not result in filling. Both sides of the *True* position
- remain unfilled due to the adjacent *False* values.
- interpolate : bool, default: False
- This option is only relevant if *where* is used and the two curves
- are crossing each other.
- Semantically, *where* is often used for *f1* > *f2* or
- similar. By default, the nodes of the polygon defining the filled
- region will only be placed at the positions in the *t* array.
- Such a polygon cannot describe the above semantics close to the
- intersection. The t-sections containing the intersection are
- simply clipped.
- Setting *interpolate* to *True* will calculate the actual
- intersection point and extend the filled region up to this point.
- step : {{'pre', 'post', 'mid'}}, optional
- Define *step* if the filling should be a step function,
- i.e. constant in between *t*. The value determines where the
- step will occur:
- - 'pre': The f value is continued constantly to the left from
- every *t* position, i.e. the interval ``(t[i-1], t[i]]`` has the
- value ``f[i]``.
- - 'post': The y value is continued constantly to the right from
- every *x* position, i.e. the interval ``[t[i], t[i+1])`` has the
- value ``f[i]``.
- - 'mid': Steps occur half-way between the *t* positions.
- **kwargs
- Forwarded to `.PolyCollection`.
- See Also
- --------
- .Axes.fill_between, .Axes.fill_betweenx
- """
- self.t_direction = t_direction
- self._interpolate = interpolate
- self._step = step
- verts = self._make_verts(t, f1, f2, where)
- super().__init__(verts, **kwargs)
- @staticmethod
- def _f_dir_from_t(t_direction):
- """The direction that is other than `t_direction`."""
- if t_direction == "x":
- return "y"
- elif t_direction == "y":
- return "x"
- else:
- msg = f"t_direction must be 'x' or 'y', got {t_direction!r}"
- raise ValueError(msg)
- @property
- def _f_direction(self):
- """The direction that is other than `self.t_direction`."""
- return self._f_dir_from_t(self.t_direction)
- def set_data(self, t, f1, f2, *, where=None):
- """
- Set new values for the two bounding curves.
- Parameters
- ----------
- t : array-like
- The ``self.t_direction`` coordinates of the nodes defining the curves.
- f1 : array-like or float
- The other coordinates of the nodes defining the first curve.
- f2 : array-like or float
- The other coordinates of the nodes defining the second curve.
- where : array-like of bool, optional
- Define *where* to exclude some {dir} regions from being filled.
- The filled regions are defined by the coordinates ``t[where]``.
- More precisely, fill between ``t[i]`` and ``t[i+1]`` if
- ``where[i] and where[i+1]``. Note that this definition implies
- that an isolated *True* value between two *False* values in *where*
- will not result in filling. Both sides of the *True* position
- remain unfilled due to the adjacent *False* values.
- See Also
- --------
- .PolyCollection.set_verts, .Line2D.set_data
- """
- t, f1, f2 = self.axes._fill_between_process_units(
- self.t_direction, self._f_direction, t, f1, f2)
- verts = self._make_verts(t, f1, f2, where)
- self.set_verts(verts)
- def get_datalim(self, transData):
- """Calculate the data limits and return them as a `.Bbox`."""
- datalim = transforms.Bbox.null()
- datalim.update_from_data_xy((self.get_transform() - transData).transform(
- np.concatenate([self._bbox, [self._bbox.minpos]])))
- return datalim
- def _make_verts(self, t, f1, f2, where):
- """
- Make verts that can be forwarded to `.PolyCollection`.
- """
- self._validate_shapes(self.t_direction, self._f_direction, t, f1, f2)
- where = self._get_data_mask(t, f1, f2, where)
- t, f1, f2 = np.broadcast_arrays(np.atleast_1d(t), f1, f2, subok=True)
- self._bbox = transforms.Bbox.null()
- self._bbox.update_from_data_xy(self._fix_pts_xy_order(np.concatenate([
- np.stack((t[where], f[where]), axis=-1) for f in (f1, f2)])))
- return [
- self._make_verts_for_region(t, f1, f2, idx0, idx1)
- for idx0, idx1 in cbook.contiguous_regions(where)
- ]
- def _get_data_mask(self, t, f1, f2, where):
- """
- Return a bool array, with True at all points that should eventually be rendered.
- The array is True at a point if none of the data inputs
- *t*, *f1*, *f2* is masked and if the input *where* is true at that point.
- """
- if where is None:
- where = True
- else:
- where = np.asarray(where, dtype=bool)
- if where.size != t.size:
- msg = "where size ({}) does not match {!r} size ({})".format(
- where.size, self.t_direction, t.size)
- raise ValueError(msg)
- return where & ~functools.reduce(
- np.logical_or, map(np.ma.getmaskarray, [t, f1, f2]))
- @staticmethod
- def _validate_shapes(t_dir, f_dir, t, f1, f2):
- """Validate that t, f1 and f2 are 1-dimensional and have the same length."""
- names = (d + s for d, s in zip((t_dir, f_dir, f_dir), ("", "1", "2")))
- for name, array in zip(names, [t, f1, f2]):
- if array.ndim > 1:
- raise ValueError(f"{name!r} is not 1-dimensional")
- if t.size > 1 and array.size > 1 and t.size != array.size:
- msg = "{!r} has size {}, but {!r} has an unequal size of {}".format(
- t_dir, t.size, name, array.size)
- raise ValueError(msg)
- def _make_verts_for_region(self, t, f1, f2, idx0, idx1):
- """
- Make ``verts`` for a contiguous region between ``idx0`` and ``idx1``, taking
- into account ``step`` and ``interpolate``.
- """
- t_slice = t[idx0:idx1]
- f1_slice = f1[idx0:idx1]
- f2_slice = f2[idx0:idx1]
- if self._step is not None:
- step_func = cbook.STEP_LOOKUP_MAP["steps-" + self._step]
- t_slice, f1_slice, f2_slice = step_func(t_slice, f1_slice, f2_slice)
- if self._interpolate:
- start = self._get_interpolating_points(t, f1, f2, idx0)
- end = self._get_interpolating_points(t, f1, f2, idx1)
- else:
- # Handle scalar f2 (e.g. 0): the fill should go all
- # the way down to 0 even if none of the dep1 sample points do.
- start = t_slice[0], f2_slice[0]
- end = t_slice[-1], f2_slice[-1]
- pts = np.concatenate((
- np.asarray([start]),
- np.stack((t_slice, f1_slice), axis=-1),
- np.asarray([end]),
- np.stack((t_slice, f2_slice), axis=-1)[::-1]))
- return self._fix_pts_xy_order(pts)
- @classmethod
- def _get_interpolating_points(cls, t, f1, f2, idx):
- """Calculate interpolating points."""
- im1 = max(idx - 1, 0)
- t_values = t[im1:idx+1]
- diff_values = f1[im1:idx+1] - f2[im1:idx+1]
- f1_values = f1[im1:idx+1]
- if len(diff_values) == 2:
- if np.ma.is_masked(diff_values[1]):
- return t[im1], f1[im1]
- elif np.ma.is_masked(diff_values[0]):
- return t[idx], f1[idx]
- diff_root_t = cls._get_diff_root(0, diff_values, t_values)
- diff_root_f = cls._get_diff_root(diff_root_t, t_values, f1_values)
- return diff_root_t, diff_root_f
- @staticmethod
- def _get_diff_root(x, xp, fp):
- """Calculate diff root."""
- order = xp.argsort()
- return np.interp(x, xp[order], fp[order])
- def _fix_pts_xy_order(self, pts):
- """
- Fix pts calculation results with `self.t_direction`.
- In the workflow, it is assumed that `self.t_direction` is 'x'. If this
- is not true, we need to exchange the coordinates.
- """
- return pts[:, ::-1] if self.t_direction == "y" else pts
- class RegularPolyCollection(_CollectionWithSizes):
- """A collection of n-sided regular polygons."""
- _path_generator = mpath.Path.unit_regular_polygon
- _factor = np.pi ** (-1/2)
- def __init__(self,
- numsides,
- *,
- rotation=0,
- sizes=(1,),
- **kwargs):
- """
- Parameters
- ----------
- numsides : int
- The number of sides of the polygon.
- rotation : float
- The rotation of the polygon in radians.
- sizes : tuple of float
- The area of the circle circumscribing the polygon in points^2.
- **kwargs
- Forwarded to `.Collection`.
- Examples
- --------
- See :doc:`/gallery/event_handling/lasso_demo` for a complete example::
- offsets = np.random.rand(20, 2)
- facecolors = [cm.jet(x) for x in np.random.rand(20)]
- collection = RegularPolyCollection(
- numsides=5, # a pentagon
- rotation=0, sizes=(50,),
- facecolors=facecolors,
- edgecolors=("black",),
- linewidths=(1,),
- offsets=offsets,
- offset_transform=ax.transData,
- )
- """
- super().__init__(**kwargs)
- self.set_sizes(sizes)
- self._numsides = numsides
- self._paths = [self._path_generator(numsides)]
- self._rotation = rotation
- self.set_transform(transforms.IdentityTransform())
- def get_numsides(self):
- return self._numsides
- def get_rotation(self):
- return self._rotation
- @artist.allow_rasterization
- def draw(self, renderer):
- self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
- self._transforms = [
- transforms.Affine2D(x).rotate(-self._rotation).get_matrix()
- for x in self._transforms
- ]
- # Explicitly not super().draw, because set_sizes must be called before
- # updating self._transforms.
- Collection.draw(self, renderer)
- class StarPolygonCollection(RegularPolyCollection):
- """Draw a collection of regular stars with *numsides* points."""
- _path_generator = mpath.Path.unit_regular_star
- class AsteriskPolygonCollection(RegularPolyCollection):
- """Draw a collection of regular asterisks with *numsides* points."""
- _path_generator = mpath.Path.unit_regular_asterisk
- class LineCollection(Collection):
- r"""
- Represents a sequence of `.Line2D`\s that should be drawn together.
- This class extends `.Collection` to represent a sequence of
- `.Line2D`\s instead of just a sequence of `.Patch`\s.
- Just as in `.Collection`, each property of a *LineCollection* may be either
- a single value or a list of values. This list is then used cyclically for
- each element of the LineCollection, so the property of the ``i``\th element
- of the collection is::
- prop[i % len(prop)]
- The properties of each member of a *LineCollection* default to their values
- in :rc:`lines.*` instead of :rc:`patch.*`, and the property *colors* is
- added in place of *edgecolors*.
- """
- _edge_default = True
- def __init__(self, segments, # Can be None.
- *,
- zorder=2, # Collection.zorder is 1
- **kwargs
- ):
- """
- Parameters
- ----------
- segments : list of (N, 2) array-like
- A sequence ``[line0, line1, ...]`` where each line is a (N, 2)-shape
- array-like containing points::
- line0 = [(x0, y0), (x1, y1), ...]
- Each line can contain a different number of points.
- linewidths : float or list of float, default: :rc:`lines.linewidth`
- The width of each line in points.
- colors : :mpltype:`color` or list of color, default: :rc:`lines.color`
- A sequence of RGBA tuples (e.g., arbitrary color strings, etc, not
- allowed).
- antialiaseds : bool or list of bool, default: :rc:`lines.antialiased`
- Whether to use antialiasing for each line.
- zorder : float, default: 2
- zorder of the lines once drawn.
- facecolors : :mpltype:`color` or list of :mpltype:`color`, default: 'none'
- When setting *facecolors*, each line is interpreted as a boundary
- for an area, implicitly closing the path from the last point to the
- first point. The enclosed area is filled with *facecolor*.
- In order to manually specify what should count as the "interior" of
- each line, please use `.PathCollection` instead, where the
- "interior" can be specified by appropriate usage of
- `~.path.Path.CLOSEPOLY`.
- **kwargs
- Forwarded to `.Collection`.
- """
- # Unfortunately, mplot3d needs this explicit setting of 'facecolors'.
- kwargs.setdefault('facecolors', 'none')
- super().__init__(
- zorder=zorder,
- **kwargs)
- self.set_segments(segments)
- def set_segments(self, segments):
- if segments is None:
- return
- self._paths = [mpath.Path(seg) if isinstance(seg, np.ma.MaskedArray)
- else mpath.Path(np.asarray(seg, float))
- for seg in segments]
- self.stale = True
- set_verts = set_segments # for compatibility with PolyCollection
- set_paths = set_segments
- def get_segments(self):
- """
- Returns
- -------
- list
- List of segments in the LineCollection. Each list item contains an
- array of vertices.
- """
- segments = []
- for path in self._paths:
- vertices = [
- vertex
- for vertex, _
- # Never simplify here, we want to get the data-space values
- # back and there in no way to know the "right" simplification
- # threshold so never try.
- in path.iter_segments(simplify=False)
- ]
- vertices = np.asarray(vertices)
- segments.append(vertices)
- return segments
- def _get_default_linewidth(self):
- return mpl.rcParams['lines.linewidth']
- def _get_default_antialiased(self):
- return mpl.rcParams['lines.antialiased']
- def _get_default_edgecolor(self):
- return mpl.rcParams['lines.color']
- def _get_default_facecolor(self):
- return 'none'
- def set_alpha(self, alpha):
- # docstring inherited
- super().set_alpha(alpha)
- if self._gapcolor is not None:
- self.set_gapcolor(self._original_gapcolor)
- def set_color(self, c):
- """
- Set the edgecolor(s) of the LineCollection.
- Parameters
- ----------
- c : :mpltype:`color` or list of :mpltype:`color`
- Single color (all lines have same color), or a
- sequence of RGBA tuples; if it is a sequence the lines will
- cycle through the sequence.
- """
- self.set_edgecolor(c)
- set_colors = set_color
- def get_color(self):
- return self._edgecolors
- get_colors = get_color # for compatibility with old versions
- def set_gapcolor(self, gapcolor):
- """
- Set a color to fill the gaps in the dashed line style.
- .. note::
- Striped lines are created by drawing two interleaved dashed lines.
- There can be overlaps between those two, which may result in
- artifacts when using transparency.
- This functionality is experimental and may change.
- Parameters
- ----------
- gapcolor : :mpltype:`color` or list of :mpltype:`color` or None
- The color with which to fill the gaps. If None, the gaps are
- unfilled.
- """
- self._original_gapcolor = gapcolor
- self._set_gapcolor(gapcolor)
- def _set_gapcolor(self, gapcolor):
- if gapcolor is not None:
- gapcolor = mcolors.to_rgba_array(gapcolor, self._alpha)
- self._gapcolor = gapcolor
- self.stale = True
- def get_gapcolor(self):
- return self._gapcolor
- def _get_inverse_paths_linestyles(self):
- """
- Returns the path and pattern for the gaps in the non-solid lines.
- This path and pattern is the inverse of the path and pattern used to
- construct the non-solid lines. For solid lines, we set the inverse path
- to nans to prevent drawing an inverse line.
- """
- path_patterns = [
- (mpath.Path(np.full((1, 2), np.nan)), ls)
- if ls == (0, None) else
- (path, mlines._get_inverse_dash_pattern(*ls))
- for (path, ls) in
- zip(self._paths, itertools.cycle(self._linestyles))]
- return zip(*path_patterns)
- class EventCollection(LineCollection):
- """
- A collection of locations along a single axis at which an "event" occurred.
- The events are given by a 1-dimensional array. They do not have an
- amplitude and are displayed as parallel lines.
- """
- _edge_default = True
- def __init__(self,
- positions, # Cannot be None.
- orientation='horizontal',
- *,
- lineoffset=0,
- linelength=1,
- linewidth=None,
- color=None,
- linestyle='solid',
- antialiased=None,
- **kwargs
- ):
- """
- Parameters
- ----------
- positions : 1D array-like
- Each value is an event.
- orientation : {'horizontal', 'vertical'}, default: 'horizontal'
- The sequence of events is plotted along this direction.
- The marker lines of the single events are along the orthogonal
- direction.
- lineoffset : float, default: 0
- The offset of the center of the markers from the origin, in the
- direction orthogonal to *orientation*.
- linelength : float, default: 1
- The total height of the marker (i.e. the marker stretches from
- ``lineoffset - linelength/2`` to ``lineoffset + linelength/2``).
- linewidth : float or list thereof, default: :rc:`lines.linewidth`
- The line width of the event lines, in points.
- color : :mpltype:`color` or list of :mpltype:`color`, default: :rc:`lines.color`
- The color of the event lines.
- linestyle : str or tuple or list thereof, default: 'solid'
- Valid strings are ['solid', 'dashed', 'dashdot', 'dotted',
- '-', '--', '-.', ':']. Dash tuples should be of the form::
- (offset, onoffseq),
- where *onoffseq* is an even length tuple of on and off ink
- in points.
- antialiased : bool or list thereof, default: :rc:`lines.antialiased`
- Whether to use antialiasing for drawing the lines.
- **kwargs
- Forwarded to `.LineCollection`.
- Examples
- --------
- .. plot:: gallery/lines_bars_and_markers/eventcollection_demo.py
- """
- super().__init__([],
- linewidths=linewidth, linestyles=linestyle,
- colors=color, antialiaseds=antialiased,
- **kwargs)
- self._is_horizontal = True # Initial value, may be switched below.
- self._linelength = linelength
- self._lineoffset = lineoffset
- self.set_orientation(orientation)
- self.set_positions(positions)
- def get_positions(self):
- """
- Return an array containing the floating-point values of the positions.
- """
- pos = 0 if self.is_horizontal() else 1
- return [segment[0, pos] for segment in self.get_segments()]
- def set_positions(self, positions):
- """Set the positions of the events."""
- if positions is None:
- positions = []
- if np.ndim(positions) != 1:
- raise ValueError('positions must be one-dimensional')
- lineoffset = self.get_lineoffset()
- linelength = self.get_linelength()
- pos_idx = 0 if self.is_horizontal() else 1
- segments = np.empty((len(positions), 2, 2))
- segments[:, :, pos_idx] = np.sort(positions)[:, None]
- segments[:, 0, 1 - pos_idx] = lineoffset + linelength / 2
- segments[:, 1, 1 - pos_idx] = lineoffset - linelength / 2
- self.set_segments(segments)
- def add_positions(self, position):
- """Add one or more events at the specified positions."""
- if position is None or (hasattr(position, 'len') and
- len(position) == 0):
- return
- positions = self.get_positions()
- positions = np.hstack([positions, np.asanyarray(position)])
- self.set_positions(positions)
- extend_positions = append_positions = add_positions
- def is_horizontal(self):
- """True if the eventcollection is horizontal, False if vertical."""
- return self._is_horizontal
- def get_orientation(self):
- """
- Return the orientation of the event line ('horizontal' or 'vertical').
- """
- return 'horizontal' if self.is_horizontal() else 'vertical'
- def switch_orientation(self):
- """
- Switch the orientation of the event line, either from vertical to
- horizontal or vice versus.
- """
- segments = self.get_segments()
- for i, segment in enumerate(segments):
- segments[i] = np.fliplr(segment)
- self.set_segments(segments)
- self._is_horizontal = not self.is_horizontal()
- self.stale = True
- def set_orientation(self, orientation):
- """
- Set the orientation of the event line.
- Parameters
- ----------
- orientation : {'horizontal', 'vertical'}
- """
- is_horizontal = _api.check_getitem(
- {"horizontal": True, "vertical": False},
- orientation=orientation)
- if is_horizontal == self.is_horizontal():
- return
- self.switch_orientation()
- def get_linelength(self):
- """Return the length of the lines used to mark each event."""
- return self._linelength
- def set_linelength(self, linelength):
- """Set the length of the lines used to mark each event."""
- if linelength == self.get_linelength():
- return
- lineoffset = self.get_lineoffset()
- segments = self.get_segments()
- pos = 1 if self.is_horizontal() else 0
- for segment in segments:
- segment[0, pos] = lineoffset + linelength / 2.
- segment[1, pos] = lineoffset - linelength / 2.
- self.set_segments(segments)
- self._linelength = linelength
- def get_lineoffset(self):
- """Return the offset of the lines used to mark each event."""
- return self._lineoffset
- def set_lineoffset(self, lineoffset):
- """Set the offset of the lines used to mark each event."""
- if lineoffset == self.get_lineoffset():
- return
- linelength = self.get_linelength()
- segments = self.get_segments()
- pos = 1 if self.is_horizontal() else 0
- for segment in segments:
- segment[0, pos] = lineoffset + linelength / 2.
- segment[1, pos] = lineoffset - linelength / 2.
- self.set_segments(segments)
- self._lineoffset = lineoffset
- def get_linewidth(self):
- """Get the width of the lines used to mark each event."""
- return super().get_linewidth()[0]
- def get_linewidths(self):
- return super().get_linewidth()
- def get_color(self):
- """Return the color of the lines used to mark each event."""
- return self.get_colors()[0]
- class CircleCollection(_CollectionWithSizes):
- """A collection of circles, drawn using splines."""
- _factor = np.pi ** (-1/2)
- def __init__(self, sizes, **kwargs):
- """
- Parameters
- ----------
- sizes : float or array-like
- The area of each circle in points^2.
- **kwargs
- Forwarded to `.Collection`.
- """
- super().__init__(**kwargs)
- self.set_sizes(sizes)
- self.set_transform(transforms.IdentityTransform())
- self._paths = [mpath.Path.unit_circle()]
- class EllipseCollection(Collection):
- """A collection of ellipses, drawn using splines."""
- def __init__(self, widths, heights, angles, *, units='points', **kwargs):
- """
- Parameters
- ----------
- widths : array-like
- The lengths of the first axes (e.g., major axis lengths).
- heights : array-like
- The lengths of second axes.
- angles : array-like
- The angles of the first axes, degrees CCW from the x-axis.
- units : {'points', 'inches', 'dots', 'width', 'height', 'x', 'y', 'xy'}
- The units in which majors and minors are given; 'width' and
- 'height' refer to the dimensions of the axes, while 'x' and 'y'
- refer to the *offsets* data units. 'xy' differs from all others in
- that the angle as plotted varies with the aspect ratio, and equals
- the specified angle only when the aspect ratio is unity. Hence
- it behaves the same as the `~.patches.Ellipse` with
- ``axes.transData`` as its transform.
- **kwargs
- Forwarded to `Collection`.
- """
- super().__init__(**kwargs)
- self.set_widths(widths)
- self.set_heights(heights)
- self.set_angles(angles)
- self._units = units
- self.set_transform(transforms.IdentityTransform())
- self._transforms = np.empty((0, 3, 3))
- self._paths = [mpath.Path.unit_circle()]
- def _set_transforms(self):
- """Calculate transforms immediately before drawing."""
- ax = self.axes
- fig = self.get_figure(root=False)
- if self._units == 'xy':
- sc = 1
- elif self._units == 'x':
- sc = ax.bbox.width / ax.viewLim.width
- elif self._units == 'y':
- sc = ax.bbox.height / ax.viewLim.height
- elif self._units == 'inches':
- sc = fig.dpi
- elif self._units == 'points':
- sc = fig.dpi / 72.0
- elif self._units == 'width':
- sc = ax.bbox.width
- elif self._units == 'height':
- sc = ax.bbox.height
- elif self._units == 'dots':
- sc = 1.0
- else:
- raise ValueError(f'Unrecognized units: {self._units!r}')
- self._transforms = np.zeros((len(self._widths), 3, 3))
- widths = self._widths * sc
- heights = self._heights * sc
- sin_angle = np.sin(self._angles)
- cos_angle = np.cos(self._angles)
- self._transforms[:, 0, 0] = widths * cos_angle
- self._transforms[:, 0, 1] = heights * -sin_angle
- self._transforms[:, 1, 0] = widths * sin_angle
- self._transforms[:, 1, 1] = heights * cos_angle
- self._transforms[:, 2, 2] = 1.0
- _affine = transforms.Affine2D
- if self._units == 'xy':
- m = ax.transData.get_affine().get_matrix().copy()
- m[:2, 2:] = 0
- self.set_transform(_affine(m))
- def set_widths(self, widths):
- """Set the lengths of the first axes (e.g., major axis)."""
- self._widths = 0.5 * np.asarray(widths).ravel()
- self.stale = True
- def set_heights(self, heights):
- """Set the lengths of second axes (e.g., minor axes)."""
- self._heights = 0.5 * np.asarray(heights).ravel()
- self.stale = True
- def set_angles(self, angles):
- """Set the angles of the first axes, degrees CCW from the x-axis."""
- self._angles = np.deg2rad(angles).ravel()
- self.stale = True
- def get_widths(self):
- """Get the lengths of the first axes (e.g., major axis)."""
- return self._widths * 2
- def get_heights(self):
- """Set the lengths of second axes (e.g., minor axes)."""
- return self._heights * 2
- def get_angles(self):
- """Get the angles of the first axes, degrees CCW from the x-axis."""
- return np.rad2deg(self._angles)
- @artist.allow_rasterization
- def draw(self, renderer):
- self._set_transforms()
- super().draw(renderer)
- class PatchCollection(Collection):
- """
- A generic collection of patches.
- PatchCollection draws faster than a large number of equivalent individual
- Patches. It also makes it easier to assign a colormap to a heterogeneous
- collection of patches.
- """
- def __init__(self, patches, *, match_original=False, **kwargs):
- """
- Parameters
- ----------
- patches : list of `.Patch`
- A sequence of Patch objects. This list may include
- a heterogeneous assortment of different patch types.
- match_original : bool, default: False
- If True, use the colors and linewidths of the original
- patches. If False, new colors may be assigned by
- providing the standard collection arguments, facecolor,
- edgecolor, linewidths, norm or cmap.
- **kwargs
- All other parameters are forwarded to `.Collection`.
- If any of *edgecolors*, *facecolors*, *linewidths*, *antialiaseds*
- are None, they default to their `.rcParams` patch setting, in
- sequence form.
- Notes
- -----
- The use of `~matplotlib.cm.ScalarMappable` functionality is optional.
- If the `~matplotlib.cm.ScalarMappable` matrix ``_A`` has been set (via
- a call to `~.ScalarMappable.set_array`), at draw time a call to scalar
- mappable will be made to set the face colors.
- """
- if match_original:
- def determine_facecolor(patch):
- if patch.get_fill():
- return patch.get_facecolor()
- return [0, 0, 0, 0]
- kwargs['facecolors'] = [determine_facecolor(p) for p in patches]
- kwargs['edgecolors'] = [p.get_edgecolor() for p in patches]
- kwargs['linewidths'] = [p.get_linewidth() for p in patches]
- kwargs['linestyles'] = [p.get_linestyle() for p in patches]
- kwargs['antialiaseds'] = [p.get_antialiased() for p in patches]
- super().__init__(**kwargs)
- self.set_paths(patches)
- def set_paths(self, patches):
- paths = [p.get_transform().transform_path(p.get_path())
- for p in patches]
- self._paths = paths
- class TriMesh(Collection):
- """
- Class for the efficient drawing of a triangular mesh using Gouraud shading.
- A triangular mesh is a `~matplotlib.tri.Triangulation` object.
- """
- def __init__(self, triangulation, **kwargs):
- super().__init__(**kwargs)
- self._triangulation = triangulation
- self._shading = 'gouraud'
- self._bbox = transforms.Bbox.unit()
- # Unfortunately this requires a copy, unless Triangulation
- # was rewritten.
- xy = np.hstack((triangulation.x.reshape(-1, 1),
- triangulation.y.reshape(-1, 1)))
- self._bbox.update_from_data_xy(xy)
- def get_paths(self):
- if self._paths is None:
- self.set_paths()
- return self._paths
- def set_paths(self):
- self._paths = self.convert_mesh_to_paths(self._triangulation)
- @staticmethod
- def convert_mesh_to_paths(tri):
- """
- Convert a given mesh into a sequence of `.Path` objects.
- This function is primarily of use to implementers of backends that do
- not directly support meshes.
- """
- triangles = tri.get_masked_triangles()
- verts = np.stack((tri.x[triangles], tri.y[triangles]), axis=-1)
- return [mpath.Path(x) for x in verts]
- @artist.allow_rasterization
- def draw(self, renderer):
- if not self.get_visible():
- return
- renderer.open_group(self.__class__.__name__, gid=self.get_gid())
- transform = self.get_transform()
- # Get a list of triangles and the color at each vertex.
- tri = self._triangulation
- triangles = tri.get_masked_triangles()
- verts = np.stack((tri.x[triangles], tri.y[triangles]), axis=-1)
- self.update_scalarmappable()
- colors = self._facecolors[triangles]
- gc = renderer.new_gc()
- self._set_gc_clip(gc)
- gc.set_linewidth(self.get_linewidth()[0])
- renderer.draw_gouraud_triangles(gc, verts, colors, transform.frozen())
- gc.restore()
- renderer.close_group(self.__class__.__name__)
- class _MeshData:
- r"""
- Class for managing the two dimensional coordinates of Quadrilateral meshes
- and the associated data with them. This class is a mixin and is intended to
- be used with another collection that will implement the draw separately.
- A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are
- defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is
- defined by the vertices ::
- (m+1, n) ----------- (m+1, n+1)
- / /
- / /
- / /
- (m, n) -------- (m, n+1)
- The mesh need not be regular and the polygons need not be convex.
- Parameters
- ----------
- coordinates : (M+1, N+1, 2) array-like
- The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates
- of vertex (m, n).
- shading : {'flat', 'gouraud'}, default: 'flat'
- """
- def __init__(self, coordinates, *, shading='flat'):
- _api.check_shape((None, None, 2), coordinates=coordinates)
- self._coordinates = coordinates
- self._shading = shading
- def set_array(self, A):
- """
- Set the data values.
- Parameters
- ----------
- A : array-like
- The mesh data. Supported array shapes are:
- - (M, N) or (M*N,): a mesh with scalar data. The values are mapped
- to colors using normalization and a colormap. See parameters
- *norm*, *cmap*, *vmin*, *vmax*.
- - (M, N, 3): an image with RGB values (0-1 float or 0-255 int).
- - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int),
- i.e. including transparency.
- If the values are provided as a 2D grid, the shape must match the
- coordinates grid. If the values are 1D, they are reshaped to 2D.
- M, N follow from the coordinates grid, where the coordinates grid
- shape is (M, N) for 'gouraud' *shading* and (M+1, N+1) for 'flat'
- shading.
- """
- height, width = self._coordinates.shape[0:-1]
- if self._shading == 'flat':
- h, w = height - 1, width - 1
- else:
- h, w = height, width
- ok_shapes = [(h, w, 3), (h, w, 4), (h, w), (h * w,)]
- if A is not None:
- shape = np.shape(A)
- if shape not in ok_shapes:
- raise ValueError(
- f"For X ({width}) and Y ({height}) with {self._shading} "
- f"shading, A should have shape "
- f"{' or '.join(map(str, ok_shapes))}, not {A.shape}")
- return super().set_array(A)
- def get_coordinates(self):
- """
- Return the vertices of the mesh as an (M+1, N+1, 2) array.
- M, N are the number of quadrilaterals in the rows / columns of the
- mesh, corresponding to (M+1, N+1) vertices.
- The last dimension specifies the components (x, y).
- """
- return self._coordinates
- def get_edgecolor(self):
- # docstring inherited
- # Note that we want to return an array of shape (N*M, 4)
- # a flattened RGBA collection
- return super().get_edgecolor().reshape(-1, 4)
- def get_facecolor(self):
- # docstring inherited
- # Note that we want to return an array of shape (N*M, 4)
- # a flattened RGBA collection
- return super().get_facecolor().reshape(-1, 4)
- @staticmethod
- def _convert_mesh_to_paths(coordinates):
- """
- Convert a given mesh into a sequence of `.Path` objects.
- This function is primarily of use to implementers of backends that do
- not directly support quadmeshes.
- """
- if isinstance(coordinates, np.ma.MaskedArray):
- c = coordinates.data
- else:
- c = coordinates
- points = np.concatenate([
- c[:-1, :-1],
- c[:-1, 1:],
- c[1:, 1:],
- c[1:, :-1],
- c[:-1, :-1]
- ], axis=2).reshape((-1, 5, 2))
- return [mpath.Path(x) for x in points]
- def _convert_mesh_to_triangles(self, coordinates):
- """
- Convert a given mesh into a sequence of triangles, each point
- with its own color. The result can be used to construct a call to
- `~.RendererBase.draw_gouraud_triangles`.
- """
- if isinstance(coordinates, np.ma.MaskedArray):
- p = coordinates.data
- else:
- p = coordinates
- p_a = p[:-1, :-1]
- p_b = p[:-1, 1:]
- p_c = p[1:, 1:]
- p_d = p[1:, :-1]
- p_center = (p_a + p_b + p_c + p_d) / 4.0
- triangles = np.concatenate([
- p_a, p_b, p_center,
- p_b, p_c, p_center,
- p_c, p_d, p_center,
- p_d, p_a, p_center,
- ], axis=2).reshape((-1, 3, 2))
- c = self.get_facecolor().reshape((*coordinates.shape[:2], 4))
- z = self.get_array()
- mask = z.mask if np.ma.is_masked(z) else None
- if mask is not None:
- c[mask, 3] = np.nan
- c_a = c[:-1, :-1]
- c_b = c[:-1, 1:]
- c_c = c[1:, 1:]
- c_d = c[1:, :-1]
- c_center = (c_a + c_b + c_c + c_d) / 4.0
- colors = np.concatenate([
- c_a, c_b, c_center,
- c_b, c_c, c_center,
- c_c, c_d, c_center,
- c_d, c_a, c_center,
- ], axis=2).reshape((-1, 3, 4))
- tmask = np.isnan(colors[..., 2, 3])
- return triangles[~tmask], colors[~tmask]
- class QuadMesh(_MeshData, Collection):
- r"""
- Class for the efficient drawing of a quadrilateral mesh.
- A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are
- defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is
- defined by the vertices ::
- (m+1, n) ----------- (m+1, n+1)
- / /
- / /
- / /
- (m, n) -------- (m, n+1)
- The mesh need not be regular and the polygons need not be convex.
- Parameters
- ----------
- coordinates : (M+1, N+1, 2) array-like
- The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates
- of vertex (m, n).
- antialiased : bool, default: True
- shading : {'flat', 'gouraud'}, default: 'flat'
- Notes
- -----
- Unlike other `.Collection`\s, the default *pickradius* of `.QuadMesh` is 0,
- i.e. `~.Artist.contains` checks whether the test point is within any of the
- mesh quadrilaterals.
- """
- def __init__(self, coordinates, *, antialiased=True, shading='flat',
- **kwargs):
- kwargs.setdefault("pickradius", 0)
- super().__init__(coordinates=coordinates, shading=shading)
- Collection.__init__(self, **kwargs)
- self._antialiased = antialiased
- self._bbox = transforms.Bbox.unit()
- self._bbox.update_from_data_xy(self._coordinates.reshape(-1, 2))
- self.set_mouseover(False)
- def get_paths(self):
- if self._paths is None:
- self.set_paths()
- return self._paths
- def set_paths(self):
- self._paths = self._convert_mesh_to_paths(self._coordinates)
- self.stale = True
- def get_datalim(self, transData):
- return (self.get_transform() - transData).transform_bbox(self._bbox)
- @artist.allow_rasterization
- def draw(self, renderer):
- if not self.get_visible():
- return
- renderer.open_group(self.__class__.__name__, self.get_gid())
- transform = self.get_transform()
- offset_trf = self.get_offset_transform()
- offsets = self.get_offsets()
- if self.have_units():
- xs = self.convert_xunits(offsets[:, 0])
- ys = self.convert_yunits(offsets[:, 1])
- offsets = np.column_stack([xs, ys])
- self.update_scalarmappable()
- if not transform.is_affine:
- coordinates = self._coordinates.reshape((-1, 2))
- coordinates = transform.transform(coordinates)
- coordinates = coordinates.reshape(self._coordinates.shape)
- transform = transforms.IdentityTransform()
- else:
- coordinates = self._coordinates
- if not offset_trf.is_affine:
- offsets = offset_trf.transform_non_affine(offsets)
- offset_trf = offset_trf.get_affine()
- gc = renderer.new_gc()
- gc.set_snap(self.get_snap())
- self._set_gc_clip(gc)
- gc.set_linewidth(self.get_linewidth()[0])
- if self._shading == 'gouraud':
- triangles, colors = self._convert_mesh_to_triangles(coordinates)
- renderer.draw_gouraud_triangles(
- gc, triangles, colors, transform.frozen())
- else:
- renderer.draw_quad_mesh(
- gc, transform.frozen(),
- coordinates.shape[1] - 1, coordinates.shape[0] - 1,
- coordinates, offsets, offset_trf,
- # Backends expect flattened rgba arrays (n*m, 4) for fc and ec
- self.get_facecolor().reshape((-1, 4)),
- self._antialiased, self.get_edgecolors().reshape((-1, 4)))
- gc.restore()
- renderer.close_group(self.__class__.__name__)
- self.stale = False
- def get_cursor_data(self, event):
- contained, info = self.contains(event)
- if contained and self.get_array() is not None:
- return self.get_array().ravel()[info["ind"]]
- return None
- class PolyQuadMesh(_MeshData, PolyCollection):
- """
- Class for drawing a quadrilateral mesh as individual Polygons.
- A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are
- defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is
- defined by the vertices ::
- (m+1, n) ----------- (m+1, n+1)
- / /
- / /
- / /
- (m, n) -------- (m, n+1)
- The mesh need not be regular and the polygons need not be convex.
- Parameters
- ----------
- coordinates : (M+1, N+1, 2) array-like
- The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates
- of vertex (m, n).
- Notes
- -----
- Unlike `.QuadMesh`, this class will draw each cell as an individual Polygon.
- This is significantly slower, but allows for more flexibility when wanting
- to add additional properties to the cells, such as hatching.
- Another difference from `.QuadMesh` is that if any of the vertices or data
- of a cell are masked, that Polygon will **not** be drawn and it won't be in
- the list of paths returned.
- """
- def __init__(self, coordinates, **kwargs):
- super().__init__(coordinates=coordinates)
- PolyCollection.__init__(self, verts=[], **kwargs)
- # Setting the verts updates the paths of the PolyCollection
- # This is called after the initializers to make sure the kwargs
- # have all been processed and available for the masking calculations
- self._set_unmasked_verts()
- def _get_unmasked_polys(self):
- """Get the unmasked regions using the coordinates and array"""
- # mask(X) | mask(Y)
- mask = np.any(np.ma.getmaskarray(self._coordinates), axis=-1)
- # We want the shape of the polygon, which is the corner of each X/Y array
- mask = (mask[0:-1, 0:-1] | mask[1:, 1:] | mask[0:-1, 1:] | mask[1:, 0:-1])
- arr = self.get_array()
- if arr is not None:
- arr = np.ma.getmaskarray(arr)
- if arr.ndim == 3:
- # RGB(A) case
- mask |= np.any(arr, axis=-1)
- elif arr.ndim == 2:
- mask |= arr
- else:
- mask |= arr.reshape(self._coordinates[:-1, :-1, :].shape[:2])
- return ~mask
- def _set_unmasked_verts(self):
- X = self._coordinates[..., 0]
- Y = self._coordinates[..., 1]
- unmask = self._get_unmasked_polys()
- X1 = np.ma.filled(X[:-1, :-1])[unmask]
- Y1 = np.ma.filled(Y[:-1, :-1])[unmask]
- X2 = np.ma.filled(X[1:, :-1])[unmask]
- Y2 = np.ma.filled(Y[1:, :-1])[unmask]
- X3 = np.ma.filled(X[1:, 1:])[unmask]
- Y3 = np.ma.filled(Y[1:, 1:])[unmask]
- X4 = np.ma.filled(X[:-1, 1:])[unmask]
- Y4 = np.ma.filled(Y[:-1, 1:])[unmask]
- npoly = len(X1)
- xy = np.ma.stack([X1, Y1, X2, Y2, X3, Y3, X4, Y4, X1, Y1], axis=-1)
- verts = xy.reshape((npoly, 5, 2))
- self.set_verts(verts)
- def get_edgecolor(self):
- # docstring inherited
- # We only want to return the facecolors of the polygons
- # that were drawn.
- ec = super().get_edgecolor()
- unmasked_polys = self._get_unmasked_polys().ravel()
- if len(ec) != len(unmasked_polys):
- # Mapping is off
- return ec
- return ec[unmasked_polys, :]
- def get_facecolor(self):
- # docstring inherited
- # We only want to return the facecolors of the polygons
- # that were drawn.
- fc = super().get_facecolor()
- unmasked_polys = self._get_unmasked_polys().ravel()
- if len(fc) != len(unmasked_polys):
- # Mapping is off
- return fc
- return fc[unmasked_polys, :]
- def set_array(self, A):
- # docstring inherited
- prev_unmask = self._get_unmasked_polys()
- super().set_array(A)
- # If the mask has changed at all we need to update
- # the set of Polys that we are drawing
- if not np.array_equal(prev_unmask, self._get_unmasked_polys()):
- self._set_unmasked_verts()
|