1094 lines
41 KiB
Python
1094 lines
41 KiB
Python
|
r"""
|
|||
|
A module for dealing with the polylines used throughout Matplotlib.
|
|||
|
|
|||
|
The primary class for polyline handling in Matplotlib is `Path`. Almost all
|
|||
|
vector drawing makes use of `Path`\s somewhere in the drawing pipeline.
|
|||
|
|
|||
|
Whilst a `Path` instance itself cannot be drawn, some `.Artist` subclasses,
|
|||
|
such as `.PathPatch` and `.PathCollection`, can be used for convenient `Path`
|
|||
|
visualisation.
|
|||
|
"""
|
|||
|
|
|||
|
import copy
|
|||
|
from functools import lru_cache
|
|||
|
from weakref import WeakValueDictionary
|
|||
|
|
|||
|
import numpy as np
|
|||
|
|
|||
|
import matplotlib as mpl
|
|||
|
from . import _api, _path
|
|||
|
from .cbook import _to_unmasked_float_array, simple_linear_interpolation
|
|||
|
from .bezier import BezierSegment
|
|||
|
|
|||
|
|
|||
|
class Path:
|
|||
|
"""
|
|||
|
A series of possibly disconnected, possibly closed, line and curve
|
|||
|
segments.
|
|||
|
|
|||
|
The underlying storage is made up of two parallel numpy arrays:
|
|||
|
|
|||
|
- *vertices*: an (N, 2) float array of vertices
|
|||
|
- *codes*: an N-length `numpy.uint8` array of path codes, or None
|
|||
|
|
|||
|
These two arrays always have the same length in the first
|
|||
|
dimension. For example, to represent a cubic curve, you must
|
|||
|
provide three vertices and three `CURVE4` codes.
|
|||
|
|
|||
|
The code types are:
|
|||
|
|
|||
|
- `STOP` : 1 vertex (ignored)
|
|||
|
A marker for the end of the entire path (currently not required and
|
|||
|
ignored)
|
|||
|
|
|||
|
- `MOVETO` : 1 vertex
|
|||
|
Pick up the pen and move to the given vertex.
|
|||
|
|
|||
|
- `LINETO` : 1 vertex
|
|||
|
Draw a line from the current position to the given vertex.
|
|||
|
|
|||
|
- `CURVE3` : 1 control point, 1 endpoint
|
|||
|
Draw a quadratic Bézier curve from the current position, with the given
|
|||
|
control point, to the given end point.
|
|||
|
|
|||
|
- `CURVE4` : 2 control points, 1 endpoint
|
|||
|
Draw a cubic Bézier curve from the current position, with the given
|
|||
|
control points, to the given end point.
|
|||
|
|
|||
|
- `CLOSEPOLY` : 1 vertex (ignored)
|
|||
|
Draw a line segment to the start point of the current polyline.
|
|||
|
|
|||
|
If *codes* is None, it is interpreted as a `MOVETO` followed by a series
|
|||
|
of `LINETO`.
|
|||
|
|
|||
|
Users of Path objects should not access the vertices and codes arrays
|
|||
|
directly. Instead, they should use `iter_segments` or `cleaned` to get the
|
|||
|
vertex/code pairs. This helps, in particular, to consistently handle the
|
|||
|
case of *codes* being None.
|
|||
|
|
|||
|
Some behavior of Path objects can be controlled by rcParams. See the
|
|||
|
rcParams whose keys start with 'path.'.
|
|||
|
|
|||
|
.. note::
|
|||
|
|
|||
|
The vertices and codes arrays should be treated as
|
|||
|
immutable -- there are a number of optimizations and assumptions
|
|||
|
made up front in the constructor that will not change when the
|
|||
|
data changes.
|
|||
|
"""
|
|||
|
|
|||
|
code_type = np.uint8
|
|||
|
|
|||
|
# Path codes
|
|||
|
STOP = code_type(0) # 1 vertex
|
|||
|
MOVETO = code_type(1) # 1 vertex
|
|||
|
LINETO = code_type(2) # 1 vertex
|
|||
|
CURVE3 = code_type(3) # 2 vertices
|
|||
|
CURVE4 = code_type(4) # 3 vertices
|
|||
|
CLOSEPOLY = code_type(79) # 1 vertex
|
|||
|
|
|||
|
#: A dictionary mapping Path codes to the number of vertices that the
|
|||
|
#: code expects.
|
|||
|
NUM_VERTICES_FOR_CODE = {STOP: 1,
|
|||
|
MOVETO: 1,
|
|||
|
LINETO: 1,
|
|||
|
CURVE3: 2,
|
|||
|
CURVE4: 3,
|
|||
|
CLOSEPOLY: 1}
|
|||
|
|
|||
|
def __init__(self, vertices, codes=None, _interpolation_steps=1,
|
|||
|
closed=False, readonly=False):
|
|||
|
"""
|
|||
|
Create a new path with the given vertices and codes.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
vertices : (N, 2) array-like
|
|||
|
The path vertices, as an array, masked array or sequence of pairs.
|
|||
|
Masked values, if any, will be converted to NaNs, which are then
|
|||
|
handled correctly by the Agg PathIterator and other consumers of
|
|||
|
path data, such as :meth:`iter_segments`.
|
|||
|
codes : array-like or None, optional
|
|||
|
N-length array of integers representing the codes of the path.
|
|||
|
If not None, codes must be the same length as vertices.
|
|||
|
If None, *vertices* will be treated as a series of line segments.
|
|||
|
_interpolation_steps : int, optional
|
|||
|
Used as a hint to certain projections, such as Polar, that this
|
|||
|
path should be linearly interpolated immediately before drawing.
|
|||
|
This attribute is primarily an implementation detail and is not
|
|||
|
intended for public use.
|
|||
|
closed : bool, optional
|
|||
|
If *codes* is None and closed is True, vertices will be treated as
|
|||
|
line segments of a closed polygon. Note that the last vertex will
|
|||
|
then be ignored (as the corresponding code will be set to
|
|||
|
`CLOSEPOLY`).
|
|||
|
readonly : bool, optional
|
|||
|
Makes the path behave in an immutable way and sets the vertices
|
|||
|
and codes as read-only arrays.
|
|||
|
"""
|
|||
|
vertices = _to_unmasked_float_array(vertices)
|
|||
|
_api.check_shape((None, 2), vertices=vertices)
|
|||
|
|
|||
|
if codes is not None:
|
|||
|
codes = np.asarray(codes, self.code_type)
|
|||
|
if codes.ndim != 1 or len(codes) != len(vertices):
|
|||
|
raise ValueError("'codes' must be a 1D list or array with the "
|
|||
|
"same length of 'vertices'. "
|
|||
|
f"Your vertices have shape {vertices.shape} "
|
|||
|
f"but your codes have shape {codes.shape}")
|
|||
|
if len(codes) and codes[0] != self.MOVETO:
|
|||
|
raise ValueError("The first element of 'code' must be equal "
|
|||
|
f"to 'MOVETO' ({self.MOVETO}). "
|
|||
|
f"Your first code is {codes[0]}")
|
|||
|
elif closed and len(vertices):
|
|||
|
codes = np.empty(len(vertices), dtype=self.code_type)
|
|||
|
codes[0] = self.MOVETO
|
|||
|
codes[1:-1] = self.LINETO
|
|||
|
codes[-1] = self.CLOSEPOLY
|
|||
|
|
|||
|
self._vertices = vertices
|
|||
|
self._codes = codes
|
|||
|
self._interpolation_steps = _interpolation_steps
|
|||
|
self._update_values()
|
|||
|
|
|||
|
if readonly:
|
|||
|
self._vertices.flags.writeable = False
|
|||
|
if self._codes is not None:
|
|||
|
self._codes.flags.writeable = False
|
|||
|
self._readonly = True
|
|||
|
else:
|
|||
|
self._readonly = False
|
|||
|
|
|||
|
@classmethod
|
|||
|
def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None):
|
|||
|
"""
|
|||
|
Create a Path instance without the expense of calling the constructor.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
verts : array-like
|
|||
|
codes : array
|
|||
|
internals_from : Path or None
|
|||
|
If not None, another `Path` from which the attributes
|
|||
|
``should_simplify``, ``simplify_threshold``, and
|
|||
|
``interpolation_steps`` will be copied. Note that ``readonly`` is
|
|||
|
never copied, and always set to ``False`` by this constructor.
|
|||
|
"""
|
|||
|
pth = cls.__new__(cls)
|
|||
|
pth._vertices = _to_unmasked_float_array(verts)
|
|||
|
pth._codes = codes
|
|||
|
pth._readonly = False
|
|||
|
if internals_from is not None:
|
|||
|
pth._should_simplify = internals_from._should_simplify
|
|||
|
pth._simplify_threshold = internals_from._simplify_threshold
|
|||
|
pth._interpolation_steps = internals_from._interpolation_steps
|
|||
|
else:
|
|||
|
pth._should_simplify = True
|
|||
|
pth._simplify_threshold = mpl.rcParams['path.simplify_threshold']
|
|||
|
pth._interpolation_steps = 1
|
|||
|
return pth
|
|||
|
|
|||
|
@classmethod
|
|||
|
def _create_closed(cls, vertices):
|
|||
|
"""
|
|||
|
Create a closed polygonal path going through *vertices*.
|
|||
|
|
|||
|
Unlike ``Path(..., closed=True)``, *vertices* should **not** end with
|
|||
|
an entry for the CLOSEPATH; this entry is added by `._create_closed`.
|
|||
|
"""
|
|||
|
v = _to_unmasked_float_array(vertices)
|
|||
|
return cls(np.concatenate([v, v[:1]]), closed=True)
|
|||
|
|
|||
|
def _update_values(self):
|
|||
|
self._simplify_threshold = mpl.rcParams['path.simplify_threshold']
|
|||
|
self._should_simplify = (
|
|||
|
self._simplify_threshold > 0 and
|
|||
|
mpl.rcParams['path.simplify'] and
|
|||
|
len(self._vertices) >= 128 and
|
|||
|
(self._codes is None or np.all(self._codes <= Path.LINETO))
|
|||
|
)
|
|||
|
|
|||
|
@property
|
|||
|
def vertices(self):
|
|||
|
"""The vertices of the `Path` as an (N, 2) array."""
|
|||
|
return self._vertices
|
|||
|
|
|||
|
@vertices.setter
|
|||
|
def vertices(self, vertices):
|
|||
|
if self._readonly:
|
|||
|
raise AttributeError("Can't set vertices on a readonly Path")
|
|||
|
self._vertices = vertices
|
|||
|
self._update_values()
|
|||
|
|
|||
|
@property
|
|||
|
def codes(self):
|
|||
|
"""
|
|||
|
The list of codes in the `Path` as a 1D array.
|
|||
|
|
|||
|
Each code is one of `STOP`, `MOVETO`, `LINETO`, `CURVE3`, `CURVE4` or
|
|||
|
`CLOSEPOLY`. For codes that correspond to more than one vertex
|
|||
|
(`CURVE3` and `CURVE4`), that code will be repeated so that the length
|
|||
|
of `vertices` and `codes` is always the same.
|
|||
|
"""
|
|||
|
return self._codes
|
|||
|
|
|||
|
@codes.setter
|
|||
|
def codes(self, codes):
|
|||
|
if self._readonly:
|
|||
|
raise AttributeError("Can't set codes on a readonly Path")
|
|||
|
self._codes = codes
|
|||
|
self._update_values()
|
|||
|
|
|||
|
@property
|
|||
|
def simplify_threshold(self):
|
|||
|
"""
|
|||
|
The fraction of a pixel difference below which vertices will
|
|||
|
be simplified out.
|
|||
|
"""
|
|||
|
return self._simplify_threshold
|
|||
|
|
|||
|
@simplify_threshold.setter
|
|||
|
def simplify_threshold(self, threshold):
|
|||
|
self._simplify_threshold = threshold
|
|||
|
|
|||
|
@property
|
|||
|
def should_simplify(self):
|
|||
|
"""
|
|||
|
`True` if the vertices array should be simplified.
|
|||
|
"""
|
|||
|
return self._should_simplify
|
|||
|
|
|||
|
@should_simplify.setter
|
|||
|
def should_simplify(self, should_simplify):
|
|||
|
self._should_simplify = should_simplify
|
|||
|
|
|||
|
@property
|
|||
|
def readonly(self):
|
|||
|
"""
|
|||
|
`True` if the `Path` is read-only.
|
|||
|
"""
|
|||
|
return self._readonly
|
|||
|
|
|||
|
def copy(self):
|
|||
|
"""
|
|||
|
Return a shallow copy of the `Path`, which will share the
|
|||
|
vertices and codes with the source `Path`.
|
|||
|
"""
|
|||
|
return copy.copy(self)
|
|||
|
|
|||
|
def __deepcopy__(self, memo=None):
|
|||
|
"""
|
|||
|
Return a deepcopy of the `Path`. The `Path` will not be
|
|||
|
readonly, even if the source `Path` is.
|
|||
|
"""
|
|||
|
# Deepcopying arrays (vertices, codes) strips the writeable=False flag.
|
|||
|
p = copy.deepcopy(super(), memo)
|
|||
|
p._readonly = False
|
|||
|
return p
|
|||
|
|
|||
|
deepcopy = __deepcopy__
|
|||
|
|
|||
|
@classmethod
|
|||
|
def make_compound_path_from_polys(cls, XY):
|
|||
|
"""
|
|||
|
Make a compound `Path` object to draw a number of polygons with equal
|
|||
|
numbers of sides.
|
|||
|
|
|||
|
.. plot:: gallery/misc/histogram_path.py
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
XY : (numpolys, numsides, 2) array
|
|||
|
"""
|
|||
|
# for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for
|
|||
|
# the CLOSEPOLY; the vert for the closepoly is ignored but we still
|
|||
|
# need it to keep the codes aligned with the vertices
|
|||
|
numpolys, numsides, two = XY.shape
|
|||
|
if two != 2:
|
|||
|
raise ValueError("The third dimension of 'XY' must be 2")
|
|||
|
stride = numsides + 1
|
|||
|
nverts = numpolys * stride
|
|||
|
verts = np.zeros((nverts, 2))
|
|||
|
codes = np.full(nverts, cls.LINETO, dtype=cls.code_type)
|
|||
|
codes[0::stride] = cls.MOVETO
|
|||
|
codes[numsides::stride] = cls.CLOSEPOLY
|
|||
|
for i in range(numsides):
|
|||
|
verts[i::stride] = XY[:, i]
|
|||
|
return cls(verts, codes)
|
|||
|
|
|||
|
@classmethod
|
|||
|
def make_compound_path(cls, *args):
|
|||
|
r"""
|
|||
|
Concatenate a list of `Path`\s into a single `Path`, removing all `STOP`\s.
|
|||
|
"""
|
|||
|
if not args:
|
|||
|
return Path(np.empty([0, 2], dtype=np.float32))
|
|||
|
vertices = np.concatenate([path.vertices for path in args])
|
|||
|
codes = np.empty(len(vertices), dtype=cls.code_type)
|
|||
|
i = 0
|
|||
|
for path in args:
|
|||
|
size = len(path.vertices)
|
|||
|
if path.codes is None:
|
|||
|
if size:
|
|||
|
codes[i] = cls.MOVETO
|
|||
|
codes[i+1:i+size] = cls.LINETO
|
|||
|
else:
|
|||
|
codes[i:i+size] = path.codes
|
|||
|
i += size
|
|||
|
not_stop_mask = codes != cls.STOP # Remove STOPs, as internal STOPs are a bug.
|
|||
|
return cls(vertices[not_stop_mask], codes[not_stop_mask])
|
|||
|
|
|||
|
def __repr__(self):
|
|||
|
return f"Path({self.vertices!r}, {self.codes!r})"
|
|||
|
|
|||
|
def __len__(self):
|
|||
|
return len(self.vertices)
|
|||
|
|
|||
|
def iter_segments(self, transform=None, remove_nans=True, clip=None,
|
|||
|
snap=False, stroke_width=1.0, simplify=None,
|
|||
|
curves=True, sketch=None):
|
|||
|
"""
|
|||
|
Iterate over all curve segments in the path.
|
|||
|
|
|||
|
Each iteration returns a pair ``(vertices, code)``, where ``vertices``
|
|||
|
is a sequence of 1-3 coordinate pairs, and ``code`` is a `Path` code.
|
|||
|
|
|||
|
Additionally, this method can provide a number of standard cleanups and
|
|||
|
conversions to the path.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
transform : None or :class:`~matplotlib.transforms.Transform`
|
|||
|
If not None, the given affine transformation will be applied to the
|
|||
|
path.
|
|||
|
remove_nans : bool, optional
|
|||
|
Whether to remove all NaNs from the path and skip over them using
|
|||
|
MOVETO commands.
|
|||
|
clip : None or (float, float, float, float), optional
|
|||
|
If not None, must be a four-tuple (x1, y1, x2, y2)
|
|||
|
defining a rectangle in which to clip the path.
|
|||
|
snap : None or bool, optional
|
|||
|
If True, snap all nodes to pixels; if False, don't snap them.
|
|||
|
If None, snap if the path contains only segments
|
|||
|
parallel to the x or y axes, and no more than 1024 of them.
|
|||
|
stroke_width : float, optional
|
|||
|
The width of the stroke being drawn (used for path snapping).
|
|||
|
simplify : None or bool, optional
|
|||
|
Whether to simplify the path by removing vertices
|
|||
|
that do not affect its appearance. If None, use the
|
|||
|
:attr:`should_simplify` attribute. See also :rc:`path.simplify`
|
|||
|
and :rc:`path.simplify_threshold`.
|
|||
|
curves : bool, optional
|
|||
|
If True, curve segments will be returned as curve segments.
|
|||
|
If False, all curves will be converted to line segments.
|
|||
|
sketch : None or sequence, optional
|
|||
|
If not None, must be a 3-tuple of the form
|
|||
|
(scale, length, randomness), representing the sketch parameters.
|
|||
|
"""
|
|||
|
if not len(self):
|
|||
|
return
|
|||
|
|
|||
|
cleaned = self.cleaned(transform=transform,
|
|||
|
remove_nans=remove_nans, clip=clip,
|
|||
|
snap=snap, stroke_width=stroke_width,
|
|||
|
simplify=simplify, curves=curves,
|
|||
|
sketch=sketch)
|
|||
|
|
|||
|
# Cache these object lookups for performance in the loop.
|
|||
|
NUM_VERTICES_FOR_CODE = self.NUM_VERTICES_FOR_CODE
|
|||
|
STOP = self.STOP
|
|||
|
|
|||
|
vertices = iter(cleaned.vertices)
|
|||
|
codes = iter(cleaned.codes)
|
|||
|
for curr_vertices, code in zip(vertices, codes):
|
|||
|
if code == STOP:
|
|||
|
break
|
|||
|
extra_vertices = NUM_VERTICES_FOR_CODE[code] - 1
|
|||
|
if extra_vertices:
|
|||
|
for i in range(extra_vertices):
|
|||
|
next(codes)
|
|||
|
curr_vertices = np.append(curr_vertices, next(vertices))
|
|||
|
yield curr_vertices, code
|
|||
|
|
|||
|
def iter_bezier(self, **kwargs):
|
|||
|
"""
|
|||
|
Iterate over each Bézier curve (lines included) in a `Path`.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
**kwargs
|
|||
|
Forwarded to `.iter_segments`.
|
|||
|
|
|||
|
Yields
|
|||
|
------
|
|||
|
B : `~matplotlib.bezier.BezierSegment`
|
|||
|
The Bézier curves that make up the current path. Note in particular
|
|||
|
that freestanding points are Bézier curves of order 0, and lines
|
|||
|
are Bézier curves of order 1 (with two control points).
|
|||
|
code : `~matplotlib.path.Path.code_type`
|
|||
|
The code describing what kind of curve is being returned.
|
|||
|
`MOVETO`, `LINETO`, `CURVE3`, and `CURVE4` correspond to
|
|||
|
Bézier curves with 1, 2, 3, and 4 control points (respectively).
|
|||
|
`CLOSEPOLY` is a `LINETO` with the control points correctly
|
|||
|
chosen based on the start/end points of the current stroke.
|
|||
|
"""
|
|||
|
first_vert = None
|
|||
|
prev_vert = None
|
|||
|
for verts, code in self.iter_segments(**kwargs):
|
|||
|
if first_vert is None:
|
|||
|
if code != Path.MOVETO:
|
|||
|
raise ValueError("Malformed path, must start with MOVETO.")
|
|||
|
if code == Path.MOVETO: # a point is like "CURVE1"
|
|||
|
first_vert = verts
|
|||
|
yield BezierSegment(np.array([first_vert])), code
|
|||
|
elif code == Path.LINETO: # "CURVE2"
|
|||
|
yield BezierSegment(np.array([prev_vert, verts])), code
|
|||
|
elif code == Path.CURVE3:
|
|||
|
yield BezierSegment(np.array([prev_vert, verts[:2],
|
|||
|
verts[2:]])), code
|
|||
|
elif code == Path.CURVE4:
|
|||
|
yield BezierSegment(np.array([prev_vert, verts[:2],
|
|||
|
verts[2:4], verts[4:]])), code
|
|||
|
elif code == Path.CLOSEPOLY:
|
|||
|
yield BezierSegment(np.array([prev_vert, first_vert])), code
|
|||
|
elif code == Path.STOP:
|
|||
|
return
|
|||
|
else:
|
|||
|
raise ValueError(f"Invalid Path.code_type: {code}")
|
|||
|
prev_vert = verts[-2:]
|
|||
|
|
|||
|
def _iter_connected_components(self):
|
|||
|
"""Return subpaths split at MOVETOs."""
|
|||
|
if self.codes is None:
|
|||
|
yield self
|
|||
|
else:
|
|||
|
idxs = np.append((self.codes == Path.MOVETO).nonzero()[0], len(self.codes))
|
|||
|
for sl in map(slice, idxs, idxs[1:]):
|
|||
|
yield Path._fast_from_codes_and_verts(
|
|||
|
self.vertices[sl], self.codes[sl], self)
|
|||
|
|
|||
|
def cleaned(self, transform=None, remove_nans=False, clip=None,
|
|||
|
*, simplify=False, curves=False,
|
|||
|
stroke_width=1.0, snap=False, sketch=None):
|
|||
|
"""
|
|||
|
Return a new `Path` with vertices and codes cleaned according to the
|
|||
|
parameters.
|
|||
|
|
|||
|
See Also
|
|||
|
--------
|
|||
|
Path.iter_segments : for details of the keyword arguments.
|
|||
|
"""
|
|||
|
vertices, codes = _path.cleanup_path(
|
|||
|
self, transform, remove_nans, clip, snap, stroke_width, simplify,
|
|||
|
curves, sketch)
|
|||
|
pth = Path._fast_from_codes_and_verts(vertices, codes, self)
|
|||
|
if not simplify:
|
|||
|
pth._should_simplify = False
|
|||
|
return pth
|
|||
|
|
|||
|
def transformed(self, transform):
|
|||
|
"""
|
|||
|
Return a transformed copy of the path.
|
|||
|
|
|||
|
See Also
|
|||
|
--------
|
|||
|
matplotlib.transforms.TransformedPath
|
|||
|
A specialized path class that will cache the transformed result and
|
|||
|
automatically update when the transform changes.
|
|||
|
"""
|
|||
|
return Path(transform.transform(self.vertices), self.codes,
|
|||
|
self._interpolation_steps)
|
|||
|
|
|||
|
def contains_point(self, point, transform=None, radius=0.0):
|
|||
|
"""
|
|||
|
Return whether the area enclosed by the path contains the given point.
|
|||
|
|
|||
|
The path is always treated as closed; i.e. if the last code is not
|
|||
|
`CLOSEPOLY` an implicit segment connecting the last vertex to the first
|
|||
|
vertex is assumed.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
point : (float, float)
|
|||
|
The point (x, y) to check.
|
|||
|
transform : `~matplotlib.transforms.Transform`, optional
|
|||
|
If not ``None``, *point* will be compared to ``self`` transformed
|
|||
|
by *transform*; i.e. for a correct check, *transform* should
|
|||
|
transform the path into the coordinate system of *point*.
|
|||
|
radius : float, default: 0
|
|||
|
Additional margin on the path in coordinates of *point*.
|
|||
|
The path is extended tangentially by *radius/2*; i.e. if you would
|
|||
|
draw the path with a linewidth of *radius*, all points on the line
|
|||
|
would still be considered to be contained in the area. Conversely,
|
|||
|
negative values shrink the area: Points on the imaginary line
|
|||
|
will be considered outside the area.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
bool
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
The current algorithm has some limitations:
|
|||
|
|
|||
|
- The result is undefined for points exactly at the boundary
|
|||
|
(i.e. at the path shifted by *radius/2*).
|
|||
|
- The result is undefined if there is no enclosed area, i.e. all
|
|||
|
vertices are on a straight line.
|
|||
|
- If bounding lines start to cross each other due to *radius* shift,
|
|||
|
the result is not guaranteed to be correct.
|
|||
|
"""
|
|||
|
if transform is not None:
|
|||
|
transform = transform.frozen()
|
|||
|
# `point_in_path` does not handle nonlinear transforms, so we
|
|||
|
# transform the path ourselves. If *transform* is affine, letting
|
|||
|
# `point_in_path` handle the transform avoids allocating an extra
|
|||
|
# buffer.
|
|||
|
if transform and not transform.is_affine:
|
|||
|
self = transform.transform_path(self)
|
|||
|
transform = None
|
|||
|
return _path.point_in_path(point[0], point[1], radius, self, transform)
|
|||
|
|
|||
|
def contains_points(self, points, transform=None, radius=0.0):
|
|||
|
"""
|
|||
|
Return whether the area enclosed by the path contains the given points.
|
|||
|
|
|||
|
The path is always treated as closed; i.e. if the last code is not
|
|||
|
`CLOSEPOLY` an implicit segment connecting the last vertex to the first
|
|||
|
vertex is assumed.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
points : (N, 2) array
|
|||
|
The points to check. Columns contain x and y values.
|
|||
|
transform : `~matplotlib.transforms.Transform`, optional
|
|||
|
If not ``None``, *points* will be compared to ``self`` transformed
|
|||
|
by *transform*; i.e. for a correct check, *transform* should
|
|||
|
transform the path into the coordinate system of *points*.
|
|||
|
radius : float, default: 0
|
|||
|
Additional margin on the path in coordinates of *points*.
|
|||
|
The path is extended tangentially by *radius/2*; i.e. if you would
|
|||
|
draw the path with a linewidth of *radius*, all points on the line
|
|||
|
would still be considered to be contained in the area. Conversely,
|
|||
|
negative values shrink the area: Points on the imaginary line
|
|||
|
will be considered outside the area.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
length-N bool array
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
The current algorithm has some limitations:
|
|||
|
|
|||
|
- The result is undefined for points exactly at the boundary
|
|||
|
(i.e. at the path shifted by *radius/2*).
|
|||
|
- The result is undefined if there is no enclosed area, i.e. all
|
|||
|
vertices are on a straight line.
|
|||
|
- If bounding lines start to cross each other due to *radius* shift,
|
|||
|
the result is not guaranteed to be correct.
|
|||
|
"""
|
|||
|
if transform is not None:
|
|||
|
transform = transform.frozen()
|
|||
|
result = _path.points_in_path(points, radius, self, transform)
|
|||
|
return result.astype('bool')
|
|||
|
|
|||
|
def contains_path(self, path, transform=None):
|
|||
|
"""
|
|||
|
Return whether this (closed) path completely contains the given path.
|
|||
|
|
|||
|
If *transform* is not ``None``, the path will be transformed before
|
|||
|
checking for containment.
|
|||
|
"""
|
|||
|
if transform is not None:
|
|||
|
transform = transform.frozen()
|
|||
|
return _path.path_in_path(self, None, path, transform)
|
|||
|
|
|||
|
def get_extents(self, transform=None, **kwargs):
|
|||
|
"""
|
|||
|
Get Bbox of the path.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
transform : `~matplotlib.transforms.Transform`, optional
|
|||
|
Transform to apply to path before computing extents, if any.
|
|||
|
**kwargs
|
|||
|
Forwarded to `.iter_bezier`.
|
|||
|
|
|||
|
Returns
|
|||
|
-------
|
|||
|
matplotlib.transforms.Bbox
|
|||
|
The extents of the path Bbox([[xmin, ymin], [xmax, ymax]])
|
|||
|
"""
|
|||
|
from .transforms import Bbox
|
|||
|
if transform is not None:
|
|||
|
self = transform.transform_path(self)
|
|||
|
if self.codes is None:
|
|||
|
xys = self.vertices
|
|||
|
elif len(np.intersect1d(self.codes, [Path.CURVE3, Path.CURVE4])) == 0:
|
|||
|
# Optimization for the straight line case.
|
|||
|
# Instead of iterating through each curve, consider
|
|||
|
# each line segment's end-points
|
|||
|
# (recall that STOP and CLOSEPOLY vertices are ignored)
|
|||
|
xys = self.vertices[np.isin(self.codes,
|
|||
|
[Path.MOVETO, Path.LINETO])]
|
|||
|
else:
|
|||
|
xys = []
|
|||
|
for curve, code in self.iter_bezier(**kwargs):
|
|||
|
# places where the derivative is zero can be extrema
|
|||
|
_, dzeros = curve.axis_aligned_extrema()
|
|||
|
# as can the ends of the curve
|
|||
|
xys.append(curve([0, *dzeros, 1]))
|
|||
|
xys = np.concatenate(xys)
|
|||
|
if len(xys):
|
|||
|
return Bbox([xys.min(axis=0), xys.max(axis=0)])
|
|||
|
else:
|
|||
|
return Bbox.null()
|
|||
|
|
|||
|
def intersects_path(self, other, filled=True):
|
|||
|
"""
|
|||
|
Return whether if this path intersects another given path.
|
|||
|
|
|||
|
If *filled* is True, then this also returns True if one path completely
|
|||
|
encloses the other (i.e., the paths are treated as filled).
|
|||
|
"""
|
|||
|
return _path.path_intersects_path(self, other, filled)
|
|||
|
|
|||
|
def intersects_bbox(self, bbox, filled=True):
|
|||
|
"""
|
|||
|
Return whether this path intersects a given `~.transforms.Bbox`.
|
|||
|
|
|||
|
If *filled* is True, then this also returns True if the path completely
|
|||
|
encloses the `.Bbox` (i.e., the path is treated as filled).
|
|||
|
|
|||
|
The bounding box is always considered filled.
|
|||
|
"""
|
|||
|
return _path.path_intersects_rectangle(
|
|||
|
self, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled)
|
|||
|
|
|||
|
def interpolated(self, steps):
|
|||
|
"""
|
|||
|
Return a new path resampled to length N x *steps*.
|
|||
|
|
|||
|
Codes other than `LINETO` are not handled correctly.
|
|||
|
"""
|
|||
|
if steps == 1:
|
|||
|
return self
|
|||
|
|
|||
|
vertices = simple_linear_interpolation(self.vertices, steps)
|
|||
|
codes = self.codes
|
|||
|
if codes is not None:
|
|||
|
new_codes = np.full((len(codes) - 1) * steps + 1, Path.LINETO,
|
|||
|
dtype=self.code_type)
|
|||
|
new_codes[0::steps] = codes
|
|||
|
else:
|
|||
|
new_codes = None
|
|||
|
return Path(vertices, new_codes)
|
|||
|
|
|||
|
def to_polygons(self, transform=None, width=0, height=0, closed_only=True):
|
|||
|
"""
|
|||
|
Convert this path to a list of polygons or polylines. Each
|
|||
|
polygon/polyline is an (N, 2) array of vertices. In other words,
|
|||
|
each polygon has no `MOVETO` instructions or curves. This
|
|||
|
is useful for displaying in backends that do not support
|
|||
|
compound paths or Bézier curves.
|
|||
|
|
|||
|
If *width* and *height* are both non-zero then the lines will
|
|||
|
be simplified so that vertices outside of (0, 0), (width,
|
|||
|
height) will be clipped.
|
|||
|
|
|||
|
If *closed_only* is `True` (default), only closed polygons,
|
|||
|
with the last point being the same as the first point, will be
|
|||
|
returned. Any unclosed polylines in the path will be
|
|||
|
explicitly closed. If *closed_only* is `False`, any unclosed
|
|||
|
polygons in the path will be returned as unclosed polygons,
|
|||
|
and the closed polygons will be returned explicitly closed by
|
|||
|
setting the last point to the same as the first point.
|
|||
|
"""
|
|||
|
if len(self.vertices) == 0:
|
|||
|
return []
|
|||
|
|
|||
|
if transform is not None:
|
|||
|
transform = transform.frozen()
|
|||
|
|
|||
|
if self.codes is None and (width == 0 or height == 0):
|
|||
|
vertices = self.vertices
|
|||
|
if closed_only:
|
|||
|
if len(vertices) < 3:
|
|||
|
return []
|
|||
|
elif np.any(vertices[0] != vertices[-1]):
|
|||
|
vertices = [*vertices, vertices[0]]
|
|||
|
|
|||
|
if transform is None:
|
|||
|
return [vertices]
|
|||
|
else:
|
|||
|
return [transform.transform(vertices)]
|
|||
|
|
|||
|
# Deal with the case where there are curves and/or multiple
|
|||
|
# subpaths (using extension code)
|
|||
|
return _path.convert_path_to_polygons(
|
|||
|
self, transform, width, height, closed_only)
|
|||
|
|
|||
|
_unit_rectangle = None
|
|||
|
|
|||
|
@classmethod
|
|||
|
def unit_rectangle(cls):
|
|||
|
"""
|
|||
|
Return a `Path` instance of the unit rectangle from (0, 0) to (1, 1).
|
|||
|
"""
|
|||
|
if cls._unit_rectangle is None:
|
|||
|
cls._unit_rectangle = cls([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]],
|
|||
|
closed=True, readonly=True)
|
|||
|
return cls._unit_rectangle
|
|||
|
|
|||
|
_unit_regular_polygons = WeakValueDictionary()
|
|||
|
|
|||
|
@classmethod
|
|||
|
def unit_regular_polygon(cls, numVertices):
|
|||
|
"""
|
|||
|
Return a :class:`Path` instance for a unit regular polygon with the
|
|||
|
given *numVertices* such that the circumscribing circle has radius 1.0,
|
|||
|
centered at (0, 0).
|
|||
|
"""
|
|||
|
if numVertices <= 16:
|
|||
|
path = cls._unit_regular_polygons.get(numVertices)
|
|||
|
else:
|
|||
|
path = None
|
|||
|
if path is None:
|
|||
|
theta = ((2 * np.pi / numVertices) * np.arange(numVertices + 1)
|
|||
|
# This initial rotation is to make sure the polygon always
|
|||
|
# "points-up".
|
|||
|
+ np.pi / 2)
|
|||
|
verts = np.column_stack((np.cos(theta), np.sin(theta)))
|
|||
|
path = cls(verts, closed=True, readonly=True)
|
|||
|
if numVertices <= 16:
|
|||
|
cls._unit_regular_polygons[numVertices] = path
|
|||
|
return path
|
|||
|
|
|||
|
_unit_regular_stars = WeakValueDictionary()
|
|||
|
|
|||
|
@classmethod
|
|||
|
def unit_regular_star(cls, numVertices, innerCircle=0.5):
|
|||
|
"""
|
|||
|
Return a :class:`Path` for a unit regular star with the given
|
|||
|
numVertices and radius of 1.0, centered at (0, 0).
|
|||
|
"""
|
|||
|
if numVertices <= 16:
|
|||
|
path = cls._unit_regular_stars.get((numVertices, innerCircle))
|
|||
|
else:
|
|||
|
path = None
|
|||
|
if path is None:
|
|||
|
ns2 = numVertices * 2
|
|||
|
theta = (2*np.pi/ns2 * np.arange(ns2 + 1))
|
|||
|
# This initial rotation is to make sure the polygon always
|
|||
|
# "points-up"
|
|||
|
theta += np.pi / 2.0
|
|||
|
r = np.ones(ns2 + 1)
|
|||
|
r[1::2] = innerCircle
|
|||
|
verts = (r * np.vstack((np.cos(theta), np.sin(theta)))).T
|
|||
|
path = cls(verts, closed=True, readonly=True)
|
|||
|
if numVertices <= 16:
|
|||
|
cls._unit_regular_stars[(numVertices, innerCircle)] = path
|
|||
|
return path
|
|||
|
|
|||
|
@classmethod
|
|||
|
def unit_regular_asterisk(cls, numVertices):
|
|||
|
"""
|
|||
|
Return a :class:`Path` for a unit regular asterisk with the given
|
|||
|
numVertices and radius of 1.0, centered at (0, 0).
|
|||
|
"""
|
|||
|
return cls.unit_regular_star(numVertices, 0.0)
|
|||
|
|
|||
|
_unit_circle = None
|
|||
|
|
|||
|
@classmethod
|
|||
|
def unit_circle(cls):
|
|||
|
"""
|
|||
|
Return the readonly :class:`Path` of the unit circle.
|
|||
|
|
|||
|
For most cases, :func:`Path.circle` will be what you want.
|
|||
|
"""
|
|||
|
if cls._unit_circle is None:
|
|||
|
cls._unit_circle = cls.circle(center=(0, 0), radius=1,
|
|||
|
readonly=True)
|
|||
|
return cls._unit_circle
|
|||
|
|
|||
|
@classmethod
|
|||
|
def circle(cls, center=(0., 0.), radius=1., readonly=False):
|
|||
|
"""
|
|||
|
Return a `Path` representing a circle of a given radius and center.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
center : (float, float), default: (0, 0)
|
|||
|
The center of the circle.
|
|||
|
radius : float, default: 1
|
|||
|
The radius of the circle.
|
|||
|
readonly : bool
|
|||
|
Whether the created path should have the "readonly" argument
|
|||
|
set when creating the Path instance.
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
The circle is approximated using 8 cubic Bézier curves, as described in
|
|||
|
|
|||
|
Lancaster, Don. `Approximating a Circle or an Ellipse Using Four
|
|||
|
Bezier Cubic Splines <https://www.tinaja.com/glib/ellipse4.pdf>`_.
|
|||
|
"""
|
|||
|
MAGIC = 0.2652031
|
|||
|
SQRTHALF = np.sqrt(0.5)
|
|||
|
MAGIC45 = SQRTHALF * MAGIC
|
|||
|
|
|||
|
vertices = np.array([[0.0, -1.0],
|
|||
|
|
|||
|
[MAGIC, -1.0],
|
|||
|
[SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45],
|
|||
|
[SQRTHALF, -SQRTHALF],
|
|||
|
|
|||
|
[SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45],
|
|||
|
[1.0, -MAGIC],
|
|||
|
[1.0, 0.0],
|
|||
|
|
|||
|
[1.0, MAGIC],
|
|||
|
[SQRTHALF+MAGIC45, SQRTHALF-MAGIC45],
|
|||
|
[SQRTHALF, SQRTHALF],
|
|||
|
|
|||
|
[SQRTHALF-MAGIC45, SQRTHALF+MAGIC45],
|
|||
|
[MAGIC, 1.0],
|
|||
|
[0.0, 1.0],
|
|||
|
|
|||
|
[-MAGIC, 1.0],
|
|||
|
[-SQRTHALF+MAGIC45, SQRTHALF+MAGIC45],
|
|||
|
[-SQRTHALF, SQRTHALF],
|
|||
|
|
|||
|
[-SQRTHALF-MAGIC45, SQRTHALF-MAGIC45],
|
|||
|
[-1.0, MAGIC],
|
|||
|
[-1.0, 0.0],
|
|||
|
|
|||
|
[-1.0, -MAGIC],
|
|||
|
[-SQRTHALF-MAGIC45, -SQRTHALF+MAGIC45],
|
|||
|
[-SQRTHALF, -SQRTHALF],
|
|||
|
|
|||
|
[-SQRTHALF+MAGIC45, -SQRTHALF-MAGIC45],
|
|||
|
[-MAGIC, -1.0],
|
|||
|
[0.0, -1.0],
|
|||
|
|
|||
|
[0.0, -1.0]],
|
|||
|
dtype=float)
|
|||
|
|
|||
|
codes = [cls.CURVE4] * 26
|
|||
|
codes[0] = cls.MOVETO
|
|||
|
codes[-1] = cls.CLOSEPOLY
|
|||
|
return Path(vertices * radius + center, codes, readonly=readonly)
|
|||
|
|
|||
|
_unit_circle_righthalf = None
|
|||
|
|
|||
|
@classmethod
|
|||
|
def unit_circle_righthalf(cls):
|
|||
|
"""
|
|||
|
Return a `Path` of the right half of a unit circle.
|
|||
|
|
|||
|
See `Path.circle` for the reference on the approximation used.
|
|||
|
"""
|
|||
|
if cls._unit_circle_righthalf is None:
|
|||
|
MAGIC = 0.2652031
|
|||
|
SQRTHALF = np.sqrt(0.5)
|
|||
|
MAGIC45 = SQRTHALF * MAGIC
|
|||
|
|
|||
|
vertices = np.array(
|
|||
|
[[0.0, -1.0],
|
|||
|
|
|||
|
[MAGIC, -1.0],
|
|||
|
[SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45],
|
|||
|
[SQRTHALF, -SQRTHALF],
|
|||
|
|
|||
|
[SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45],
|
|||
|
[1.0, -MAGIC],
|
|||
|
[1.0, 0.0],
|
|||
|
|
|||
|
[1.0, MAGIC],
|
|||
|
[SQRTHALF+MAGIC45, SQRTHALF-MAGIC45],
|
|||
|
[SQRTHALF, SQRTHALF],
|
|||
|
|
|||
|
[SQRTHALF-MAGIC45, SQRTHALF+MAGIC45],
|
|||
|
[MAGIC, 1.0],
|
|||
|
[0.0, 1.0],
|
|||
|
|
|||
|
[0.0, -1.0]],
|
|||
|
|
|||
|
float)
|
|||
|
|
|||
|
codes = np.full(14, cls.CURVE4, dtype=cls.code_type)
|
|||
|
codes[0] = cls.MOVETO
|
|||
|
codes[-1] = cls.CLOSEPOLY
|
|||
|
|
|||
|
cls._unit_circle_righthalf = cls(vertices, codes, readonly=True)
|
|||
|
return cls._unit_circle_righthalf
|
|||
|
|
|||
|
@classmethod
|
|||
|
def arc(cls, theta1, theta2, n=None, is_wedge=False):
|
|||
|
"""
|
|||
|
Return a `Path` for the unit circle arc from angles *theta1* to
|
|||
|
*theta2* (in degrees).
|
|||
|
|
|||
|
*theta2* is unwrapped to produce the shortest arc within 360 degrees.
|
|||
|
That is, if *theta2* > *theta1* + 360, the arc will be from *theta1* to
|
|||
|
*theta2* - 360 and not a full circle plus some extra overlap.
|
|||
|
|
|||
|
If *n* is provided, it is the number of spline segments to make.
|
|||
|
If *n* is not provided, the number of spline segments is
|
|||
|
determined based on the delta between *theta1* and *theta2*.
|
|||
|
|
|||
|
Masionobe, L. 2003. `Drawing an elliptical arc using
|
|||
|
polylines, quadratic or cubic Bezier curves
|
|||
|
<https://web.archive.org/web/20190318044212/http://www.spaceroots.org/documents/ellipse/index.html>`_.
|
|||
|
"""
|
|||
|
halfpi = np.pi * 0.5
|
|||
|
|
|||
|
eta1 = theta1
|
|||
|
eta2 = theta2 - 360 * np.floor((theta2 - theta1) / 360)
|
|||
|
# Ensure 2pi range is not flattened to 0 due to floating-point errors,
|
|||
|
# but don't try to expand existing 0 range.
|
|||
|
if theta2 != theta1 and eta2 <= eta1:
|
|||
|
eta2 += 360
|
|||
|
eta1, eta2 = np.deg2rad([eta1, eta2])
|
|||
|
|
|||
|
# number of curve segments to make
|
|||
|
if n is None:
|
|||
|
n = int(2 ** np.ceil((eta2 - eta1) / halfpi))
|
|||
|
if n < 1:
|
|||
|
raise ValueError("n must be >= 1 or None")
|
|||
|
|
|||
|
deta = (eta2 - eta1) / n
|
|||
|
t = np.tan(0.5 * deta)
|
|||
|
alpha = np.sin(deta) * (np.sqrt(4.0 + 3.0 * t * t) - 1) / 3.0
|
|||
|
|
|||
|
steps = np.linspace(eta1, eta2, n + 1, True)
|
|||
|
cos_eta = np.cos(steps)
|
|||
|
sin_eta = np.sin(steps)
|
|||
|
|
|||
|
xA = cos_eta[:-1]
|
|||
|
yA = sin_eta[:-1]
|
|||
|
xA_dot = -yA
|
|||
|
yA_dot = xA
|
|||
|
|
|||
|
xB = cos_eta[1:]
|
|||
|
yB = sin_eta[1:]
|
|||
|
xB_dot = -yB
|
|||
|
yB_dot = xB
|
|||
|
|
|||
|
if is_wedge:
|
|||
|
length = n * 3 + 4
|
|||
|
vertices = np.zeros((length, 2), float)
|
|||
|
codes = np.full(length, cls.CURVE4, dtype=cls.code_type)
|
|||
|
vertices[1] = [xA[0], yA[0]]
|
|||
|
codes[0:2] = [cls.MOVETO, cls.LINETO]
|
|||
|
codes[-2:] = [cls.LINETO, cls.CLOSEPOLY]
|
|||
|
vertex_offset = 2
|
|||
|
end = length - 2
|
|||
|
else:
|
|||
|
length = n * 3 + 1
|
|||
|
vertices = np.empty((length, 2), float)
|
|||
|
codes = np.full(length, cls.CURVE4, dtype=cls.code_type)
|
|||
|
vertices[0] = [xA[0], yA[0]]
|
|||
|
codes[0] = cls.MOVETO
|
|||
|
vertex_offset = 1
|
|||
|
end = length
|
|||
|
|
|||
|
vertices[vertex_offset:end:3, 0] = xA + alpha * xA_dot
|
|||
|
vertices[vertex_offset:end:3, 1] = yA + alpha * yA_dot
|
|||
|
vertices[vertex_offset+1:end:3, 0] = xB - alpha * xB_dot
|
|||
|
vertices[vertex_offset+1:end:3, 1] = yB - alpha * yB_dot
|
|||
|
vertices[vertex_offset+2:end:3, 0] = xB
|
|||
|
vertices[vertex_offset+2:end:3, 1] = yB
|
|||
|
|
|||
|
return cls(vertices, codes, readonly=True)
|
|||
|
|
|||
|
@classmethod
|
|||
|
def wedge(cls, theta1, theta2, n=None):
|
|||
|
"""
|
|||
|
Return a `Path` for the unit circle wedge from angles *theta1* to
|
|||
|
*theta2* (in degrees).
|
|||
|
|
|||
|
*theta2* is unwrapped to produce the shortest wedge within 360 degrees.
|
|||
|
That is, if *theta2* > *theta1* + 360, the wedge will be from *theta1*
|
|||
|
to *theta2* - 360 and not a full circle plus some extra overlap.
|
|||
|
|
|||
|
If *n* is provided, it is the number of spline segments to make.
|
|||
|
If *n* is not provided, the number of spline segments is
|
|||
|
determined based on the delta between *theta1* and *theta2*.
|
|||
|
|
|||
|
See `Path.arc` for the reference on the approximation used.
|
|||
|
"""
|
|||
|
return cls.arc(theta1, theta2, n, True)
|
|||
|
|
|||
|
@staticmethod
|
|||
|
@lru_cache(8)
|
|||
|
def hatch(hatchpattern, density=6):
|
|||
|
"""
|
|||
|
Given a hatch specifier, *hatchpattern*, generates a `Path` that
|
|||
|
can be used in a repeated hatching pattern. *density* is the
|
|||
|
number of lines per unit square.
|
|||
|
"""
|
|||
|
from matplotlib.hatch import get_path
|
|||
|
return (get_path(hatchpattern, density)
|
|||
|
if hatchpattern is not None else None)
|
|||
|
|
|||
|
def clip_to_bbox(self, bbox, inside=True):
|
|||
|
"""
|
|||
|
Clip the path to the given bounding box.
|
|||
|
|
|||
|
The path must be made up of one or more closed polygons. This
|
|||
|
algorithm will not behave correctly for unclosed paths.
|
|||
|
|
|||
|
If *inside* is `True`, clip to the inside of the box, otherwise
|
|||
|
to the outside of the box.
|
|||
|
"""
|
|||
|
verts = _path.clip_path_to_rect(self, bbox, inside)
|
|||
|
paths = [Path(poly) for poly in verts]
|
|||
|
return self.make_compound_path(*paths)
|
|||
|
|
|||
|
|
|||
|
def get_path_collection_extents(
|
|||
|
master_transform, paths, transforms, offsets, offset_transform):
|
|||
|
r"""
|
|||
|
Get bounding box of a `.PathCollection`\s internal objects.
|
|||
|
|
|||
|
That is, given a sequence of `Path`\s, `.Transform`\s objects, and offsets, as found
|
|||
|
in a `.PathCollection`, return the bounding box that encapsulates all of them.
|
|||
|
|
|||
|
Parameters
|
|||
|
----------
|
|||
|
master_transform : `~matplotlib.transforms.Transform`
|
|||
|
Global transformation applied to all paths.
|
|||
|
paths : list of `Path`
|
|||
|
transforms : list of `~matplotlib.transforms.Affine2DBase`
|
|||
|
If non-empty, this overrides *master_transform*.
|
|||
|
offsets : (N, 2) array-like
|
|||
|
offset_transform : `~matplotlib.transforms.Affine2DBase`
|
|||
|
Transform applied to the offsets before offsetting the path.
|
|||
|
|
|||
|
Notes
|
|||
|
-----
|
|||
|
The way that *paths*, *transforms* and *offsets* are combined follows the same
|
|||
|
method as for collections: each is iterated over independently, so if you have 3
|
|||
|
paths (A, B, C), 2 transforms (α, β) and 1 offset (O), their combinations are as
|
|||
|
follows:
|
|||
|
|
|||
|
- (A, α, O)
|
|||
|
- (B, β, O)
|
|||
|
- (C, α, O)
|
|||
|
"""
|
|||
|
from .transforms import Bbox
|
|||
|
if len(paths) == 0:
|
|||
|
raise ValueError("No paths provided")
|
|||
|
if len(offsets) == 0:
|
|||
|
_api.warn_deprecated(
|
|||
|
"3.8", message="Calling get_path_collection_extents() with an"
|
|||
|
" empty offsets list is deprecated since %(since)s. Support will"
|
|||
|
" be removed %(removal)s.")
|
|||
|
extents, minpos = _path.get_path_collection_extents(
|
|||
|
master_transform, paths, np.atleast_3d(transforms),
|
|||
|
offsets, offset_transform)
|
|||
|
return Bbox.from_extents(*extents, minpos=minpos)
|