Coverage for /usr/lib/python3/dist-packages/matplotlib/path.py: 52%

381 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1r""" 

2A module for dealing with the polylines used throughout Matplotlib. 

3 

4The primary class for polyline handling in Matplotlib is `Path`. Almost all 

5vector drawing makes use of `Path`\s somewhere in the drawing pipeline. 

6 

7Whilst a `Path` instance itself cannot be drawn, some `.Artist` subclasses, 

8such as `.PathPatch` and `.PathCollection`, can be used for convenient `Path` 

9visualisation. 

10""" 

11 

12import copy 

13from functools import lru_cache 

14from weakref import WeakValueDictionary 

15 

16import numpy as np 

17 

18import matplotlib as mpl 

19from . import _api, _path 

20from .cbook import _to_unmasked_float_array, simple_linear_interpolation 

21from .bezier import BezierSegment 

22 

23 

24class Path: 

25 """ 

26 A series of possibly disconnected, possibly closed, line and curve 

27 segments. 

28 

29 The underlying storage is made up of two parallel numpy arrays: 

30 

31 - *vertices*: an Nx2 float array of vertices 

32 - *codes*: an N-length uint8 array of path codes, or None 

33 

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. 

37 

38 The code types are: 

39 

40 - ``STOP`` : 1 vertex (ignored) 

41 A marker for the end of the entire path (currently not required and 

42 ignored) 

43 

44 - ``MOVETO`` : 1 vertex 

45 Pick up the pen and move to the given vertex. 

46 

47 - ``LINETO`` : 1 vertex 

48 Draw a line from the current position to the given vertex. 

49 

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. 

53 

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. 

57 

58 - ``CLOSEPOLY`` : 1 vertex (ignored) 

59 Draw a line segment to the start point of the current polyline. 

60 

61 If *codes* is None, it is interpreted as a ``MOVETO`` followed by a series 

62 of ``LINETO``. 

63 

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. 

68 

69 Some behavior of Path objects can be controlled by rcParams. See the 

70 rcParams whose keys start with 'path.'. 

71 

72 .. note:: 

73 

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 """ 

79 

80 code_type = np.uint8 

81 

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 

89 

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} 

98 

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. 

103 

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) 

131 

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 

148 

149 self._vertices = vertices 

150 self._codes = codes 

151 self._interpolation_steps = _interpolation_steps 

152 self._update_values() 

153 

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 

161 

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. 

166 

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 

190 

191 @classmethod 

192 def _create_closed(cls, vertices): 

193 """ 

194 Create a closed polygonal path going through *vertices*. 

195 

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) 

201 

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 ) 

210 

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 

217 

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() 

224 

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 

236 

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() 

243 

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 

251 

252 @simplify_threshold.setter 

253 def simplify_threshold(self, threshold): 

254 self._simplify_threshold = threshold 

255 

256 @property 

257 def should_simplify(self): 

258 """ 

259 `True` if the vertices array should be simplified. 

260 """ 

261 return self._should_simplify 

262 

263 @should_simplify.setter 

264 def should_simplify(self, should_simplify): 

265 self._should_simplify = should_simplify 

266 

267 @property 

268 def readonly(self): 

269 """ 

270 `True` if the `Path` is read-only. 

271 """ 

272 return self._readonly 

273 

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) 

280 

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 

290 

291 deepcopy = __deepcopy__ 

292 

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`. 

300 

301 .. plot:: gallery/misc/histogram_path.py 

302 

303 """ 

304 

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] 

319 

320 return cls(verts, codes) 

321 

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] 

345 

346 return cls(vertices, codes) 

347 

348 def __repr__(self): 

349 return "Path(%r, %r)" % (self.vertices, self.codes) 

350 

351 def __len__(self): 

352 return len(self.vertices) 

353 

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. 

359 

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. 

362 

363 Additionally, this method can provide a number of standard cleanups and 

364 conversions to the path. 

365 

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 

397 

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) 

403 

404 # Cache these object lookups for performance in the loop. 

405 NUM_VERTICES_FOR_CODE = self.NUM_VERTICES_FOR_CODE 

406 STOP = self.STOP 

407 

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 

419 

420 def iter_bezier(self, **kwargs): 

421 """ 

422 Iterate over each Bézier curve (lines included) in a Path. 

423 

424 Parameters 

425 ---------- 

426 **kwargs 

427 Forwarded to `.iter_segments`. 

428 

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:] 

466 

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. 

473 

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 

485 

486 def transformed(self, transform): 

487 """ 

488 Return a transformed copy of the path. 

489 

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) 

498 

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. 

502 

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. 

506 

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. 

522 

523 Returns 

524 ------- 

525 bool 

526 

527 Notes 

528 ----- 

529 The current algorithm has some limitations: 

530 

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) 

548 

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. 

552 

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. 

556 

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. 

572 

573 Returns 

574 ------- 

575 length-N bool array 

576 

577 Notes 

578 ----- 

579 The current algorithm has some limitations: 

580 

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') 

592 

593 def contains_path(self, path, transform=None): 

594 """ 

595 Return whether this (closed) path completely contains the given path. 

596 

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) 

603 

604 def get_extents(self, transform=None, **kwargs): 

605 """ 

606 Get Bbox of the path. 

607 

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`. 

614 

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() 

644 

645 def intersects_path(self, other, filled=True): 

646 """ 

647 Return whether if this path intersects another given path. 

648 

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) 

653 

654 def intersects_bbox(self, bbox, filled=True): 

655 """ 

656 Return whether this path intersects a given `~.transforms.Bbox`. 

657 

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). 

660 

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) 

665 

666 def interpolated(self, steps): 

667 """ 

668 Return a new path resampled to length N x steps. 

669 

670 Codes other than LINETO are not handled correctly. 

671 """ 

672 if steps == 1: 

673 return self 

674 

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) 

684 

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. 

692 

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. 

696 

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 [] 

707 

708 if transform is not None: 

709 transform = transform.frozen() 

710 

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]] 

718 

719 if transform is None: 

720 return [vertices] 

721 else: 

722 return [transform.transform(vertices)] 

723 

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) 

728 

729 _unit_rectangle = None 

730 

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 

740 

741 _unit_regular_polygons = WeakValueDictionary() 

742 

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 

764 

765 _unit_regular_stars = WeakValueDictionary() 

766 

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 

790 

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) 

798 

799 _unit_circle = None 

800 

801 @classmethod 

802 def unit_circle(cls): 

803 """ 

804 Return the readonly :class:`Path` of the unit circle. 

805 

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 

812 

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. 

817 

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. 

827 

828 Notes 

829 ----- 

830 The circle is approximated using 8 cubic Bézier curves, as described in 

831 

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 

838 

839 vertices = np.array([[0.0, -1.0], 

840 

841 [MAGIC, -1.0], 

842 [SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45], 

843 [SQRTHALF, -SQRTHALF], 

844 

845 [SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45], 

846 [1.0, -MAGIC], 

847 [1.0, 0.0], 

848 

849 [1.0, MAGIC], 

850 [SQRTHALF+MAGIC45, SQRTHALF-MAGIC45], 

851 [SQRTHALF, SQRTHALF], 

852 

853 [SQRTHALF-MAGIC45, SQRTHALF+MAGIC45], 

854 [MAGIC, 1.0], 

855 [0.0, 1.0], 

856 

857 [-MAGIC, 1.0], 

858 [-SQRTHALF+MAGIC45, SQRTHALF+MAGIC45], 

859 [-SQRTHALF, SQRTHALF], 

860 

861 [-SQRTHALF-MAGIC45, SQRTHALF-MAGIC45], 

862 [-1.0, MAGIC], 

863 [-1.0, 0.0], 

864 

865 [-1.0, -MAGIC], 

866 [-SQRTHALF-MAGIC45, -SQRTHALF+MAGIC45], 

867 [-SQRTHALF, -SQRTHALF], 

868 

869 [-SQRTHALF+MAGIC45, -SQRTHALF-MAGIC45], 

870 [-MAGIC, -1.0], 

871 [0.0, -1.0], 

872 

873 [0.0, -1.0]], 

874 dtype=float) 

875 

876 codes = [cls.CURVE4] * 26 

877 codes[0] = cls.MOVETO 

878 codes[-1] = cls.CLOSEPOLY 

879 return Path(vertices * radius + center, codes, readonly=readonly) 

880 

881 _unit_circle_righthalf = None 

882 

883 @classmethod 

884 def unit_circle_righthalf(cls): 

885 """ 

886 Return a `Path` of the right half of a unit circle. 

887 

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 

894 

895 vertices = np.array( 

896 [[0.0, -1.0], 

897 

898 [MAGIC, -1.0], 

899 [SQRTHALF-MAGIC45, -SQRTHALF-MAGIC45], 

900 [SQRTHALF, -SQRTHALF], 

901 

902 [SQRTHALF+MAGIC45, -SQRTHALF+MAGIC45], 

903 [1.0, -MAGIC], 

904 [1.0, 0.0], 

905 

906 [1.0, MAGIC], 

907 [SQRTHALF+MAGIC45, SQRTHALF-MAGIC45], 

908 [SQRTHALF, SQRTHALF], 

909 

910 [SQRTHALF-MAGIC45, SQRTHALF+MAGIC45], 

911 [MAGIC, 1.0], 

912 [0.0, 1.0], 

913 

914 [0.0, -1.0]], 

915 

916 float) 

917 

918 codes = np.full(14, cls.CURVE4, dtype=cls.code_type) 

919 codes[0] = cls.MOVETO 

920 codes[-1] = cls.CLOSEPOLY 

921 

922 cls._unit_circle_righthalf = cls(vertices, codes, readonly=True) 

923 return cls._unit_circle_righthalf 

924 

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). 

930 

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. 

934 

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*. 

938 

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 

944 

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]) 

952 

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") 

958 

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 

962 

963 steps = np.linspace(eta1, eta2, n + 1, True) 

964 cos_eta = np.cos(steps) 

965 sin_eta = np.sin(steps) 

966 

967 xA = cos_eta[:-1] 

968 yA = sin_eta[:-1] 

969 xA_dot = -yA 

970 yA_dot = xA 

971 

972 xB = cos_eta[1:] 

973 yB = sin_eta[1:] 

974 xB_dot = -yB 

975 yB_dot = xB 

976 

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 

994 

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 

1001 

1002 return cls(vertices, codes, readonly=True) 

1003 

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). 

1009 

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. 

1013 

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*. 

1017 

1018 See `Path.arc` for the reference on the approximation used. 

1019 """ 

1020 return cls.arc(theta1, theta2, n, True) 

1021 

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) 

1033 

1034 def clip_to_bbox(self, bbox, inside=True): 

1035 """ 

1036 Clip the path to the given bounding box. 

1037 

1038 The path must be made up of one or more closed polygons. This 

1039 algorithm will not behave correctly for unclosed paths. 

1040 

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) 

1048 

1049 

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. 

1056 

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. 

1066 

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: 

1073 

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)