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 Bzier curve from the current position, with the given
control point, to the given end point.
- `CURVE4` : 2 control points, 1 endpoint
Draw a cubic Bzier 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 and len(vertices):
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