Coverage for /usr/lib/python3/dist-packages/matplotlib/path.py: 29%
381 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1r"""
2A module for dealing with the polylines used throughout Matplotlib.
4The primary class for polyline handling in Matplotlib is `Path`. Almost all
5vector drawing makes use of `Path`\s somewhere in the drawing pipeline.
7Whilst a `Path` instance itself cannot be drawn, some `.Artist` subclasses,
8such as `.PathPatch` and `.PathCollection`, can be used for convenient `Path`
9visualisation.
10"""
12import copy
13from functools import lru_cache
14from weakref import WeakValueDictionary
16import numpy as np
18import matplotlib as mpl
19from . import _api, _path
20from .cbook import _to_unmasked_float_array, simple_linear_interpolation
21from .bezier import BezierSegment
24class Path:
25 """
26 A series of possibly disconnected, possibly closed, line and curve
27 segments.
29 The underlying storage is made up of two parallel numpy arrays:
31 - *vertices*: an Nx2 float array of vertices
32 - *codes*: an N-length uint8 array of path codes, or None
34 These two arrays always have the same length in the first
35 dimension. For example, to represent a cubic curve, you must
36 provide three vertices and three ``CURVE4`` codes.
38 The code types are:
40 - ``STOP`` : 1 vertex (ignored)
41 A marker for the end of the entire path (currently not required and
42 ignored)
44 - ``MOVETO`` : 1 vertex
45 Pick up the pen and move to the given vertex.
47 - ``LINETO`` : 1 vertex
48 Draw a line from the current position to the given vertex.
50 - ``CURVE3`` : 1 control point, 1 endpoint
51 Draw a quadratic Bézier curve from the current position, with the given
52 control point, to the given end point.
54 - ``CURVE4`` : 2 control points, 1 endpoint
55 Draw a cubic Bézier curve from the current position, with the given
56 control points, to the given end point.
58 - ``CLOSEPOLY`` : 1 vertex (ignored)
59 Draw a line segment to the start point of the current polyline.
61 If *codes* is None, it is interpreted as a ``MOVETO`` followed by a series
62 of ``LINETO``.
64 Users of Path objects should not access the vertices and codes arrays
65 directly. Instead, they should use `iter_segments` or `cleaned` to get the
66 vertex/code pairs. This helps, in particular, to consistently handle the
67 case of *codes* being None.
69 Some behavior of Path objects can be controlled by rcParams. See the
70 rcParams whose keys start with 'path.'.
72 .. note::
74 The vertices and codes arrays should be treated as
75 immutable -- there are a number of optimizations and assumptions
76 made up front in the constructor that will not change when the
77 data changes.
78 """
80 code_type = np.uint8
82 # Path codes
83 STOP = code_type(0) # 1 vertex
84 MOVETO = code_type(1) # 1 vertex
85 LINETO = code_type(2) # 1 vertex
86 CURVE3 = code_type(3) # 2 vertices
87 CURVE4 = code_type(4) # 3 vertices
88 CLOSEPOLY = code_type(79) # 1 vertex
90 #: A dictionary mapping Path codes to the number of vertices that the
91 #: code expects.
92 NUM_VERTICES_FOR_CODE = {STOP: 1,
93 MOVETO: 1,
94 LINETO: 1,
95 CURVE3: 2,
96 CURVE4: 3,
97 CLOSEPOLY: 1}
99 def __init__(self, vertices, codes=None, _interpolation_steps=1,
100 closed=False, readonly=False):
101 """
102 Create a new path with the given vertices and codes.
104 Parameters
105 ----------
106 vertices : (N, 2) array-like
107 The path vertices, as an array, masked array or sequence of pairs.
108 Masked values, if any, will be converted to NaNs, which are then
109 handled correctly by the Agg PathIterator and other consumers of
110 path data, such as :meth:`iter_segments`.
111 codes : array-like or None, optional
112 N-length array of integers representing the codes of the path.
113 If not None, codes must be the same length as vertices.
114 If None, *vertices* will be treated as a series of line segments.
115 _interpolation_steps : int, optional
116 Used as a hint to certain projections, such as Polar, that this
117 path should be linearly interpolated immediately before drawing.
118 This attribute is primarily an implementation detail and is not
119 intended for public use.
120 closed : bool, optional
121 If *codes* is None and closed is True, vertices will be treated as
122 line segments of a closed polygon. Note that the last vertex will
123 then be ignored (as the corresponding code will be set to
124 CLOSEPOLY).
125 readonly : bool, optional
126 Makes the path behave in an immutable way and sets the vertices
127 and codes as read-only arrays.
128 """
129 vertices = _to_unmasked_float_array(vertices)
130 _api.check_shape((None, 2), vertices=vertices)
132 if codes is not None:
133 codes = np.asarray(codes, self.code_type)
134 if codes.ndim != 1 or len(codes) != len(vertices):
135 raise ValueError("'codes' must be a 1D list or array with the "
136 "same length of 'vertices'. "
137 f"Your vertices have shape {vertices.shape} "
138 f"but your codes have shape {codes.shape}")
139 if len(codes) and codes[0] != self.MOVETO:
140 raise ValueError("The first element of 'code' must be equal "
141 f"to 'MOVETO' ({self.MOVETO}). "
142 f"Your first code is {codes[0]}")
143 elif closed and len(vertices):
144 codes = np.empty(len(vertices), dtype=self.code_type)
145 codes[0] = self.MOVETO
146 codes[1:-1] = self.LINETO
147 codes[-1] = self.CLOSEPOLY
149 self._vertices = vertices
150 self._codes = codes
151 self._interpolation_steps = _interpolation_steps
152 self._update_values()
154 if readonly:
155 self._vertices.flags.writeable = False
156 if self._codes is not None:
157 self._codes.flags.writeable = False
158 self._readonly = True
159 else:
160 self._readonly = False
162 @classmethod
163 def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None):
164 """
165 Create a Path instance without the expense of calling the constructor.
167 Parameters
168 ----------
169 verts : numpy array
170 codes : numpy array
171 internals_from : Path or None
172 If not None, another `Path` from which the attributes
173 ``should_simplify``, ``simplify_threshold``, and
174 ``interpolation_steps`` will be copied. Note that ``readonly`` is
175 never copied, and always set to ``False`` by this constructor.
176 """
177 pth = cls.__new__(cls)
178 pth._vertices = _to_unmasked_float_array(verts)
179 pth._codes = codes
180 pth._readonly = False
181 if internals_from is not None:
182 pth._should_simplify = internals_from._should_simplify
183 pth._simplify_threshold = internals_from._simplify_threshold
184 pth._interpolation_steps = internals_from._interpolation_steps
185 else:
186 pth._should_simplify = True
187 pth._simplify_threshold = mpl.rcParams['path.simplify_threshold']
188 pth._interpolation_steps = 1
189 return pth
191 @classmethod
192 def _create_closed(cls, vertices):
193 """
194 Create a closed polygonal path going through *vertices*.
196 Unlike ``Path(..., closed=True)``, *vertices* should **not** end with
197 an entry for the CLOSEPATH; this entry is added by `._create_closed`.
198 """
199 v = _to_unmasked_float_array(vertices)
200 return cls(np.concatenate([v, v[:1]]), closed=True)
202 def _update_values(self):
203 self._simplify_threshold = mpl.rcParams['path.simplify_threshold']
204 self._should_simplify = (
205 self._simplify_threshold > 0 and
206 mpl.rcParams['path.simplify'] and
207 len(self._vertices) >= 128 and
208 (self._codes is None or np.all(self._codes <= Path.LINETO))
209 )
211 @property
212 def vertices(self):
213 """
214 The list of vertices in the `Path` as an Nx2 numpy array.
215 """
216 return self._vertices
218 @vertices.setter
219 def vertices(self, vertices):
220 if self._readonly:
221 raise AttributeError("Can't set vertices on a readonly Path")
222 self._vertices = vertices
223 self._update_values()
225 @property
226 def codes(self):
227 """
228 The list of codes in the `Path` as a 1D numpy array. Each
229 code is one of `STOP`, `MOVETO`, `LINETO`, `CURVE3`, `CURVE4`
230 or `CLOSEPOLY`. For codes that correspond to more than one
231 vertex (`CURVE3` and `CURVE4`), that code will be repeated so
232 that the length of `vertices` and `codes` is always
233 the same.
234 """
235 return self._codes
237 @codes.setter
238 def codes(self, codes):
239 if self._readonly:
240 raise AttributeError("Can't set codes on a readonly Path")
241 self._codes = codes
242 self._update_values()
244 @property
245 def simplify_threshold(self):
246 """
247 The fraction of a pixel difference below which vertices will
248 be simplified out.
249 """
250 return self._simplify_threshold
252 @simplify_threshold.setter
253 def simplify_threshold(self, threshold):
254 self._simplify_threshold = threshold
256 @property
257 def should_simplify(self):
258 """
259 `True` if the vertices array should be simplified.
260 """
261 return self._should_simplify
263 @should_simplify.setter
264 def should_simplify(self, should_simplify):
265 self._should_simplify = should_simplify
267 @property
268 def readonly(self):
269 """
270 `True` if the `Path` is read-only.
271 """
272 return self._readonly
274 def copy(self):
275 """
276 Return a shallow copy of the `Path`, which will share the
277 vertices and codes with the source `Path`.
278 """
279 return copy.copy(self)
281 def __deepcopy__(self, memo=None):
282 """
283 Return a deepcopy of the `Path`. The `Path` will not be
284 readonly, even if the source `Path` is.
285 """
286 # Deepcopying arrays (vertices, codes) strips the writeable=False flag.
287 p = copy.deepcopy(super(), memo)
288 p._readonly = False
289 return p
291 deepcopy = __deepcopy__
293 @classmethod
294 def make_compound_path_from_polys(cls, XY):
295 """
296 Make a compound path object to draw a number
297 of polygons with equal numbers of sides XY is a (numpolys x
298 numsides x 2) numpy array of vertices. Return object is a
299 :class:`Path`.
301 .. plot:: gallery/misc/histogram_path.py
303 """
305 # for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for
306 # the CLOSEPOLY; the vert for the closepoly is ignored but we still
307 # need it to keep the codes aligned with the vertices
308 numpolys, numsides, two = XY.shape
309 if two != 2:
310 raise ValueError("The third dimension of 'XY' must be 2")
311 stride = numsides + 1
312 nverts = numpolys * stride
313 verts = np.zeros((nverts, 2))
314 codes = np.full(nverts, cls.LINETO, dtype=cls.code_type)
315 codes[0::stride] = cls.MOVETO
316 codes[numsides::stride] = cls.CLOSEPOLY
317 for i in range(numsides):
318 verts[i::stride] = XY[:, i]
320 return cls(verts, codes)
322 @classmethod
323 def make_compound_path(cls, *args):
324 """
325 Make a compound path from a list of `Path` objects. Blindly removes
326 all `Path.STOP` control points.
327 """
328 # Handle an empty list in args (i.e. no args).
329 if not args:
330 return Path(np.empty([0, 2], dtype=np.float32))
331 vertices = np.concatenate([x.vertices for x in args])
332 codes = np.empty(len(vertices), dtype=cls.code_type)
333 i = 0
334 for path in args:
335 if path.codes is None:
336 codes[i] = cls.MOVETO
337 codes[i + 1:i + len(path.vertices)] = cls.LINETO
338 else:
339 codes[i:i + len(path.codes)] = path.codes
340 i += len(path.vertices)
341 # remove STOP's, since internal STOPs are a bug
342 not_stop_mask = codes != cls.STOP
343 vertices = vertices[not_stop_mask, :]
344 codes = codes[not_stop_mask]
346 return cls(vertices, codes)
348 def __repr__(self):
349 return "Path(%r, %r)" % (self.vertices, self.codes)
351 def __len__(self):
352 return len(self.vertices)
354 def iter_segments(self, transform=None, remove_nans=True, clip=None,
355 snap=False, stroke_width=1.0, simplify=None,
356 curves=True, sketch=None):
357 """
358 Iterate over all curve segments in the path.
360 Each iteration returns a pair ``(vertices, code)``, where ``vertices``
361 is a sequence of 1-3 coordinate pairs, and ``code`` is a `Path` code.
363 Additionally, this method can provide a number of standard cleanups and
364 conversions to the path.
366 Parameters
367 ----------
368 transform : None or :class:`~matplotlib.transforms.Transform`
369 If not None, the given affine transformation will be applied to the
370 path.
371 remove_nans : bool, optional
372 Whether to remove all NaNs from the path and skip over them using
373 MOVETO commands.
374 clip : None or (float, float, float, float), optional
375 If not None, must be a four-tuple (x1, y1, x2, y2)
376 defining a rectangle in which to clip the path.
377 snap : None or bool, optional
378 If True, snap all nodes to pixels; if False, don't snap them.
379 If None, snap if the path contains only segments
380 parallel to the x or y axes, and no more than 1024 of them.
381 stroke_width : float, optional
382 The width of the stroke being drawn (used for path snapping).
383 simplify : None or bool, optional
384 Whether to simplify the path by removing vertices
385 that do not affect its appearance. If None, use the
386 :attr:`should_simplify` attribute. See also :rc:`path.simplify`
387 and :rc:`path.simplify_threshold`.
388 curves : bool, optional
389 If True, curve segments will be returned as curve segments.
390 If False, all curves will be converted to line segments.
391 sketch : None or sequence, optional
392 If not None, must be a 3-tuple of the form
393 (scale, length, randomness), representing the sketch parameters.
394 """
395 if not len(self):
396 return
398 cleaned = self.cleaned(transform=transform,
399 remove_nans=remove_nans, clip=clip,
400 snap=snap, stroke_width=stroke_width,
401 simplify=simplify, curves=curves,
402 sketch=sketch)
404 # Cache these object lookups for performance in the loop.
405 NUM_VERTICES_FOR_CODE = self.NUM_VERTICES_FOR_CODE
406 STOP = self.STOP
408 vertices = iter(cleaned.vertices)
409 codes = iter(cleaned.codes)
410 for curr_vertices, code in zip(vertices, codes):
411 if code == STOP:
412 break
413 extra_vertices = NUM_VERTICES_FOR_CODE[code] - 1
414 if extra_vertices:
415 for i in range(extra_vertices):
416 next(codes)
417 curr_vertices = np.append(curr_vertices, next(vertices))
418 yield curr_vertices, code
420 def iter_bezier(self, **kwargs):
421 """
422 Iterate over each Bézier curve (lines included) in a Path.
424 Parameters
425 ----------
426 **kwargs
427 Forwarded to `.iter_segments`.
429 Yields
430 ------
431 B : matplotlib.bezier.BezierSegment
432 The Bézier curves that make up the current path. Note in particular
433 that freestanding points are Bézier curves of order 0, and lines
434 are Bézier curves of order 1 (with two control points).
435 code : Path.code_type
436 The code describing what kind of curve is being returned.
437 Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE4 correspond to
438 Bézier curves with 1, 2, 3, and 4 control points (respectively).
439 Path.CLOSEPOLY is a Path.LINETO with the control points correctly
440 chosen based on the start/end points of the current stroke.
441 """
442 first_vert = None
443 prev_vert = None
444 for verts, code in self.iter_segments(**kwargs):
445 if first_vert is None:
446 if code != Path.MOVETO:
447 raise ValueError("Malformed path, must start with MOVETO.")
448 if code == Path.MOVETO: # a point is like "CURVE1"
449 first_vert = verts
450 yield BezierSegment(np.array([first_vert])), code
451 elif code == Path.LINETO: # "CURVE2"
452 yield BezierSegment(np.array([prev_vert, verts])), code
453 elif code == Path.CURVE3:
454 yield BezierSegment(np.array([prev_vert, verts[:2],
455 verts[2:]])), code
456 elif code == Path.CURVE4:
457 yield BezierSegment(np.array([prev_vert, verts[:2],
458 verts[2:4], verts[4:]])), code
459 elif code == Path.CLOSEPOLY:
460 yield BezierSegment(np.array([prev_vert, first_vert])), code
461 elif code == Path.STOP:
462 return
463 else:
464 raise ValueError("Invalid Path.code_type: " + str(code))
465 prev_vert = verts[-2:]
467 def cleaned(self, transform=None, remove_nans=False, clip=None,
468 *, simplify=False, curves=False,
469 stroke_width=1.0, snap=False, sketch=None):
470 """
471 Return a new Path with vertices and codes cleaned according to the
472 parameters.
474 See Also
475 --------
476 Path.iter_segments : for details of the keyword arguments.
477 """
478 vertices, codes = _path.cleanup_path(
479 self, transform, remove_nans, clip, snap, stroke_width, simplify,
480 curves, sketch)
481 pth = Path._fast_from_codes_and_verts(vertices, codes, self)
482 if not simplify:
483 pth._should_simplify = False
484 return pth
486 def transformed(self, transform):
487 """
488 Return a transformed copy of the path.
490 See Also
491 --------
492 matplotlib.transforms.TransformedPath
493 A specialized path class that will cache the transformed result and
494 automatically update when the transform changes.
495 """
496 return Path(transform.transform(self.vertices), self.codes,
497 self._interpolation_steps)
499 def contains_point(self, point, transform=None, radius=0.0):
500 """
501 Return whether the area enclosed by the path contains the given point.
503 The path is always treated as closed; i.e. if the last code is not
504 CLOSEPOLY an implicit segment connecting the last vertex to the first
505 vertex is assumed.
507 Parameters
508 ----------
509 point : (float, float)
510 The point (x, y) to check.
511 transform : `matplotlib.transforms.Transform`, optional
512 If not ``None``, *point* will be compared to ``self`` transformed
513 by *transform*; i.e. for a correct check, *transform* should
514 transform the path into the coordinate system of *point*.
515 radius : float, default: 0
516 Additional margin on the path in coordinates of *point*.
517 The path is extended tangentially by *radius/2*; i.e. if you would
518 draw the path with a linewidth of *radius*, all points on the line
519 would still be considered to be contained in the area. Conversely,
520 negative values shrink the area: Points on the imaginary line
521 will be considered outside the area.
523 Returns
524 -------
525 bool
527 Notes
528 -----
529 The current algorithm has some limitations:
531 - The result is undefined for points exactly at the boundary
532 (i.e. at the path shifted by *radius/2*).
533 - The result is undefined if there is no enclosed area, i.e. all
534 vertices are on a straight line.
535 - If bounding lines start to cross each other due to *radius* shift,
536 the result is not guaranteed to be correct.
537 """
538 if transform is not None:
539 transform = transform.frozen()
540 # `point_in_path` does not handle nonlinear transforms, so we
541 # transform the path ourselves. If *transform* is affine, letting
542 # `point_in_path` handle the transform avoids allocating an extra
543 # buffer.
544 if transform and not transform.is_affine:
545 self = transform.transform_path(self)
546 transform = None
547 return _path.point_in_path(point[0], point[1], radius, self, transform)
549 def contains_points(self, points, transform=None, radius=0.0):
550 """
551 Return whether the area enclosed by the path contains the given points.
553 The path is always treated as closed; i.e. if the last code is not
554 CLOSEPOLY an implicit segment connecting the last vertex to the first
555 vertex is assumed.
557 Parameters
558 ----------
559 points : (N, 2) array
560 The points to check. Columns contain x and y values.
561 transform : `matplotlib.transforms.Transform`, optional
562 If not ``None``, *points* will be compared to ``self`` transformed
563 by *transform*; i.e. for a correct check, *transform* should
564 transform the path into the coordinate system of *points*.
565 radius : float, default: 0
566 Additional margin on the path in coordinates of *points*.
567 The path is extended tangentially by *radius/2*; i.e. if you would
568 draw the path with a linewidth of *radius*, all points on the line
569 would still be considered to be contained in the area. Conversely,
570 negative values shrink the area: Points on the imaginary line
571 will be considered outside the area.
573 Returns
574 -------
575 length-N bool array
577 Notes
578 -----
579 The current algorithm has some limitations:
581 - The result is undefined for points exactly at the boundary
582 (i.e. at the path shifted by *radius/2*).
583 - The result is undefined if there is no enclosed area, i.e. all
584 vertices are on a straight line.
585 - If bounding lines start to cross each other due to *radius* shift,
586 the result is not guaranteed to be correct.
587 """
588 if transform is not None:
589 transform = transform.frozen()
590 result = _path.points_in_path(points, radius, self, transform)
591 return result.astype('bool')
593 def contains_path(self, path, transform=None):
594 """
595 Return whether this (closed) path completely contains the given path.
597 If *transform* is not ``None``, the path will be transformed before
598 checking for containment.
599 """
600 if transform is not None:
601 transform = transform.frozen()
602 return _path.path_in_path(self, None, path, transform)
604 def get_extents(self, transform=None, **kwargs):
605 """
606 Get Bbox of the path.
608 Parameters
609 ----------
610 transform : matplotlib.transforms.Transform, optional
611 Transform to apply to path before computing extents, if any.
612 **kwargs
613 Forwarded to `.iter_bezier`.
615 Returns
616 -------
617 matplotlib.transforms.Bbox
618 The extents of the path Bbox([[xmin, ymin], [xmax, ymax]])
619 """
620 from .transforms import Bbox
621 if transform is not None:
622 self = transform.transform_path(self)
623 if self.codes is None:
624 xys = self.vertices
625 elif len(np.intersect1d(self.codes, [Path.CURVE3, Path.CURVE4])) == 0:
626 # Optimization for the straight line case.
627 # Instead of iterating through each curve, consider
628 # each line segment's end-points
629 # (recall that STOP and CLOSEPOLY vertices are ignored)
630 xys = self.vertices[np.isin(self.codes,
631 [Path.MOVETO, Path.LINETO])]
632 else:
633 xys = []
634 for curve, code in self.iter_bezier(**kwargs):
635 # places where the derivative is zero can be extrema
636 _, dzeros = curve.axis_aligned_extrema()
637 # as can the ends of the curve
638 xys.append(curve([0, *dzeros, 1]))
639 xys = np.concatenate(xys)
640 if len(xys):
641 return Bbox([xys.min(axis=0), xys.max(axis=0)])
642 else:
643 return Bbox.null()
645 def intersects_path(self, other, filled=True):
646 """
647 Return whether if this path intersects another given path.
649 If *filled* is True, then this also returns True if one path completely
650 encloses the other (i.e., the paths are treated as filled).
651 """
652 return _path.path_intersects_path(self, other, filled)
654 def intersects_bbox(self, bbox, filled=True):
655 """
656 Return whether this path intersects a given `~.transforms.Bbox`.
658 If *filled* is True, then this also returns True if the path completely
659 encloses the `.Bbox` (i.e., the path is treated as filled).
661 The bounding box is always considered filled.
662 """
663 return _path.path_intersects_rectangle(
664 self, bbox.x0, bbox.y0, bbox.x1, bbox.y1, filled)
666 def interpolated(self, steps):
667 """
668 Return a new path resampled to length N x steps.
670 Codes other than LINETO are not handled correctly.
671 """
672 if steps == 1:
673 return self
675 vertices = simple_linear_interpolation(self.vertices, steps)
676 codes = self.codes
677 if codes is not None:
678 new_codes = np.full((len(codes) - 1) * steps + 1, Path.LINETO,
679 dtype=self.code_type)
680 new_codes[0::steps] = codes
681 else:
682 new_codes = None
683 return Path(vertices, new_codes)
685 def to_polygons(self, transform=None, width=0, height=0, closed_only=True):
686 """
687 Convert this path to a list of polygons or polylines. Each
688 polygon/polyline is an Nx2 array of vertices. In other words,
689 each polygon has no ``MOVETO`` instructions or curves. This
690 is useful for displaying in backends that do not support
691 compound paths or Bézier curves.
693 If *width* and *height* are both non-zero then the lines will
694 be simplified so that vertices outside of (0, 0), (width,
695 height) will be clipped.
697 If *closed_only* is `True` (default), only closed polygons,
698 with the last point being the same as the first point, will be
699 returned. Any unclosed polylines in the path will be
700 explicitly closed. If *closed_only* is `False`, any unclosed
701 polygons in the path will be returned as unclosed polygons,
702 and the closed polygons will be returned explicitly closed by
703 setting the last point to the same as the first point.
704 """
705 if len(self.vertices) == 0:
706 return []
708 if transform is not None:
709 transform = transform.frozen()
711 if self.codes is None and (width == 0 or height == 0):
712 vertices = self.vertices
713 if closed_only:
714 if len(vertices) < 3:
715 return []
716 elif np.any(vertices[0] != vertices[-1]):
717 vertices = [*vertices, vertices[0]]
719 if transform is None:
720 return [vertices]
721 else:
722 return [transform.transform(vertices)]
724 # Deal with the case where there are curves and/or multiple
725 # subpaths (using extension code)
726 return _path.convert_path_to_polygons(
727 self, transform, width, height, closed_only)
729 _unit_rectangle = None
731 @classmethod
732 def unit_rectangle(cls):
733 """
734 Return a `Path` instance of the unit rectangle from (0, 0) to (1, 1).
735 """
736 if cls._unit_rectangle is None:
737 cls._unit_rectangle = cls([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]],
738 closed=True, readonly=True)
739 return cls._unit_rectangle
741 _unit_regular_polygons = WeakValueDictionary()
743 @classmethod
744 def unit_regular_polygon(cls, numVertices):
745 """
746 Return a :class:`Path` instance for a unit regular polygon with the
747 given *numVertices* such that the circumscribing circle has radius 1.0,
748 centered at (0, 0).
749 """
750 if numVertices <= 16:
751 path = cls._unit_regular_polygons.get(numVertices)
752 else:
753 path = None
754 if path is None:
755 theta = ((2 * np.pi / numVertices) * np.arange(numVertices + 1)
756 # This initial rotation is to make sure the polygon always
757 # "points-up".
758 + np.pi / 2)
759 verts = np.column_stack((np.cos(theta), np.sin(theta)))
760 path = cls(verts, closed=True, readonly=True)
761 if numVertices <= 16:
762 cls._unit_regular_polygons[numVertices] = path
763 return path
765 _unit_regular_stars = WeakValueDictionary()
767 @classmethod
768 def unit_regular_star(cls, numVertices, innerCircle=0.5):
769 """
770 Return a :class:`Path` for a unit regular star with the given
771 numVertices and radius of 1.0, centered at (0, 0).
772 """
773 if numVertices <= 16:
774 path = cls._unit_regular_stars.get((numVertices, innerCircle))
775 else:
776 path = None
777 if path is None:
778 ns2 = numVertices * 2
779 theta = (2*np.pi/ns2 * np.arange(ns2 + 1))
780 # This initial rotation is to make sure the polygon always
781 # "points-up"
782 theta += np.pi / 2.0
783 r = np.ones(ns2 + 1)
784 r[1::2] = innerCircle
785 verts = (r * np.vstack((np.cos(theta), np.sin(theta)))).T
786 path = cls(verts, closed=True, readonly=True)
787 if numVertices <= 16:
788 cls._unit_regular_stars[(numVertices, innerCircle)] = path
789 return path
791 @classmethod
792 def unit_regular_asterisk(cls, numVertices):
793 """
794 Return a :class:`Path` for a unit regular asterisk with the given
795 numVertices and radius of 1.0, centered at (0, 0).
796 """
797 return cls.unit_regular_star(numVertices, 0.0)
799 _unit_circle = None
801 @classmethod
802 def unit_circle(cls):
803 """
804 Return the readonly :class:`Path` of the unit circle.
806 For most cases, :func:`Path.circle` will be what you want.
807 """
808 if cls._unit_circle is None:
809 cls._unit_circle = cls.circle(center=(0, 0), radius=1,
810 readonly=True)
811 return cls._unit_circle
813 @classmethod
814 def circle(cls, center=(0., 0.), radius=1., readonly=False):
815 """
816 Return a `Path` representing a circle of a given radius and center.
818 Parameters
819 ----------
820 center : (float, float), default: (0, 0)
821 The center of the circle.
822 radius : float, default: 1
823 The radius of the circle.
824 readonly : bool
825 Whether the created path should have the "readonly" argument
826 set when creating the Path instance.
828 Notes
829 -----
830 The circle is approximated using 8 cubic Bézier curves, as described in
832 Lancaster, Don. `Approximating a Circle or an Ellipse Using Four
833 Bezier Cubic Splines <https://www.tinaja.com/glib/ellipse4.pdf>`_.
834 """
835 MAGIC = 0.2652031
836 SQRTHALF = np.sqrt(0.5)
837 MAGIC45 = SQRTHALF * MAGIC
839 vertices = np.array([[0.0, -1.0],
841 [MAGIC, -1.0],
842 [SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45],
843 [SQRTHALF, -SQRTHALF],
845 [SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45],
846 [1.0, -MAGIC],
847 [1.0, 0.0],
849 [1.0, MAGIC],
850 [SQRTHALF+MAGIC45, SQRTHALF-MAGIC45],
851 [SQRTHALF, SQRTHALF],
853 [SQRTHALF-MAGIC45, SQRTHALF+MAGIC45],
854 [MAGIC, 1.0],
855 [0.0, 1.0],
857 [-MAGIC, 1.0],
858 [-SQRTHALF+MAGIC45, SQRTHALF+MAGIC45],
859 [-SQRTHALF, SQRTHALF],
861 [-SQRTHALF-MAGIC45, SQRTHALF-MAGIC45],
862 [-1.0, MAGIC],
863 [-1.0, 0.0],
865 [-1.0, -MAGIC],
866 [-SQRTHALF-MAGIC45, -SQRTHALF+MAGIC45],
867 [-SQRTHALF, -SQRTHALF],
869 [-SQRTHALF+MAGIC45, -SQRTHALF-MAGIC45],
870 [-MAGIC, -1.0],
871 [0.0, -1.0],
873 [0.0, -1.0]],
874 dtype=float)
876 codes = [cls.CURVE4] * 26
877 codes[0] = cls.MOVETO
878 codes[-1] = cls.CLOSEPOLY
879 return Path(vertices * radius + center, codes, readonly=readonly)
881 _unit_circle_righthalf = None
883 @classmethod
884 def unit_circle_righthalf(cls):
885 """
886 Return a `Path` of the right half of a unit circle.
888 See `Path.circle` for the reference on the approximation used.
889 """
890 if cls._unit_circle_righthalf is None:
891 MAGIC = 0.2652031
892 SQRTHALF = np.sqrt(0.5)
893 MAGIC45 = SQRTHALF * MAGIC
895 vertices = np.array(
896 [[0.0, -1.0],
898 [MAGIC, -1.0],
899 [SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45],
900 [SQRTHALF, -SQRTHALF],
902 [SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45],
903 [1.0, -MAGIC],
904 [1.0, 0.0],
906 [1.0, MAGIC],
907 [SQRTHALF+MAGIC45, SQRTHALF-MAGIC45],
908 [SQRTHALF, SQRTHALF],
910 [SQRTHALF-MAGIC45, SQRTHALF+MAGIC45],
911 [MAGIC, 1.0],
912 [0.0, 1.0],
914 [0.0, -1.0]],
916 float)
918 codes = np.full(14, cls.CURVE4, dtype=cls.code_type)
919 codes[0] = cls.MOVETO
920 codes[-1] = cls.CLOSEPOLY
922 cls._unit_circle_righthalf = cls(vertices, codes, readonly=True)
923 return cls._unit_circle_righthalf
925 @classmethod
926 def arc(cls, theta1, theta2, n=None, is_wedge=False):
927 """
928 Return a `Path` for the unit circle arc from angles *theta1* to
929 *theta2* (in degrees).
931 *theta2* is unwrapped to produce the shortest arc within 360 degrees.
932 That is, if *theta2* > *theta1* + 360, the arc will be from *theta1* to
933 *theta2* - 360 and not a full circle plus some extra overlap.
935 If *n* is provided, it is the number of spline segments to make.
936 If *n* is not provided, the number of spline segments is
937 determined based on the delta between *theta1* and *theta2*.
939 Masionobe, L. 2003. `Drawing an elliptical arc using
940 polylines, quadratic or cubic Bezier curves
941 <http://www.spaceroots.org/documents/ellipse/index.html>`_.
942 """
943 halfpi = np.pi * 0.5
945 eta1 = theta1
946 eta2 = theta2 - 360 * np.floor((theta2 - theta1) / 360)
947 # Ensure 2pi range is not flattened to 0 due to floating-point errors,
948 # but don't try to expand existing 0 range.
949 if theta2 != theta1 and eta2 <= eta1:
950 eta2 += 360
951 eta1, eta2 = np.deg2rad([eta1, eta2])
953 # number of curve segments to make
954 if n is None:
955 n = int(2 ** np.ceil((eta2 - eta1) / halfpi))
956 if n < 1:
957 raise ValueError("n must be >= 1 or None")
959 deta = (eta2 - eta1) / n
960 t = np.tan(0.5 * deta)
961 alpha = np.sin(deta) * (np.sqrt(4.0 + 3.0 * t * t) - 1) / 3.0
963 steps = np.linspace(eta1, eta2, n + 1, True)
964 cos_eta = np.cos(steps)
965 sin_eta = np.sin(steps)
967 xA = cos_eta[:-1]
968 yA = sin_eta[:-1]
969 xA_dot = -yA
970 yA_dot = xA
972 xB = cos_eta[1:]
973 yB = sin_eta[1:]
974 xB_dot = -yB
975 yB_dot = xB
977 if is_wedge:
978 length = n * 3 + 4
979 vertices = np.zeros((length, 2), float)
980 codes = np.full(length, cls.CURVE4, dtype=cls.code_type)
981 vertices[1] = [xA[0], yA[0]]
982 codes[0:2] = [cls.MOVETO, cls.LINETO]
983 codes[-2:] = [cls.LINETO, cls.CLOSEPOLY]
984 vertex_offset = 2
985 end = length - 2
986 else:
987 length = n * 3 + 1
988 vertices = np.empty((length, 2), float)
989 codes = np.full(length, cls.CURVE4, dtype=cls.code_type)
990 vertices[0] = [xA[0], yA[0]]
991 codes[0] = cls.MOVETO
992 vertex_offset = 1
993 end = length
995 vertices[vertex_offset:end:3, 0] = xA + alpha * xA_dot
996 vertices[vertex_offset:end:3, 1] = yA + alpha * yA_dot
997 vertices[vertex_offset+1:end:3, 0] = xB - alpha * xB_dot
998 vertices[vertex_offset+1:end:3, 1] = yB - alpha * yB_dot
999 vertices[vertex_offset+2:end:3, 0] = xB
1000 vertices[vertex_offset+2:end:3, 1] = yB
1002 return cls(vertices, codes, readonly=True)
1004 @classmethod
1005 def wedge(cls, theta1, theta2, n=None):
1006 """
1007 Return a `Path` for the unit circle wedge from angles *theta1* to
1008 *theta2* (in degrees).
1010 *theta2* is unwrapped to produce the shortest wedge within 360 degrees.
1011 That is, if *theta2* > *theta1* + 360, the wedge will be from *theta1*
1012 to *theta2* - 360 and not a full circle plus some extra overlap.
1014 If *n* is provided, it is the number of spline segments to make.
1015 If *n* is not provided, the number of spline segments is
1016 determined based on the delta between *theta1* and *theta2*.
1018 See `Path.arc` for the reference on the approximation used.
1019 """
1020 return cls.arc(theta1, theta2, n, True)
1022 @staticmethod
1023 @lru_cache(8)
1024 def hatch(hatchpattern, density=6):
1025 """
1026 Given a hatch specifier, *hatchpattern*, generates a Path that
1027 can be used in a repeated hatching pattern. *density* is the
1028 number of lines per unit square.
1029 """
1030 from matplotlib.hatch import get_path
1031 return (get_path(hatchpattern, density)
1032 if hatchpattern is not None else None)
1034 def clip_to_bbox(self, bbox, inside=True):
1035 """
1036 Clip the path to the given bounding box.
1038 The path must be made up of one or more closed polygons. This
1039 algorithm will not behave correctly for unclosed paths.
1041 If *inside* is `True`, clip to the inside of the box, otherwise
1042 to the outside of the box.
1043 """
1044 # Use make_compound_path_from_polys
1045 verts = _path.clip_path_to_rect(self, bbox, inside)
1046 paths = [Path(poly) for poly in verts]
1047 return self.make_compound_path(*paths)
1050def get_path_collection_extents(
1051 master_transform, paths, transforms, offsets, offset_transform):
1052 r"""
1053 Given a sequence of `Path`\s, `.Transform`\s objects, and offsets, as
1054 found in a `.PathCollection`, returns the bounding box that encapsulates
1055 all of them.
1057 Parameters
1058 ----------
1059 master_transform : `.Transform`
1060 Global transformation applied to all paths.
1061 paths : list of `Path`
1062 transforms : list of `.Affine2D`
1063 offsets : (N, 2) array-like
1064 offset_transform : `.Affine2D`
1065 Transform applied to the offsets before offsetting the path.
1067 Notes
1068 -----
1069 The way that *paths*, *transforms* and *offsets* are combined
1070 follows the same method as for collections: Each is iterated over
1071 independently, so if you have 3 paths, 2 transforms and 1 offset,
1072 their combinations are as follows:
1074 (A, A, A), (B, B, A), (C, A, A)
1075 """
1076 from .transforms import Bbox
1077 if len(paths) == 0:
1078 raise ValueError("No paths provided")
1079 extents, minpos = _path.get_path_collection_extents(
1080 master_transform, paths, np.atleast_3d(transforms),
1081 offsets, offset_transform)
1082 return Bbox.from_extents(*extents, minpos=minpos)