2377 lines
87 KiB
Python
2377 lines
87 KiB
Python
"""
|
|
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 math
|
|
from numbers import Number, Real
|
|
import warnings
|
|
|
|
import numpy as np
|
|
|
|
import matplotlib as mpl
|
|
from . import (_api, _path, artist, cbook, cm, 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(artist.Artist, cm.ScalarMappable):
|
|
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
|
|
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: :rc:`patch.capstyle`
|
|
Style to use for capping lines for all paths in the collection.
|
|
Allowed values are %(CapStyle)s.
|
|
joinstyle : `.JoinStyle`-like, default: :rc:`patch.joinstyle`
|
|
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*.
|
|
"""
|
|
artist.Artist.__init__(self)
|
|
cm.ScalarMappable.__init__(self, norm, cmap)
|
|
# 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.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)
|
|
|
|
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.figure.bbox.width
|
|
and extents.height < self.figure.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_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.
|
|
"""
|
|
try:
|
|
dashes = [mlines._get_dash_pattern(ls)]
|
|
except ValueError:
|
|
try:
|
|
dashes = [mlines._get_dash_pattern(x) for x in ls]
|
|
except ValueError as err:
|
|
emsg = f'Do not know how to convert {ls!r} to dashes'
|
|
raise ValueError(emsg) from err
|
|
|
|
# get the list of raw 'unscaled' dash patterns
|
|
self._us_linestyles = dashes
|
|
|
|
# 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.figure.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 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.figure.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 array-like
|
|
A sequence (*line0*, *line1*, *line2*) of lines, where each line is a list
|
|
of points::
|
|
|
|
lineN = [(x0, y0), (x1, y1), ... (xm, ym)]
|
|
|
|
or the equivalent Mx2 numpy array with two columns. Each line
|
|
can have a different number of segments.
|
|
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.figure
|
|
|
|
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):
|
|
# We need to keep track of whether we are using deprecated compression
|
|
# Update it after the initializers
|
|
self._deprecated_compression = False
|
|
super().__init__(coordinates=coordinates)
|
|
PolyCollection.__init__(self, verts=[], **kwargs)
|
|
# Store this during the compression deprecation period
|
|
self._original_mask = ~self._get_unmasked_polys()
|
|
self._deprecated_compression = np.any(self._original_mask)
|
|
# 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])
|
|
|
|
if (getattr(self, "_deprecated_compression", False) and
|
|
np.any(self._original_mask)):
|
|
return ~(mask | self._original_mask)
|
|
# Take account of the array data too, temporarily avoiding
|
|
# the compression warning and resetting the variable after the call
|
|
with cbook._setattr_cm(self, _deprecated_compression=False):
|
|
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()
|
|
# MPL <3.8 compressed the mask, so we need to handle flattened 1d input
|
|
# until the deprecation expires, also only warning when there are masked
|
|
# elements and thus compression occurring.
|
|
if self._deprecated_compression and np.ndim(A) == 1:
|
|
_api.warn_deprecated("3.8", message="Setting a PolyQuadMesh array using "
|
|
"the compressed values is deprecated. "
|
|
"Pass the full 2D shape of the original array "
|
|
f"{prev_unmask.shape} including the masked elements.")
|
|
Afull = np.empty(self._original_mask.shape)
|
|
Afull[~self._original_mask] = A
|
|
# We also want to update the mask with any potential
|
|
# new masked elements that came in. But, we don't want
|
|
# to update any of the compression from the original
|
|
mask = self._original_mask.copy()
|
|
mask[~self._original_mask] |= np.ma.getmask(A)
|
|
A = np.ma.array(Afull, mask=mask)
|
|
return super().set_array(A)
|
|
self._deprecated_compression = False
|
|
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()
|
|
|
|
def get_array(self):
|
|
# docstring inherited
|
|
# Can remove this entire function once the deprecation period ends
|
|
A = super().get_array()
|
|
if A is None:
|
|
return
|
|
if self._deprecated_compression and np.any(np.ma.getmask(A)):
|
|
_api.warn_deprecated("3.8", message=(
|
|
"Getting the array from a PolyQuadMesh will return the full "
|
|
"array in the future (uncompressed). To get this behavior now "
|
|
"set the PolyQuadMesh with a 2D array .set_array(data2d)."))
|
|
# Setting an array of a polycollection required
|
|
# compressing the array
|
|
return np.ma.compressed(A)
|
|
return A
|