Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/transforms.py: 32%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1185 statements  

1""" 

2Matplotlib includes a framework for arbitrary geometric 

3transformations that is used determine the final position of all 

4elements drawn on the canvas. 

5 

6Transforms are composed into trees of `TransformNode` objects 

7whose actual value depends on their children. When the contents of 

8children change, their parents are automatically invalidated. The 

9next time an invalidated transform is accessed, it is recomputed to 

10reflect those changes. This invalidation/caching approach prevents 

11unnecessary recomputations of transforms, and contributes to better 

12interactive performance. 

13 

14For example, here is a graph of the transform tree used to plot data 

15to the graph: 

16 

17.. image:: ../_static/transforms.png 

18 

19The framework can be used for both affine and non-affine 

20transformations. However, for speed, we want to use the backend 

21renderers to perform affine transformations whenever possible. 

22Therefore, it is possible to perform just the affine or non-affine 

23part of a transformation on a set of data. The affine is always 

24assumed to occur after the non-affine. For any transform:: 

25 

26 full transform == non-affine part + affine part 

27 

28The backends are not expected to handle non-affine transformations 

29themselves. 

30 

31See the tutorial :ref:`transforms_tutorial` for examples 

32of how to use transforms. 

33""" 

34 

35# Note: There are a number of places in the code where we use `np.min` or 

36# `np.minimum` instead of the builtin `min`, and likewise for `max`. This is 

37# done so that `nan`s are propagated, instead of being silently dropped. 

38 

39import copy 

40import functools 

41import textwrap 

42import weakref 

43import math 

44 

45import numpy as np 

46from numpy.linalg import inv 

47 

48from matplotlib import _api 

49from matplotlib._path import ( 

50 affine_transform, count_bboxes_overlapping_bbox, update_path_extents) 

51from .path import Path 

52 

53DEBUG = False 

54 

55 

56def _make_str_method(*args, **kwargs): 

57 """ 

58 Generate a ``__str__`` method for a `.Transform` subclass. 

59 

60 After :: 

61 

62 class T: 

63 __str__ = _make_str_method("attr", key="other") 

64 

65 ``str(T(...))`` will be 

66 

67 .. code-block:: text 

68 

69 {type(T).__name__}( 

70 {self.attr}, 

71 key={self.other}) 

72 """ 

73 indent = functools.partial(textwrap.indent, prefix=" " * 4) 

74 def strrepr(x): return repr(x) if isinstance(x, str) else str(x) 

75 return lambda self: ( 

76 type(self).__name__ + "(" 

77 + ",".join([*(indent("\n" + strrepr(getattr(self, arg))) 

78 for arg in args), 

79 *(indent("\n" + k + "=" + strrepr(getattr(self, arg))) 

80 for k, arg in kwargs.items())]) 

81 + ")") 

82 

83 

84class TransformNode: 

85 """ 

86 The base class for anything that participates in the transform tree 

87 and needs to invalidate its parents or be invalidated. This includes 

88 classes that are not really transforms, such as bounding boxes, since some 

89 transforms depend on bounding boxes to compute their values. 

90 """ 

91 

92 # Invalidation may affect only the affine part. If the 

93 # invalidation was "affine-only", the _invalid member is set to 

94 # INVALID_AFFINE_ONLY 

95 INVALID_NON_AFFINE = _api.deprecated("3.8")(_api.classproperty(lambda cls: 1)) 

96 INVALID_AFFINE = _api.deprecated("3.8")(_api.classproperty(lambda cls: 2)) 

97 INVALID = _api.deprecated("3.8")(_api.classproperty(lambda cls: 3)) 

98 

99 # Possible values for the _invalid attribute. 

100 _VALID, _INVALID_AFFINE_ONLY, _INVALID_FULL = range(3) 

101 

102 # Some metadata about the transform, used to determine whether an 

103 # invalidation is affine-only 

104 is_affine = False 

105 is_bbox = _api.deprecated("3.9")(_api.classproperty(lambda cls: False)) 

106 

107 pass_through = False 

108 """ 

109 If pass_through is True, all ancestors will always be 

110 invalidated, even if 'self' is already invalid. 

111 """ 

112 

113 def __init__(self, shorthand_name=None): 

114 """ 

115 Parameters 

116 ---------- 

117 shorthand_name : str 

118 A string representing the "name" of the transform. The name carries 

119 no significance other than to improve the readability of 

120 ``str(transform)`` when DEBUG=True. 

121 """ 

122 self._parents = {} 

123 # Initially invalid, until first computation. 

124 self._invalid = self._INVALID_FULL 

125 self._shorthand_name = shorthand_name or '' 

126 

127 if DEBUG: 

128 def __str__(self): 

129 # either just return the name of this TransformNode, or its repr 

130 return self._shorthand_name or repr(self) 

131 

132 def __getstate__(self): 

133 # turn the dictionary with weak values into a normal dictionary 

134 return {**self.__dict__, 

135 '_parents': {k: v() for k, v in self._parents.items()}} 

136 

137 def __setstate__(self, data_dict): 

138 self.__dict__ = data_dict 

139 # turn the normal dictionary back into a dictionary with weak values 

140 # The extra lambda is to provide a callback to remove dead 

141 # weakrefs from the dictionary when garbage collection is done. 

142 self._parents = { 

143 k: weakref.ref(v, lambda _, pop=self._parents.pop, k=k: pop(k)) 

144 for k, v in self._parents.items() if v is not None} 

145 

146 def __copy__(self): 

147 other = copy.copy(super()) 

148 # If `c = a + b; a1 = copy(a)`, then modifications to `a1` do not 

149 # propagate back to `c`, i.e. we need to clear the parents of `a1`. 

150 other._parents = {} 

151 # If `c = a + b; c1 = copy(c)`, then modifications to `a` also need to 

152 # be propagated to `c1`. 

153 for key, val in vars(self).items(): 

154 if isinstance(val, TransformNode) and id(self) in val._parents: 

155 other.set_children(val) # val == getattr(other, key) 

156 return other 

157 

158 def invalidate(self): 

159 """ 

160 Invalidate this `TransformNode` and triggers an invalidation of its 

161 ancestors. Should be called any time the transform changes. 

162 """ 

163 return self._invalidate_internal( 

164 level=self._INVALID_AFFINE_ONLY if self.is_affine else self._INVALID_FULL, 

165 invalidating_node=self) 

166 

167 def _invalidate_internal(self, level, invalidating_node): 

168 """ 

169 Called by :meth:`invalidate` and subsequently ascends the transform 

170 stack calling each TransformNode's _invalidate_internal method. 

171 """ 

172 # If we are already more invalid than the currently propagated invalidation, 

173 # then we don't need to do anything. 

174 if level <= self._invalid and not self.pass_through: 

175 return 

176 self._invalid = level 

177 for parent in list(self._parents.values()): 

178 parent = parent() # Dereference the weak reference. 

179 if parent is not None: 

180 parent._invalidate_internal(level=level, invalidating_node=self) 

181 

182 def set_children(self, *children): 

183 """ 

184 Set the children of the transform, to let the invalidation 

185 system know which transforms can invalidate this transform. 

186 Should be called from the constructor of any transforms that 

187 depend on other transforms. 

188 """ 

189 # Parents are stored as weak references, so that if the 

190 # parents are destroyed, references from the children won't 

191 # keep them alive. 

192 id_self = id(self) 

193 for child in children: 

194 # Use weak references so this dictionary won't keep obsolete nodes 

195 # alive; the callback deletes the dictionary entry. This is a 

196 # performance improvement over using WeakValueDictionary. 

197 ref = weakref.ref( 

198 self, lambda _, pop=child._parents.pop, k=id_self: pop(k)) 

199 child._parents[id_self] = ref 

200 

201 def frozen(self): 

202 """ 

203 Return a frozen copy of this transform node. The frozen copy will not 

204 be updated when its children change. Useful for storing a previously 

205 known state of a transform where ``copy.deepcopy()`` might normally be 

206 used. 

207 """ 

208 return self 

209 

210 

211class BboxBase(TransformNode): 

212 """ 

213 The base class of all bounding boxes. 

214 

215 This class is immutable; `Bbox` is a mutable subclass. 

216 

217 The canonical representation is as two points, with no 

218 restrictions on their ordering. Convenience properties are 

219 provided to get the left, bottom, right and top edges and width 

220 and height, but these are not stored explicitly. 

221 """ 

222 

223 is_bbox = _api.deprecated("3.9")(_api.classproperty(lambda cls: True)) 

224 is_affine = True 

225 

226 if DEBUG: 

227 @staticmethod 

228 def _check(points): 

229 if isinstance(points, np.ma.MaskedArray): 

230 _api.warn_external("Bbox bounds are a masked array.") 

231 points = np.asarray(points) 

232 if any((points[1, :] - points[0, :]) == 0): 

233 _api.warn_external("Singular Bbox.") 

234 

235 def frozen(self): 

236 return Bbox(self.get_points().copy()) 

237 frozen.__doc__ = TransformNode.__doc__ 

238 

239 def __array__(self, *args, **kwargs): 

240 return self.get_points() 

241 

242 @property 

243 def x0(self): 

244 """ 

245 The first of the pair of *x* coordinates that define the bounding box. 

246 

247 This is not guaranteed to be less than :attr:`x1` (for that, use 

248 :attr:`xmin`). 

249 """ 

250 return self.get_points()[0, 0] 

251 

252 @property 

253 def y0(self): 

254 """ 

255 The first of the pair of *y* coordinates that define the bounding box. 

256 

257 This is not guaranteed to be less than :attr:`y1` (for that, use 

258 :attr:`ymin`). 

259 """ 

260 return self.get_points()[0, 1] 

261 

262 @property 

263 def x1(self): 

264 """ 

265 The second of the pair of *x* coordinates that define the bounding box. 

266 

267 This is not guaranteed to be greater than :attr:`x0` (for that, use 

268 :attr:`xmax`). 

269 """ 

270 return self.get_points()[1, 0] 

271 

272 @property 

273 def y1(self): 

274 """ 

275 The second of the pair of *y* coordinates that define the bounding box. 

276 

277 This is not guaranteed to be greater than :attr:`y0` (for that, use 

278 :attr:`ymax`). 

279 """ 

280 return self.get_points()[1, 1] 

281 

282 @property 

283 def p0(self): 

284 """ 

285 The first pair of (*x*, *y*) coordinates that define the bounding box. 

286 

287 This is not guaranteed to be the bottom-left corner (for that, use 

288 :attr:`min`). 

289 """ 

290 return self.get_points()[0] 

291 

292 @property 

293 def p1(self): 

294 """ 

295 The second pair of (*x*, *y*) coordinates that define the bounding box. 

296 

297 This is not guaranteed to be the top-right corner (for that, use 

298 :attr:`max`). 

299 """ 

300 return self.get_points()[1] 

301 

302 @property 

303 def xmin(self): 

304 """The left edge of the bounding box.""" 

305 return np.min(self.get_points()[:, 0]) 

306 

307 @property 

308 def ymin(self): 

309 """The bottom edge of the bounding box.""" 

310 return np.min(self.get_points()[:, 1]) 

311 

312 @property 

313 def xmax(self): 

314 """The right edge of the bounding box.""" 

315 return np.max(self.get_points()[:, 0]) 

316 

317 @property 

318 def ymax(self): 

319 """The top edge of the bounding box.""" 

320 return np.max(self.get_points()[:, 1]) 

321 

322 @property 

323 def min(self): 

324 """The bottom-left corner of the bounding box.""" 

325 return np.min(self.get_points(), axis=0) 

326 

327 @property 

328 def max(self): 

329 """The top-right corner of the bounding box.""" 

330 return np.max(self.get_points(), axis=0) 

331 

332 @property 

333 def intervalx(self): 

334 """ 

335 The pair of *x* coordinates that define the bounding box. 

336 

337 This is not guaranteed to be sorted from left to right. 

338 """ 

339 return self.get_points()[:, 0] 

340 

341 @property 

342 def intervaly(self): 

343 """ 

344 The pair of *y* coordinates that define the bounding box. 

345 

346 This is not guaranteed to be sorted from bottom to top. 

347 """ 

348 return self.get_points()[:, 1] 

349 

350 @property 

351 def width(self): 

352 """The (signed) width of the bounding box.""" 

353 points = self.get_points() 

354 return points[1, 0] - points[0, 0] 

355 

356 @property 

357 def height(self): 

358 """The (signed) height of the bounding box.""" 

359 points = self.get_points() 

360 return points[1, 1] - points[0, 1] 

361 

362 @property 

363 def size(self): 

364 """The (signed) width and height of the bounding box.""" 

365 points = self.get_points() 

366 return points[1] - points[0] 

367 

368 @property 

369 def bounds(self): 

370 """Return (:attr:`x0`, :attr:`y0`, :attr:`width`, :attr:`height`).""" 

371 (x0, y0), (x1, y1) = self.get_points() 

372 return (x0, y0, x1 - x0, y1 - y0) 

373 

374 @property 

375 def extents(self): 

376 """Return (:attr:`x0`, :attr:`y0`, :attr:`x1`, :attr:`y1`).""" 

377 return self.get_points().flatten() # flatten returns a copy. 

378 

379 def get_points(self): 

380 raise NotImplementedError 

381 

382 def containsx(self, x): 

383 """ 

384 Return whether *x* is in the closed (:attr:`x0`, :attr:`x1`) interval. 

385 """ 

386 x0, x1 = self.intervalx 

387 return x0 <= x <= x1 or x0 >= x >= x1 

388 

389 def containsy(self, y): 

390 """ 

391 Return whether *y* is in the closed (:attr:`y0`, :attr:`y1`) interval. 

392 """ 

393 y0, y1 = self.intervaly 

394 return y0 <= y <= y1 or y0 >= y >= y1 

395 

396 def contains(self, x, y): 

397 """ 

398 Return whether ``(x, y)`` is in the bounding box or on its edge. 

399 """ 

400 return self.containsx(x) and self.containsy(y) 

401 

402 def overlaps(self, other): 

403 """ 

404 Return whether this bounding box overlaps with the other bounding box. 

405 

406 Parameters 

407 ---------- 

408 other : `.BboxBase` 

409 """ 

410 ax1, ay1, ax2, ay2 = self.extents 

411 bx1, by1, bx2, by2 = other.extents 

412 if ax2 < ax1: 

413 ax2, ax1 = ax1, ax2 

414 if ay2 < ay1: 

415 ay2, ay1 = ay1, ay2 

416 if bx2 < bx1: 

417 bx2, bx1 = bx1, bx2 

418 if by2 < by1: 

419 by2, by1 = by1, by2 

420 return ax1 <= bx2 and bx1 <= ax2 and ay1 <= by2 and by1 <= ay2 

421 

422 def fully_containsx(self, x): 

423 """ 

424 Return whether *x* is in the open (:attr:`x0`, :attr:`x1`) interval. 

425 """ 

426 x0, x1 = self.intervalx 

427 return x0 < x < x1 or x0 > x > x1 

428 

429 def fully_containsy(self, y): 

430 """ 

431 Return whether *y* is in the open (:attr:`y0`, :attr:`y1`) interval. 

432 """ 

433 y0, y1 = self.intervaly 

434 return y0 < y < y1 or y0 > y > y1 

435 

436 def fully_contains(self, x, y): 

437 """ 

438 Return whether ``x, y`` is in the bounding box, but not on its edge. 

439 """ 

440 return self.fully_containsx(x) and self.fully_containsy(y) 

441 

442 def fully_overlaps(self, other): 

443 """ 

444 Return whether this bounding box overlaps with the other bounding box, 

445 not including the edges. 

446 

447 Parameters 

448 ---------- 

449 other : `.BboxBase` 

450 """ 

451 ax1, ay1, ax2, ay2 = self.extents 

452 bx1, by1, bx2, by2 = other.extents 

453 if ax2 < ax1: 

454 ax2, ax1 = ax1, ax2 

455 if ay2 < ay1: 

456 ay2, ay1 = ay1, ay2 

457 if bx2 < bx1: 

458 bx2, bx1 = bx1, bx2 

459 if by2 < by1: 

460 by2, by1 = by1, by2 

461 return ax1 < bx2 and bx1 < ax2 and ay1 < by2 and by1 < ay2 

462 

463 def transformed(self, transform): 

464 """ 

465 Construct a `Bbox` by statically transforming this one by *transform*. 

466 """ 

467 pts = self.get_points() 

468 ll, ul, lr = transform.transform(np.array( 

469 [pts[0], [pts[0, 0], pts[1, 1]], [pts[1, 0], pts[0, 1]]])) 

470 return Bbox([ll, [lr[0], ul[1]]]) 

471 

472 coefs = {'C': (0.5, 0.5), 

473 'SW': (0, 0), 

474 'S': (0.5, 0), 

475 'SE': (1.0, 0), 

476 'E': (1.0, 0.5), 

477 'NE': (1.0, 1.0), 

478 'N': (0.5, 1.0), 

479 'NW': (0, 1.0), 

480 'W': (0, 0.5)} 

481 

482 def anchored(self, c, container=None): 

483 """ 

484 Return a copy of the `Bbox` anchored to *c* within *container*. 

485 

486 Parameters 

487 ---------- 

488 c : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', ...} 

489 Either an (*x*, *y*) pair of relative coordinates (0 is left or 

490 bottom, 1 is right or top), 'C' (center), or a cardinal direction 

491 ('SW', southwest, is bottom left, etc.). 

492 container : `Bbox`, optional 

493 The box within which the `Bbox` is positioned. 

494 

495 See Also 

496 -------- 

497 .Axes.set_anchor 

498 """ 

499 if container is None: 

500 _api.warn_deprecated( 

501 "3.8", message="Calling anchored() with no container bbox " 

502 "returns a frozen copy of the original bbox and is deprecated " 

503 "since %(since)s.") 

504 container = self 

505 l, b, w, h = container.bounds 

506 L, B, W, H = self.bounds 

507 cx, cy = self.coefs[c] if isinstance(c, str) else c 

508 return Bbox(self._points + 

509 [(l + cx * (w - W)) - L, 

510 (b + cy * (h - H)) - B]) 

511 

512 def shrunk(self, mx, my): 

513 """ 

514 Return a copy of the `Bbox`, shrunk by the factor *mx* 

515 in the *x* direction and the factor *my* in the *y* direction. 

516 The lower left corner of the box remains unchanged. Normally 

517 *mx* and *my* will be less than 1, but this is not enforced. 

518 """ 

519 w, h = self.size 

520 return Bbox([self._points[0], 

521 self._points[0] + [mx * w, my * h]]) 

522 

523 def shrunk_to_aspect(self, box_aspect, container=None, fig_aspect=1.0): 

524 """ 

525 Return a copy of the `Bbox`, shrunk so that it is as 

526 large as it can be while having the desired aspect ratio, 

527 *box_aspect*. If the box coordinates are relative (i.e. 

528 fractions of a larger box such as a figure) then the 

529 physical aspect ratio of that figure is specified with 

530 *fig_aspect*, so that *box_aspect* can also be given as a 

531 ratio of the absolute dimensions, not the relative dimensions. 

532 """ 

533 if box_aspect <= 0 or fig_aspect <= 0: 

534 raise ValueError("'box_aspect' and 'fig_aspect' must be positive") 

535 if container is None: 

536 container = self 

537 w, h = container.size 

538 H = w * box_aspect / fig_aspect 

539 if H <= h: 

540 W = w 

541 else: 

542 W = h * fig_aspect / box_aspect 

543 H = h 

544 return Bbox([self._points[0], 

545 self._points[0] + (W, H)]) 

546 

547 def splitx(self, *args): 

548 """ 

549 Return a list of new `Bbox` objects formed by splitting the original 

550 one with vertical lines at fractional positions given by *args*. 

551 """ 

552 xf = [0, *args, 1] 

553 x0, y0, x1, y1 = self.extents 

554 w = x1 - x0 

555 return [Bbox([[x0 + xf0 * w, y0], [x0 + xf1 * w, y1]]) 

556 for xf0, xf1 in zip(xf[:-1], xf[1:])] 

557 

558 def splity(self, *args): 

559 """ 

560 Return a list of new `Bbox` objects formed by splitting the original 

561 one with horizontal lines at fractional positions given by *args*. 

562 """ 

563 yf = [0, *args, 1] 

564 x0, y0, x1, y1 = self.extents 

565 h = y1 - y0 

566 return [Bbox([[x0, y0 + yf0 * h], [x1, y0 + yf1 * h]]) 

567 for yf0, yf1 in zip(yf[:-1], yf[1:])] 

568 

569 def count_contains(self, vertices): 

570 """ 

571 Count the number of vertices contained in the `Bbox`. 

572 Any vertices with a non-finite x or y value are ignored. 

573 

574 Parameters 

575 ---------- 

576 vertices : (N, 2) array 

577 """ 

578 if len(vertices) == 0: 

579 return 0 

580 vertices = np.asarray(vertices) 

581 with np.errstate(invalid='ignore'): 

582 return (((self.min < vertices) & 

583 (vertices < self.max)).all(axis=1).sum()) 

584 

585 def count_overlaps(self, bboxes): 

586 """ 

587 Count the number of bounding boxes that overlap this one. 

588 

589 Parameters 

590 ---------- 

591 bboxes : sequence of `.BboxBase` 

592 """ 

593 return count_bboxes_overlapping_bbox( 

594 self, np.atleast_3d([np.array(x) for x in bboxes])) 

595 

596 def expanded(self, sw, sh): 

597 """ 

598 Construct a `Bbox` by expanding this one around its center by the 

599 factors *sw* and *sh*. 

600 """ 

601 width = self.width 

602 height = self.height 

603 deltaw = (sw * width - width) / 2.0 

604 deltah = (sh * height - height) / 2.0 

605 a = np.array([[-deltaw, -deltah], [deltaw, deltah]]) 

606 return Bbox(self._points + a) 

607 

608 @_api.rename_parameter("3.8", "p", "w_pad") 

609 def padded(self, w_pad, h_pad=None): 

610 """ 

611 Construct a `Bbox` by padding this one on all four sides. 

612 

613 Parameters 

614 ---------- 

615 w_pad : float 

616 Width pad 

617 h_pad : float, optional 

618 Height pad. Defaults to *w_pad*. 

619 

620 """ 

621 points = self.get_points() 

622 if h_pad is None: 

623 h_pad = w_pad 

624 return Bbox(points + [[-w_pad, -h_pad], [w_pad, h_pad]]) 

625 

626 def translated(self, tx, ty): 

627 """Construct a `Bbox` by translating this one by *tx* and *ty*.""" 

628 return Bbox(self._points + (tx, ty)) 

629 

630 def corners(self): 

631 """ 

632 Return the corners of this rectangle as an array of points. 

633 

634 Specifically, this returns the array 

635 ``[[x0, y0], [x0, y1], [x1, y0], [x1, y1]]``. 

636 """ 

637 (x0, y0), (x1, y1) = self.get_points() 

638 return np.array([[x0, y0], [x0, y1], [x1, y0], [x1, y1]]) 

639 

640 def rotated(self, radians): 

641 """ 

642 Return the axes-aligned bounding box that bounds the result of rotating 

643 this `Bbox` by an angle of *radians*. 

644 """ 

645 corners = self.corners() 

646 corners_rotated = Affine2D().rotate(radians).transform(corners) 

647 bbox = Bbox.unit() 

648 bbox.update_from_data_xy(corners_rotated, ignore=True) 

649 return bbox 

650 

651 @staticmethod 

652 def union(bboxes): 

653 """Return a `Bbox` that contains all of the given *bboxes*.""" 

654 if not len(bboxes): 

655 raise ValueError("'bboxes' cannot be empty") 

656 x0 = np.min([bbox.xmin for bbox in bboxes]) 

657 x1 = np.max([bbox.xmax for bbox in bboxes]) 

658 y0 = np.min([bbox.ymin for bbox in bboxes]) 

659 y1 = np.max([bbox.ymax for bbox in bboxes]) 

660 return Bbox([[x0, y0], [x1, y1]]) 

661 

662 @staticmethod 

663 def intersection(bbox1, bbox2): 

664 """ 

665 Return the intersection of *bbox1* and *bbox2* if they intersect, or 

666 None if they don't. 

667 """ 

668 x0 = np.maximum(bbox1.xmin, bbox2.xmin) 

669 x1 = np.minimum(bbox1.xmax, bbox2.xmax) 

670 y0 = np.maximum(bbox1.ymin, bbox2.ymin) 

671 y1 = np.minimum(bbox1.ymax, bbox2.ymax) 

672 return Bbox([[x0, y0], [x1, y1]]) if x0 <= x1 and y0 <= y1 else None 

673 

674 

675_default_minpos = np.array([np.inf, np.inf]) 

676 

677 

678class Bbox(BboxBase): 

679 """ 

680 A mutable bounding box. 

681 

682 Examples 

683 -------- 

684 **Create from known bounds** 

685 

686 The default constructor takes the boundary "points" ``[[xmin, ymin], 

687 [xmax, ymax]]``. 

688 

689 >>> Bbox([[1, 1], [3, 7]]) 

690 Bbox([[1.0, 1.0], [3.0, 7.0]]) 

691 

692 Alternatively, a Bbox can be created from the flattened points array, the 

693 so-called "extents" ``(xmin, ymin, xmax, ymax)`` 

694 

695 >>> Bbox.from_extents(1, 1, 3, 7) 

696 Bbox([[1.0, 1.0], [3.0, 7.0]]) 

697 

698 or from the "bounds" ``(xmin, ymin, width, height)``. 

699 

700 >>> Bbox.from_bounds(1, 1, 2, 6) 

701 Bbox([[1.0, 1.0], [3.0, 7.0]]) 

702 

703 **Create from collections of points** 

704 

705 The "empty" object for accumulating Bboxs is the null bbox, which is a 

706 stand-in for the empty set. 

707 

708 >>> Bbox.null() 

709 Bbox([[inf, inf], [-inf, -inf]]) 

710 

711 Adding points to the null bbox will give you the bbox of those points. 

712 

713 >>> box = Bbox.null() 

714 >>> box.update_from_data_xy([[1, 1]]) 

715 >>> box 

716 Bbox([[1.0, 1.0], [1.0, 1.0]]) 

717 >>> box.update_from_data_xy([[2, 3], [3, 2]], ignore=False) 

718 >>> box 

719 Bbox([[1.0, 1.0], [3.0, 3.0]]) 

720 

721 Setting ``ignore=True`` is equivalent to starting over from a null bbox. 

722 

723 >>> box.update_from_data_xy([[1, 1]], ignore=True) 

724 >>> box 

725 Bbox([[1.0, 1.0], [1.0, 1.0]]) 

726 

727 .. warning:: 

728 

729 It is recommended to always specify ``ignore`` explicitly. If not, the 

730 default value of ``ignore`` can be changed at any time by code with 

731 access to your Bbox, for example using the method `~.Bbox.ignore`. 

732 

733 **Properties of the ``null`` bbox** 

734 

735 .. note:: 

736 

737 The current behavior of `Bbox.null()` may be surprising as it does 

738 not have all of the properties of the "empty set", and as such does 

739 not behave like a "zero" object in the mathematical sense. We may 

740 change that in the future (with a deprecation period). 

741 

742 The null bbox is the identity for intersections 

743 

744 >>> Bbox.intersection(Bbox([[1, 1], [3, 7]]), Bbox.null()) 

745 Bbox([[1.0, 1.0], [3.0, 7.0]]) 

746 

747 except with itself, where it returns the full space. 

748 

749 >>> Bbox.intersection(Bbox.null(), Bbox.null()) 

750 Bbox([[-inf, -inf], [inf, inf]]) 

751 

752 A union containing null will always return the full space (not the other 

753 set!) 

754 

755 >>> Bbox.union([Bbox([[0, 0], [0, 0]]), Bbox.null()]) 

756 Bbox([[-inf, -inf], [inf, inf]]) 

757 """ 

758 

759 def __init__(self, points, **kwargs): 

760 """ 

761 Parameters 

762 ---------- 

763 points : `~numpy.ndarray` 

764 A (2, 2) array of the form ``[[x0, y0], [x1, y1]]``. 

765 """ 

766 super().__init__(**kwargs) 

767 points = np.asarray(points, float) 

768 if points.shape != (2, 2): 

769 raise ValueError('Bbox points must be of the form ' 

770 '"[[x0, y0], [x1, y1]]".') 

771 self._points = points 

772 self._minpos = _default_minpos.copy() 

773 self._ignore = True 

774 # it is helpful in some contexts to know if the bbox is a 

775 # default or has been mutated; we store the orig points to 

776 # support the mutated methods 

777 self._points_orig = self._points.copy() 

778 if DEBUG: 

779 ___init__ = __init__ 

780 

781 def __init__(self, points, **kwargs): 

782 self._check(points) 

783 self.___init__(points, **kwargs) 

784 

785 def invalidate(self): 

786 self._check(self._points) 

787 super().invalidate() 

788 

789 def frozen(self): 

790 # docstring inherited 

791 frozen_bbox = super().frozen() 

792 frozen_bbox._minpos = self.minpos.copy() 

793 return frozen_bbox 

794 

795 @staticmethod 

796 def unit(): 

797 """Create a new unit `Bbox` from (0, 0) to (1, 1).""" 

798 return Bbox([[0, 0], [1, 1]]) 

799 

800 @staticmethod 

801 def null(): 

802 """Create a new null `Bbox` from (inf, inf) to (-inf, -inf).""" 

803 return Bbox([[np.inf, np.inf], [-np.inf, -np.inf]]) 

804 

805 @staticmethod 

806 def from_bounds(x0, y0, width, height): 

807 """ 

808 Create a new `Bbox` from *x0*, *y0*, *width* and *height*. 

809 

810 *width* and *height* may be negative. 

811 """ 

812 return Bbox.from_extents(x0, y0, x0 + width, y0 + height) 

813 

814 @staticmethod 

815 def from_extents(*args, minpos=None): 

816 """ 

817 Create a new Bbox from *left*, *bottom*, *right* and *top*. 

818 

819 The *y*-axis increases upwards. 

820 

821 Parameters 

822 ---------- 

823 left, bottom, right, top : float 

824 The four extents of the bounding box. 

825 minpos : float or None 

826 If this is supplied, the Bbox will have a minimum positive value 

827 set. This is useful when dealing with logarithmic scales and other 

828 scales where negative bounds result in floating point errors. 

829 """ 

830 bbox = Bbox(np.reshape(args, (2, 2))) 

831 if minpos is not None: 

832 bbox._minpos[:] = minpos 

833 return bbox 

834 

835 def __format__(self, fmt): 

836 return ( 

837 'Bbox(x0={0.x0:{1}}, y0={0.y0:{1}}, x1={0.x1:{1}}, y1={0.y1:{1}})'. 

838 format(self, fmt)) 

839 

840 def __str__(self): 

841 return format(self, '') 

842 

843 def __repr__(self): 

844 return 'Bbox([[{0.x0}, {0.y0}], [{0.x1}, {0.y1}]])'.format(self) 

845 

846 def ignore(self, value): 

847 """ 

848 Set whether the existing bounds of the box should be ignored 

849 by subsequent calls to :meth:`update_from_data_xy`. 

850 

851 value : bool 

852 - When ``True``, subsequent calls to `update_from_data_xy` will 

853 ignore the existing bounds of the `Bbox`. 

854 - When ``False``, subsequent calls to `update_from_data_xy` will 

855 include the existing bounds of the `Bbox`. 

856 """ 

857 self._ignore = value 

858 

859 def update_from_path(self, path, ignore=None, updatex=True, updatey=True): 

860 """ 

861 Update the bounds of the `Bbox` to contain the vertices of the 

862 provided path. After updating, the bounds will have positive *width* 

863 and *height*; *x0* and *y0* will be the minimal values. 

864 

865 Parameters 

866 ---------- 

867 path : `~matplotlib.path.Path` 

868 ignore : bool, optional 

869 - When ``True``, ignore the existing bounds of the `Bbox`. 

870 - When ``False``, include the existing bounds of the `Bbox`. 

871 - When ``None``, use the last value passed to :meth:`ignore`. 

872 updatex, updatey : bool, default: True 

873 When ``True``, update the x/y values. 

874 """ 

875 if ignore is None: 

876 ignore = self._ignore 

877 

878 if path.vertices.size == 0: 

879 return 

880 

881 points, minpos, changed = update_path_extents( 

882 path, None, self._points, self._minpos, ignore) 

883 

884 if changed: 

885 self.invalidate() 

886 if updatex: 

887 self._points[:, 0] = points[:, 0] 

888 self._minpos[0] = minpos[0] 

889 if updatey: 

890 self._points[:, 1] = points[:, 1] 

891 self._minpos[1] = minpos[1] 

892 

893 def update_from_data_x(self, x, ignore=None): 

894 """ 

895 Update the x-bounds of the `Bbox` based on the passed in data. After 

896 updating, the bounds will have positive *width*, and *x0* will be the 

897 minimal value. 

898 

899 Parameters 

900 ---------- 

901 x : `~numpy.ndarray` 

902 Array of x-values. 

903 ignore : bool, optional 

904 - When ``True``, ignore the existing bounds of the `Bbox`. 

905 - When ``False``, include the existing bounds of the `Bbox`. 

906 - When ``None``, use the last value passed to :meth:`ignore`. 

907 """ 

908 x = np.ravel(x) 

909 self.update_from_data_xy(np.column_stack([x, np.ones(x.size)]), 

910 ignore=ignore, updatey=False) 

911 

912 def update_from_data_y(self, y, ignore=None): 

913 """ 

914 Update the y-bounds of the `Bbox` based on the passed in data. After 

915 updating, the bounds will have positive *height*, and *y0* will be the 

916 minimal value. 

917 

918 Parameters 

919 ---------- 

920 y : `~numpy.ndarray` 

921 Array of y-values. 

922 ignore : bool, optional 

923 - When ``True``, ignore the existing bounds of the `Bbox`. 

924 - When ``False``, include the existing bounds of the `Bbox`. 

925 - When ``None``, use the last value passed to :meth:`ignore`. 

926 """ 

927 y = np.ravel(y) 

928 self.update_from_data_xy(np.column_stack([np.ones(y.size), y]), 

929 ignore=ignore, updatex=False) 

930 

931 def update_from_data_xy(self, xy, ignore=None, updatex=True, updatey=True): 

932 """ 

933 Update the `Bbox` bounds based on the passed in *xy* coordinates. 

934 

935 After updating, the bounds will have positive *width* and *height*; 

936 *x0* and *y0* will be the minimal values. 

937 

938 Parameters 

939 ---------- 

940 xy : (N, 2) array-like 

941 The (x, y) coordinates. 

942 ignore : bool, optional 

943 - When ``True``, ignore the existing bounds of the `Bbox`. 

944 - When ``False``, include the existing bounds of the `Bbox`. 

945 - When ``None``, use the last value passed to :meth:`ignore`. 

946 updatex, updatey : bool, default: True 

947 When ``True``, update the x/y values. 

948 """ 

949 if len(xy) == 0: 

950 return 

951 

952 path = Path(xy) 

953 self.update_from_path(path, ignore=ignore, 

954 updatex=updatex, updatey=updatey) 

955 

956 @BboxBase.x0.setter 

957 def x0(self, val): 

958 self._points[0, 0] = val 

959 self.invalidate() 

960 

961 @BboxBase.y0.setter 

962 def y0(self, val): 

963 self._points[0, 1] = val 

964 self.invalidate() 

965 

966 @BboxBase.x1.setter 

967 def x1(self, val): 

968 self._points[1, 0] = val 

969 self.invalidate() 

970 

971 @BboxBase.y1.setter 

972 def y1(self, val): 

973 self._points[1, 1] = val 

974 self.invalidate() 

975 

976 @BboxBase.p0.setter 

977 def p0(self, val): 

978 self._points[0] = val 

979 self.invalidate() 

980 

981 @BboxBase.p1.setter 

982 def p1(self, val): 

983 self._points[1] = val 

984 self.invalidate() 

985 

986 @BboxBase.intervalx.setter 

987 def intervalx(self, interval): 

988 self._points[:, 0] = interval 

989 self.invalidate() 

990 

991 @BboxBase.intervaly.setter 

992 def intervaly(self, interval): 

993 self._points[:, 1] = interval 

994 self.invalidate() 

995 

996 @BboxBase.bounds.setter 

997 def bounds(self, bounds): 

998 l, b, w, h = bounds 

999 points = np.array([[l, b], [l + w, b + h]], float) 

1000 if np.any(self._points != points): 

1001 self._points = points 

1002 self.invalidate() 

1003 

1004 @property 

1005 def minpos(self): 

1006 """ 

1007 The minimum positive value in both directions within the Bbox. 

1008 

1009 This is useful when dealing with logarithmic scales and other scales 

1010 where negative bounds result in floating point errors, and will be used 

1011 as the minimum extent instead of *p0*. 

1012 """ 

1013 return self._minpos 

1014 

1015 @minpos.setter 

1016 def minpos(self, val): 

1017 self._minpos[:] = val 

1018 

1019 @property 

1020 def minposx(self): 

1021 """ 

1022 The minimum positive value in the *x*-direction within the Bbox. 

1023 

1024 This is useful when dealing with logarithmic scales and other scales 

1025 where negative bounds result in floating point errors, and will be used 

1026 as the minimum *x*-extent instead of *x0*. 

1027 """ 

1028 return self._minpos[0] 

1029 

1030 @minposx.setter 

1031 def minposx(self, val): 

1032 self._minpos[0] = val 

1033 

1034 @property 

1035 def minposy(self): 

1036 """ 

1037 The minimum positive value in the *y*-direction within the Bbox. 

1038 

1039 This is useful when dealing with logarithmic scales and other scales 

1040 where negative bounds result in floating point errors, and will be used 

1041 as the minimum *y*-extent instead of *y0*. 

1042 """ 

1043 return self._minpos[1] 

1044 

1045 @minposy.setter 

1046 def minposy(self, val): 

1047 self._minpos[1] = val 

1048 

1049 def get_points(self): 

1050 """ 

1051 Get the points of the bounding box as an array of the form 

1052 ``[[x0, y0], [x1, y1]]``. 

1053 """ 

1054 self._invalid = 0 

1055 return self._points 

1056 

1057 def set_points(self, points): 

1058 """ 

1059 Set the points of the bounding box directly from an array of the form 

1060 ``[[x0, y0], [x1, y1]]``. No error checking is performed, as this 

1061 method is mainly for internal use. 

1062 """ 

1063 if np.any(self._points != points): 

1064 self._points = points 

1065 self.invalidate() 

1066 

1067 def set(self, other): 

1068 """ 

1069 Set this bounding box from the "frozen" bounds of another `Bbox`. 

1070 """ 

1071 if np.any(self._points != other.get_points()): 

1072 self._points = other.get_points() 

1073 self.invalidate() 

1074 

1075 def mutated(self): 

1076 """Return whether the bbox has changed since init.""" 

1077 return self.mutatedx() or self.mutatedy() 

1078 

1079 def mutatedx(self): 

1080 """Return whether the x-limits have changed since init.""" 

1081 return (self._points[0, 0] != self._points_orig[0, 0] or 

1082 self._points[1, 0] != self._points_orig[1, 0]) 

1083 

1084 def mutatedy(self): 

1085 """Return whether the y-limits have changed since init.""" 

1086 return (self._points[0, 1] != self._points_orig[0, 1] or 

1087 self._points[1, 1] != self._points_orig[1, 1]) 

1088 

1089 

1090class TransformedBbox(BboxBase): 

1091 """ 

1092 A `Bbox` that is automatically transformed by a given 

1093 transform. When either the child bounding box or transform 

1094 changes, the bounds of this bbox will update accordingly. 

1095 """ 

1096 

1097 def __init__(self, bbox, transform, **kwargs): 

1098 """ 

1099 Parameters 

1100 ---------- 

1101 bbox : `Bbox` 

1102 transform : `Transform` 

1103 """ 

1104 _api.check_isinstance(BboxBase, bbox=bbox) 

1105 _api.check_isinstance(Transform, transform=transform) 

1106 if transform.input_dims != 2 or transform.output_dims != 2: 

1107 raise ValueError( 

1108 "The input and output dimensions of 'transform' must be 2") 

1109 

1110 super().__init__(**kwargs) 

1111 self._bbox = bbox 

1112 self._transform = transform 

1113 self.set_children(bbox, transform) 

1114 self._points = None 

1115 

1116 __str__ = _make_str_method("_bbox", "_transform") 

1117 

1118 def get_points(self): 

1119 # docstring inherited 

1120 if self._invalid: 

1121 p = self._bbox.get_points() 

1122 # Transform all four points, then make a new bounding box 

1123 # from the result, taking care to make the orientation the 

1124 # same. 

1125 points = self._transform.transform( 

1126 [[p[0, 0], p[0, 1]], 

1127 [p[1, 0], p[0, 1]], 

1128 [p[0, 0], p[1, 1]], 

1129 [p[1, 0], p[1, 1]]]) 

1130 points = np.ma.filled(points, 0.0) 

1131 

1132 xs = min(points[:, 0]), max(points[:, 0]) 

1133 if p[0, 0] > p[1, 0]: 

1134 xs = xs[::-1] 

1135 

1136 ys = min(points[:, 1]), max(points[:, 1]) 

1137 if p[0, 1] > p[1, 1]: 

1138 ys = ys[::-1] 

1139 

1140 self._points = np.array([ 

1141 [xs[0], ys[0]], 

1142 [xs[1], ys[1]] 

1143 ]) 

1144 

1145 self._invalid = 0 

1146 return self._points 

1147 

1148 if DEBUG: 

1149 _get_points = get_points 

1150 

1151 def get_points(self): 

1152 points = self._get_points() 

1153 self._check(points) 

1154 return points 

1155 

1156 def contains(self, x, y): 

1157 # Docstring inherited. 

1158 return self._bbox.contains(*self._transform.inverted().transform((x, y))) 

1159 

1160 def fully_contains(self, x, y): 

1161 # Docstring inherited. 

1162 return self._bbox.fully_contains(*self._transform.inverted().transform((x, y))) 

1163 

1164 

1165class LockableBbox(BboxBase): 

1166 """ 

1167 A `Bbox` where some elements may be locked at certain values. 

1168 

1169 When the child bounding box changes, the bounds of this bbox will update 

1170 accordingly with the exception of the locked elements. 

1171 """ 

1172 def __init__(self, bbox, x0=None, y0=None, x1=None, y1=None, **kwargs): 

1173 """ 

1174 Parameters 

1175 ---------- 

1176 bbox : `Bbox` 

1177 The child bounding box to wrap. 

1178 

1179 x0 : float or None 

1180 The locked value for x0, or None to leave unlocked. 

1181 

1182 y0 : float or None 

1183 The locked value for y0, or None to leave unlocked. 

1184 

1185 x1 : float or None 

1186 The locked value for x1, or None to leave unlocked. 

1187 

1188 y1 : float or None 

1189 The locked value for y1, or None to leave unlocked. 

1190 

1191 """ 

1192 _api.check_isinstance(BboxBase, bbox=bbox) 

1193 super().__init__(**kwargs) 

1194 self._bbox = bbox 

1195 self.set_children(bbox) 

1196 self._points = None 

1197 fp = [x0, y0, x1, y1] 

1198 mask = [val is None for val in fp] 

1199 self._locked_points = np.ma.array(fp, float, mask=mask).reshape((2, 2)) 

1200 

1201 __str__ = _make_str_method("_bbox", "_locked_points") 

1202 

1203 def get_points(self): 

1204 # docstring inherited 

1205 if self._invalid: 

1206 points = self._bbox.get_points() 

1207 self._points = np.where(self._locked_points.mask, 

1208 points, 

1209 self._locked_points) 

1210 self._invalid = 0 

1211 return self._points 

1212 

1213 if DEBUG: 

1214 _get_points = get_points 

1215 

1216 def get_points(self): 

1217 points = self._get_points() 

1218 self._check(points) 

1219 return points 

1220 

1221 @property 

1222 def locked_x0(self): 

1223 """ 

1224 float or None: The value used for the locked x0. 

1225 """ 

1226 if self._locked_points.mask[0, 0]: 

1227 return None 

1228 else: 

1229 return self._locked_points[0, 0] 

1230 

1231 @locked_x0.setter 

1232 def locked_x0(self, x0): 

1233 self._locked_points.mask[0, 0] = x0 is None 

1234 self._locked_points.data[0, 0] = x0 

1235 self.invalidate() 

1236 

1237 @property 

1238 def locked_y0(self): 

1239 """ 

1240 float or None: The value used for the locked y0. 

1241 """ 

1242 if self._locked_points.mask[0, 1]: 

1243 return None 

1244 else: 

1245 return self._locked_points[0, 1] 

1246 

1247 @locked_y0.setter 

1248 def locked_y0(self, y0): 

1249 self._locked_points.mask[0, 1] = y0 is None 

1250 self._locked_points.data[0, 1] = y0 

1251 self.invalidate() 

1252 

1253 @property 

1254 def locked_x1(self): 

1255 """ 

1256 float or None: The value used for the locked x1. 

1257 """ 

1258 if self._locked_points.mask[1, 0]: 

1259 return None 

1260 else: 

1261 return self._locked_points[1, 0] 

1262 

1263 @locked_x1.setter 

1264 def locked_x1(self, x1): 

1265 self._locked_points.mask[1, 0] = x1 is None 

1266 self._locked_points.data[1, 0] = x1 

1267 self.invalidate() 

1268 

1269 @property 

1270 def locked_y1(self): 

1271 """ 

1272 float or None: The value used for the locked y1. 

1273 """ 

1274 if self._locked_points.mask[1, 1]: 

1275 return None 

1276 else: 

1277 return self._locked_points[1, 1] 

1278 

1279 @locked_y1.setter 

1280 def locked_y1(self, y1): 

1281 self._locked_points.mask[1, 1] = y1 is None 

1282 self._locked_points.data[1, 1] = y1 

1283 self.invalidate() 

1284 

1285 

1286class Transform(TransformNode): 

1287 """ 

1288 The base class of all `TransformNode` instances that 

1289 actually perform a transformation. 

1290 

1291 All non-affine transformations should be subclasses of this class. 

1292 New affine transformations should be subclasses of `Affine2D`. 

1293 

1294 Subclasses of this class should override the following members (at 

1295 minimum): 

1296 

1297 - :attr:`input_dims` 

1298 - :attr:`output_dims` 

1299 - :meth:`transform` 

1300 - :meth:`inverted` (if an inverse exists) 

1301 

1302 The following attributes may be overridden if the default is unsuitable: 

1303 

1304 - :attr:`is_separable` (defaults to True for 1D -> 1D transforms, False 

1305 otherwise) 

1306 - :attr:`has_inverse` (defaults to True if :meth:`inverted` is overridden, 

1307 False otherwise) 

1308 

1309 If the transform needs to do something non-standard with 

1310 `matplotlib.path.Path` objects, such as adding curves 

1311 where there were once line segments, it should override: 

1312 

1313 - :meth:`transform_path` 

1314 """ 

1315 

1316 input_dims = None 

1317 """ 

1318 The number of input dimensions of this transform. 

1319 Must be overridden (with integers) in the subclass. 

1320 """ 

1321 

1322 output_dims = None 

1323 """ 

1324 The number of output dimensions of this transform. 

1325 Must be overridden (with integers) in the subclass. 

1326 """ 

1327 

1328 is_separable = False 

1329 """True if this transform is separable in the x- and y- dimensions.""" 

1330 

1331 has_inverse = False 

1332 """True if this transform has a corresponding inverse transform.""" 

1333 

1334 def __init_subclass__(cls): 

1335 # 1d transforms are always separable; we assume higher-dimensional ones 

1336 # are not but subclasses can also directly set is_separable -- this is 

1337 # verified by checking whether "is_separable" appears more than once in 

1338 # the class's MRO (it appears once in Transform). 

1339 if (sum("is_separable" in vars(parent) for parent in cls.__mro__) == 1 

1340 and cls.input_dims == cls.output_dims == 1): 

1341 cls.is_separable = True 

1342 # Transform.inverted raises NotImplementedError; we assume that if this 

1343 # is overridden then the transform is invertible but subclass can also 

1344 # directly set has_inverse. 

1345 if (sum("has_inverse" in vars(parent) for parent in cls.__mro__) == 1 

1346 and hasattr(cls, "inverted") 

1347 and cls.inverted is not Transform.inverted): 

1348 cls.has_inverse = True 

1349 

1350 def __add__(self, other): 

1351 """ 

1352 Compose two transforms together so that *self* is followed by *other*. 

1353 

1354 ``A + B`` returns a transform ``C`` so that 

1355 ``C.transform(x) == B.transform(A.transform(x))``. 

1356 """ 

1357 return (composite_transform_factory(self, other) 

1358 if isinstance(other, Transform) else 

1359 NotImplemented) 

1360 

1361 # Equality is based on object identity for `Transform`s (so we don't 

1362 # override `__eq__`), but some subclasses, such as TransformWrapper & 

1363 # AffineBase, override this behavior. 

1364 

1365 def _iter_break_from_left_to_right(self): 

1366 """ 

1367 Return an iterator breaking down this transform stack from left to 

1368 right recursively. If self == ((A, N), A) then the result will be an 

1369 iterator which yields I : ((A, N), A), followed by A : (N, A), 

1370 followed by (A, N) : (A), but not ((A, N), A) : I. 

1371 

1372 This is equivalent to flattening the stack then yielding 

1373 ``flat_stack[:i], flat_stack[i:]`` where i=0..(n-1). 

1374 """ 

1375 yield IdentityTransform(), self 

1376 

1377 @property 

1378 def depth(self): 

1379 """ 

1380 Return the number of transforms which have been chained 

1381 together to form this Transform instance. 

1382 

1383 .. note:: 

1384 

1385 For the special case of a Composite transform, the maximum depth 

1386 of the two is returned. 

1387 

1388 """ 

1389 return 1 

1390 

1391 def contains_branch(self, other): 

1392 """ 

1393 Return whether the given transform is a sub-tree of this transform. 

1394 

1395 This routine uses transform equality to identify sub-trees, therefore 

1396 in many situations it is object id which will be used. 

1397 

1398 For the case where the given transform represents the whole 

1399 of this transform, returns True. 

1400 """ 

1401 if self.depth < other.depth: 

1402 return False 

1403 

1404 # check that a subtree is equal to other (starting from self) 

1405 for _, sub_tree in self._iter_break_from_left_to_right(): 

1406 if sub_tree == other: 

1407 return True 

1408 return False 

1409 

1410 def contains_branch_seperately(self, other_transform): 

1411 """ 

1412 Return whether the given branch is a sub-tree of this transform on 

1413 each separate dimension. 

1414 

1415 A common use for this method is to identify if a transform is a blended 

1416 transform containing an Axes' data transform. e.g.:: 

1417 

1418 x_isdata, y_isdata = trans.contains_branch_seperately(ax.transData) 

1419 

1420 """ 

1421 if self.output_dims != 2: 

1422 raise ValueError('contains_branch_seperately only supports ' 

1423 'transforms with 2 output dimensions') 

1424 # for a non-blended transform each separate dimension is the same, so 

1425 # just return the appropriate shape. 

1426 return (self.contains_branch(other_transform), ) * 2 

1427 

1428 def __sub__(self, other): 

1429 """ 

1430 Compose *self* with the inverse of *other*, cancelling identical terms 

1431 if any:: 

1432 

1433 # In general: 

1434 A - B == A + B.inverted() 

1435 # (but see note regarding frozen transforms below). 

1436 

1437 # If A "ends with" B (i.e. A == A' + B for some A') we can cancel 

1438 # out B: 

1439 (A' + B) - B == A' 

1440 

1441 # Likewise, if B "starts with" A (B = A + B'), we can cancel out A: 

1442 A - (A + B') == B'.inverted() == B'^-1 

1443 

1444 Cancellation (rather than naively returning ``A + B.inverted()``) is 

1445 important for multiple reasons: 

1446 

1447 - It avoids floating-point inaccuracies when computing the inverse of 

1448 B: ``B - B`` is guaranteed to cancel out exactly (resulting in the 

1449 identity transform), whereas ``B + B.inverted()`` may differ by a 

1450 small epsilon. 

1451 - ``B.inverted()`` always returns a frozen transform: if one computes 

1452 ``A + B + B.inverted()`` and later mutates ``B``, then 

1453 ``B.inverted()`` won't be updated and the last two terms won't cancel 

1454 out anymore; on the other hand, ``A + B - B`` will always be equal to 

1455 ``A`` even if ``B`` is mutated. 

1456 """ 

1457 # we only know how to do this operation if other is a Transform. 

1458 if not isinstance(other, Transform): 

1459 return NotImplemented 

1460 for remainder, sub_tree in self._iter_break_from_left_to_right(): 

1461 if sub_tree == other: 

1462 return remainder 

1463 for remainder, sub_tree in other._iter_break_from_left_to_right(): 

1464 if sub_tree == self: 

1465 if not remainder.has_inverse: 

1466 raise ValueError( 

1467 "The shortcut cannot be computed since 'other' " 

1468 "includes a non-invertible component") 

1469 return remainder.inverted() 

1470 # if we have got this far, then there was no shortcut possible 

1471 if other.has_inverse: 

1472 return self + other.inverted() 

1473 else: 

1474 raise ValueError('It is not possible to compute transA - transB ' 

1475 'since transB cannot be inverted and there is no ' 

1476 'shortcut possible.') 

1477 

1478 def __array__(self, *args, **kwargs): 

1479 """Array interface to get at this Transform's affine matrix.""" 

1480 return self.get_affine().get_matrix() 

1481 

1482 def transform(self, values): 

1483 """ 

1484 Apply this transformation on the given array of *values*. 

1485 

1486 Parameters 

1487 ---------- 

1488 values : array-like 

1489 The input values as an array of length :attr:`input_dims` or 

1490 shape (N, :attr:`input_dims`). 

1491 

1492 Returns 

1493 ------- 

1494 array 

1495 The output values as an array of length :attr:`output_dims` or 

1496 shape (N, :attr:`output_dims`), depending on the input. 

1497 """ 

1498 # Ensure that values is a 2d array (but remember whether 

1499 # we started with a 1d or 2d array). 

1500 values = np.asanyarray(values) 

1501 ndim = values.ndim 

1502 values = values.reshape((-1, self.input_dims)) 

1503 

1504 # Transform the values 

1505 res = self.transform_affine(self.transform_non_affine(values)) 

1506 

1507 # Convert the result back to the shape of the input values. 

1508 if ndim == 0: 

1509 assert not np.ma.is_masked(res) # just to be on the safe side 

1510 return res[0, 0] 

1511 if ndim == 1: 

1512 return res.reshape(-1) 

1513 elif ndim == 2: 

1514 return res 

1515 raise ValueError( 

1516 "Input values must have shape (N, {dims}) or ({dims},)" 

1517 .format(dims=self.input_dims)) 

1518 

1519 def transform_affine(self, values): 

1520 """ 

1521 Apply only the affine part of this transformation on the 

1522 given array of values. 

1523 

1524 ``transform(values)`` is always equivalent to 

1525 ``transform_affine(transform_non_affine(values))``. 

1526 

1527 In non-affine transformations, this is generally a no-op. In 

1528 affine transformations, this is equivalent to 

1529 ``transform(values)``. 

1530 

1531 Parameters 

1532 ---------- 

1533 values : array 

1534 The input values as an array of length :attr:`input_dims` or 

1535 shape (N, :attr:`input_dims`). 

1536 

1537 Returns 

1538 ------- 

1539 array 

1540 The output values as an array of length :attr:`output_dims` or 

1541 shape (N, :attr:`output_dims`), depending on the input. 

1542 """ 

1543 return self.get_affine().transform(values) 

1544 

1545 def transform_non_affine(self, values): 

1546 """ 

1547 Apply only the non-affine part of this transformation. 

1548 

1549 ``transform(values)`` is always equivalent to 

1550 ``transform_affine(transform_non_affine(values))``. 

1551 

1552 In non-affine transformations, this is generally equivalent to 

1553 ``transform(values)``. In affine transformations, this is 

1554 always a no-op. 

1555 

1556 Parameters 

1557 ---------- 

1558 values : array 

1559 The input values as an array of length :attr:`input_dims` or 

1560 shape (N, :attr:`input_dims`). 

1561 

1562 Returns 

1563 ------- 

1564 array 

1565 The output values as an array of length :attr:`output_dims` or 

1566 shape (N, :attr:`output_dims`), depending on the input. 

1567 """ 

1568 return values 

1569 

1570 def transform_bbox(self, bbox): 

1571 """ 

1572 Transform the given bounding box. 

1573 

1574 For smarter transforms including caching (a common requirement in 

1575 Matplotlib), see `TransformedBbox`. 

1576 """ 

1577 return Bbox(self.transform(bbox.get_points())) 

1578 

1579 def get_affine(self): 

1580 """Get the affine part of this transform.""" 

1581 return IdentityTransform() 

1582 

1583 def get_matrix(self): 

1584 """Get the matrix for the affine part of this transform.""" 

1585 return self.get_affine().get_matrix() 

1586 

1587 def transform_point(self, point): 

1588 """ 

1589 Return a transformed point. 

1590 

1591 This function is only kept for backcompatibility; the more general 

1592 `.transform` method is capable of transforming both a list of points 

1593 and a single point. 

1594 

1595 The point is given as a sequence of length :attr:`input_dims`. 

1596 The transformed point is returned as a sequence of length 

1597 :attr:`output_dims`. 

1598 """ 

1599 if len(point) != self.input_dims: 

1600 raise ValueError("The length of 'point' must be 'self.input_dims'") 

1601 return self.transform(point) 

1602 

1603 def transform_path(self, path): 

1604 """ 

1605 Apply the transform to `.Path` *path*, returning a new `.Path`. 

1606 

1607 In some cases, this transform may insert curves into the path 

1608 that began as line segments. 

1609 """ 

1610 return self.transform_path_affine(self.transform_path_non_affine(path)) 

1611 

1612 def transform_path_affine(self, path): 

1613 """ 

1614 Apply the affine part of this transform to `.Path` *path*, returning a 

1615 new `.Path`. 

1616 

1617 ``transform_path(path)`` is equivalent to 

1618 ``transform_path_affine(transform_path_non_affine(values))``. 

1619 """ 

1620 return self.get_affine().transform_path_affine(path) 

1621 

1622 def transform_path_non_affine(self, path): 

1623 """ 

1624 Apply the non-affine part of this transform to `.Path` *path*, 

1625 returning a new `.Path`. 

1626 

1627 ``transform_path(path)`` is equivalent to 

1628 ``transform_path_affine(transform_path_non_affine(values))``. 

1629 """ 

1630 x = self.transform_non_affine(path.vertices) 

1631 return Path._fast_from_codes_and_verts(x, path.codes, path) 

1632 

1633 def transform_angles(self, angles, pts, radians=False, pushoff=1e-5): 

1634 """ 

1635 Transform a set of angles anchored at specific locations. 

1636 

1637 Parameters 

1638 ---------- 

1639 angles : (N,) array-like 

1640 The angles to transform. 

1641 pts : (N, 2) array-like 

1642 The points where the angles are anchored. 

1643 radians : bool, default: False 

1644 Whether *angles* are radians or degrees. 

1645 pushoff : float 

1646 For each point in *pts* and angle in *angles*, the transformed 

1647 angle is computed by transforming a segment of length *pushoff* 

1648 starting at that point and making that angle relative to the 

1649 horizontal axis, and measuring the angle between the horizontal 

1650 axis and the transformed segment. 

1651 

1652 Returns 

1653 ------- 

1654 (N,) array 

1655 """ 

1656 # Must be 2D 

1657 if self.input_dims != 2 or self.output_dims != 2: 

1658 raise NotImplementedError('Only defined in 2D') 

1659 angles = np.asarray(angles) 

1660 pts = np.asarray(pts) 

1661 _api.check_shape((None, 2), pts=pts) 

1662 _api.check_shape((None,), angles=angles) 

1663 if len(angles) != len(pts): 

1664 raise ValueError("There must be as many 'angles' as 'pts'") 

1665 # Convert to radians if desired 

1666 if not radians: 

1667 angles = np.deg2rad(angles) 

1668 # Move a short distance away 

1669 pts2 = pts + pushoff * np.column_stack([np.cos(angles), 

1670 np.sin(angles)]) 

1671 # Transform both sets of points 

1672 tpts = self.transform(pts) 

1673 tpts2 = self.transform(pts2) 

1674 # Calculate transformed angles 

1675 d = tpts2 - tpts 

1676 a = np.arctan2(d[:, 1], d[:, 0]) 

1677 # Convert back to degrees if desired 

1678 if not radians: 

1679 a = np.rad2deg(a) 

1680 return a 

1681 

1682 def inverted(self): 

1683 """ 

1684 Return the corresponding inverse transformation. 

1685 

1686 It holds ``x == self.inverted().transform(self.transform(x))``. 

1687 

1688 The return value of this method should be treated as 

1689 temporary. An update to *self* does not cause a corresponding 

1690 update to its inverted copy. 

1691 """ 

1692 raise NotImplementedError() 

1693 

1694 

1695class TransformWrapper(Transform): 

1696 """ 

1697 A helper class that holds a single child transform and acts 

1698 equivalently to it. 

1699 

1700 This is useful if a node of the transform tree must be replaced at 

1701 run time with a transform of a different type. This class allows 

1702 that replacement to correctly trigger invalidation. 

1703 

1704 `TransformWrapper` instances must have the same input and output dimensions 

1705 during their entire lifetime, so the child transform may only be replaced 

1706 with another child transform of the same dimensions. 

1707 """ 

1708 

1709 pass_through = True 

1710 

1711 def __init__(self, child): 

1712 """ 

1713 *child*: A `Transform` instance. This child may later 

1714 be replaced with :meth:`set`. 

1715 """ 

1716 _api.check_isinstance(Transform, child=child) 

1717 super().__init__() 

1718 self.set(child) 

1719 

1720 def __eq__(self, other): 

1721 return self._child.__eq__(other) 

1722 

1723 __str__ = _make_str_method("_child") 

1724 

1725 def frozen(self): 

1726 # docstring inherited 

1727 return self._child.frozen() 

1728 

1729 def set(self, child): 

1730 """ 

1731 Replace the current child of this transform with another one. 

1732 

1733 The new child must have the same number of input and output 

1734 dimensions as the current child. 

1735 """ 

1736 if hasattr(self, "_child"): # Absent during init. 

1737 self.invalidate() 

1738 new_dims = (child.input_dims, child.output_dims) 

1739 old_dims = (self._child.input_dims, self._child.output_dims) 

1740 if new_dims != old_dims: 

1741 raise ValueError( 

1742 f"The input and output dims of the new child {new_dims} " 

1743 f"do not match those of current child {old_dims}") 

1744 self._child._parents.pop(id(self), None) 

1745 

1746 self._child = child 

1747 self.set_children(child) 

1748 

1749 self.transform = child.transform 

1750 self.transform_affine = child.transform_affine 

1751 self.transform_non_affine = child.transform_non_affine 

1752 self.transform_path = child.transform_path 

1753 self.transform_path_affine = child.transform_path_affine 

1754 self.transform_path_non_affine = child.transform_path_non_affine 

1755 self.get_affine = child.get_affine 

1756 self.inverted = child.inverted 

1757 self.get_matrix = child.get_matrix 

1758 # note we do not wrap other properties here since the transform's 

1759 # child can be changed with WrappedTransform.set and so checking 

1760 # is_affine and other such properties may be dangerous. 

1761 

1762 self._invalid = 0 

1763 self.invalidate() 

1764 self._invalid = 0 

1765 

1766 input_dims = property(lambda self: self._child.input_dims) 

1767 output_dims = property(lambda self: self._child.output_dims) 

1768 is_affine = property(lambda self: self._child.is_affine) 

1769 is_separable = property(lambda self: self._child.is_separable) 

1770 has_inverse = property(lambda self: self._child.has_inverse) 

1771 

1772 

1773class AffineBase(Transform): 

1774 """ 

1775 The base class of all affine transformations of any number of dimensions. 

1776 """ 

1777 is_affine = True 

1778 

1779 def __init__(self, *args, **kwargs): 

1780 super().__init__(*args, **kwargs) 

1781 self._inverted = None 

1782 

1783 def __array__(self, *args, **kwargs): 

1784 # optimises the access of the transform matrix vs. the superclass 

1785 return self.get_matrix() 

1786 

1787 def __eq__(self, other): 

1788 if getattr(other, "is_affine", False) and hasattr(other, "get_matrix"): 

1789 return (self.get_matrix() == other.get_matrix()).all() 

1790 return NotImplemented 

1791 

1792 def transform(self, values): 

1793 # docstring inherited 

1794 return self.transform_affine(values) 

1795 

1796 def transform_affine(self, values): 

1797 # docstring inherited 

1798 raise NotImplementedError('Affine subclasses should override this ' 

1799 'method.') 

1800 

1801 @_api.rename_parameter("3.8", "points", "values") 

1802 def transform_non_affine(self, values): 

1803 # docstring inherited 

1804 return values 

1805 

1806 def transform_path(self, path): 

1807 # docstring inherited 

1808 return self.transform_path_affine(path) 

1809 

1810 def transform_path_affine(self, path): 

1811 # docstring inherited 

1812 return Path(self.transform_affine(path.vertices), 

1813 path.codes, path._interpolation_steps) 

1814 

1815 def transform_path_non_affine(self, path): 

1816 # docstring inherited 

1817 return path 

1818 

1819 def get_affine(self): 

1820 # docstring inherited 

1821 return self 

1822 

1823 

1824class Affine2DBase(AffineBase): 

1825 """ 

1826 The base class of all 2D affine transformations. 

1827 

1828 2D affine transformations are performed using a 3x3 numpy array:: 

1829 

1830 a c e 

1831 b d f 

1832 0 0 1 

1833 

1834 This class provides the read-only interface. For a mutable 2D 

1835 affine transformation, use `Affine2D`. 

1836 

1837 Subclasses of this class will generally only need to override a 

1838 constructor and `~.Transform.get_matrix` that generates a custom 3x3 matrix. 

1839 """ 

1840 input_dims = 2 

1841 output_dims = 2 

1842 

1843 def frozen(self): 

1844 # docstring inherited 

1845 return Affine2D(self.get_matrix().copy()) 

1846 

1847 @property 

1848 def is_separable(self): 

1849 mtx = self.get_matrix() 

1850 return mtx[0, 1] == mtx[1, 0] == 0.0 

1851 

1852 def to_values(self): 

1853 """ 

1854 Return the values of the matrix as an ``(a, b, c, d, e, f)`` tuple. 

1855 """ 

1856 mtx = self.get_matrix() 

1857 return tuple(mtx[:2].swapaxes(0, 1).flat) 

1858 

1859 @_api.rename_parameter("3.8", "points", "values") 

1860 def transform_affine(self, values): 

1861 mtx = self.get_matrix() 

1862 if isinstance(values, np.ma.MaskedArray): 

1863 tpoints = affine_transform(values.data, mtx) 

1864 return np.ma.MaskedArray(tpoints, mask=np.ma.getmask(values)) 

1865 return affine_transform(values, mtx) 

1866 

1867 if DEBUG: 

1868 _transform_affine = transform_affine 

1869 

1870 @_api.rename_parameter("3.8", "points", "values") 

1871 def transform_affine(self, values): 

1872 # docstring inherited 

1873 # The major speed trap here is just converting to the 

1874 # points to an array in the first place. If we can use 

1875 # more arrays upstream, that should help here. 

1876 if not isinstance(values, np.ndarray): 

1877 _api.warn_external( 

1878 f'A non-numpy array of type {type(values)} was passed in ' 

1879 f'for transformation, which results in poor performance.') 

1880 return self._transform_affine(values) 

1881 

1882 def inverted(self): 

1883 # docstring inherited 

1884 if self._inverted is None or self._invalid: 

1885 mtx = self.get_matrix() 

1886 shorthand_name = None 

1887 if self._shorthand_name: 

1888 shorthand_name = '(%s)-1' % self._shorthand_name 

1889 self._inverted = Affine2D(inv(mtx), shorthand_name=shorthand_name) 

1890 self._invalid = 0 

1891 return self._inverted 

1892 

1893 

1894class Affine2D(Affine2DBase): 

1895 """ 

1896 A mutable 2D affine transformation. 

1897 """ 

1898 

1899 def __init__(self, matrix=None, **kwargs): 

1900 """ 

1901 Initialize an Affine transform from a 3x3 numpy float array:: 

1902 

1903 a c e 

1904 b d f 

1905 0 0 1 

1906 

1907 If *matrix* is None, initialize with the identity transform. 

1908 """ 

1909 super().__init__(**kwargs) 

1910 if matrix is None: 

1911 # A bit faster than np.identity(3). 

1912 matrix = IdentityTransform._mtx 

1913 self._mtx = matrix.copy() 

1914 self._invalid = 0 

1915 

1916 _base_str = _make_str_method("_mtx") 

1917 

1918 def __str__(self): 

1919 return (self._base_str() 

1920 if (self._mtx != np.diag(np.diag(self._mtx))).any() 

1921 else f"Affine2D().scale({self._mtx[0, 0]}, {self._mtx[1, 1]})" 

1922 if self._mtx[0, 0] != self._mtx[1, 1] 

1923 else f"Affine2D().scale({self._mtx[0, 0]})") 

1924 

1925 @staticmethod 

1926 def from_values(a, b, c, d, e, f): 

1927 """ 

1928 Create a new Affine2D instance from the given values:: 

1929 

1930 a c e 

1931 b d f 

1932 0 0 1 

1933 

1934 . 

1935 """ 

1936 return Affine2D( 

1937 np.array([a, c, e, b, d, f, 0.0, 0.0, 1.0], float).reshape((3, 3))) 

1938 

1939 def get_matrix(self): 

1940 """ 

1941 Get the underlying transformation matrix as a 3x3 array:: 

1942 

1943 a c e 

1944 b d f 

1945 0 0 1 

1946 

1947 . 

1948 """ 

1949 if self._invalid: 

1950 self._inverted = None 

1951 self._invalid = 0 

1952 return self._mtx 

1953 

1954 def set_matrix(self, mtx): 

1955 """ 

1956 Set the underlying transformation matrix from a 3x3 array:: 

1957 

1958 a c e 

1959 b d f 

1960 0 0 1 

1961 

1962 . 

1963 """ 

1964 self._mtx = mtx 

1965 self.invalidate() 

1966 

1967 def set(self, other): 

1968 """ 

1969 Set this transformation from the frozen copy of another 

1970 `Affine2DBase` object. 

1971 """ 

1972 _api.check_isinstance(Affine2DBase, other=other) 

1973 self._mtx = other.get_matrix() 

1974 self.invalidate() 

1975 

1976 def clear(self): 

1977 """ 

1978 Reset the underlying matrix to the identity transform. 

1979 """ 

1980 # A bit faster than np.identity(3). 

1981 self._mtx = IdentityTransform._mtx.copy() 

1982 self.invalidate() 

1983 return self 

1984 

1985 def rotate(self, theta): 

1986 """ 

1987 Add a rotation (in radians) to this transform in place. 

1988 

1989 Returns *self*, so this method can easily be chained with more 

1990 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 

1991 and :meth:`scale`. 

1992 """ 

1993 a = math.cos(theta) 

1994 b = math.sin(theta) 

1995 mtx = self._mtx 

1996 # Operating and assigning one scalar at a time is much faster. 

1997 (xx, xy, x0), (yx, yy, y0), _ = mtx.tolist() 

1998 # mtx = [[a -b 0], [b a 0], [0 0 1]] * mtx 

1999 mtx[0, 0] = a * xx - b * yx 

2000 mtx[0, 1] = a * xy - b * yy 

2001 mtx[0, 2] = a * x0 - b * y0 

2002 mtx[1, 0] = b * xx + a * yx 

2003 mtx[1, 1] = b * xy + a * yy 

2004 mtx[1, 2] = b * x0 + a * y0 

2005 self.invalidate() 

2006 return self 

2007 

2008 def rotate_deg(self, degrees): 

2009 """ 

2010 Add a rotation (in degrees) to this transform in place. 

2011 

2012 Returns *self*, so this method can easily be chained with more 

2013 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 

2014 and :meth:`scale`. 

2015 """ 

2016 return self.rotate(math.radians(degrees)) 

2017 

2018 def rotate_around(self, x, y, theta): 

2019 """ 

2020 Add a rotation (in radians) around the point (x, y) in place. 

2021 

2022 Returns *self*, so this method can easily be chained with more 

2023 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 

2024 and :meth:`scale`. 

2025 """ 

2026 return self.translate(-x, -y).rotate(theta).translate(x, y) 

2027 

2028 def rotate_deg_around(self, x, y, degrees): 

2029 """ 

2030 Add a rotation (in degrees) around the point (x, y) in place. 

2031 

2032 Returns *self*, so this method can easily be chained with more 

2033 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 

2034 and :meth:`scale`. 

2035 """ 

2036 # Cast to float to avoid wraparound issues with uint8's 

2037 x, y = float(x), float(y) 

2038 return self.translate(-x, -y).rotate_deg(degrees).translate(x, y) 

2039 

2040 def translate(self, tx, ty): 

2041 """ 

2042 Add a translation in place. 

2043 

2044 Returns *self*, so this method can easily be chained with more 

2045 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 

2046 and :meth:`scale`. 

2047 """ 

2048 self._mtx[0, 2] += tx 

2049 self._mtx[1, 2] += ty 

2050 self.invalidate() 

2051 return self 

2052 

2053 def scale(self, sx, sy=None): 

2054 """ 

2055 Add a scale in place. 

2056 

2057 If *sy* is None, the same scale is applied in both the *x*- and 

2058 *y*-directions. 

2059 

2060 Returns *self*, so this method can easily be chained with more 

2061 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 

2062 and :meth:`scale`. 

2063 """ 

2064 if sy is None: 

2065 sy = sx 

2066 # explicit element-wise scaling is fastest 

2067 self._mtx[0, 0] *= sx 

2068 self._mtx[0, 1] *= sx 

2069 self._mtx[0, 2] *= sx 

2070 self._mtx[1, 0] *= sy 

2071 self._mtx[1, 1] *= sy 

2072 self._mtx[1, 2] *= sy 

2073 self.invalidate() 

2074 return self 

2075 

2076 def skew(self, xShear, yShear): 

2077 """ 

2078 Add a skew in place. 

2079 

2080 *xShear* and *yShear* are the shear angles along the *x*- and 

2081 *y*-axes, respectively, in radians. 

2082 

2083 Returns *self*, so this method can easily be chained with more 

2084 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 

2085 and :meth:`scale`. 

2086 """ 

2087 rx = math.tan(xShear) 

2088 ry = math.tan(yShear) 

2089 mtx = self._mtx 

2090 # Operating and assigning one scalar at a time is much faster. 

2091 (xx, xy, x0), (yx, yy, y0), _ = mtx.tolist() 

2092 # mtx = [[1 rx 0], [ry 1 0], [0 0 1]] * mtx 

2093 mtx[0, 0] += rx * yx 

2094 mtx[0, 1] += rx * yy 

2095 mtx[0, 2] += rx * y0 

2096 mtx[1, 0] += ry * xx 

2097 mtx[1, 1] += ry * xy 

2098 mtx[1, 2] += ry * x0 

2099 self.invalidate() 

2100 return self 

2101 

2102 def skew_deg(self, xShear, yShear): 

2103 """ 

2104 Add a skew in place. 

2105 

2106 *xShear* and *yShear* are the shear angles along the *x*- and 

2107 *y*-axes, respectively, in degrees. 

2108 

2109 Returns *self*, so this method can easily be chained with more 

2110 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 

2111 and :meth:`scale`. 

2112 """ 

2113 return self.skew(math.radians(xShear), math.radians(yShear)) 

2114 

2115 

2116class IdentityTransform(Affine2DBase): 

2117 """ 

2118 A special class that does one thing, the identity transform, in a 

2119 fast way. 

2120 """ 

2121 _mtx = np.identity(3) 

2122 

2123 def frozen(self): 

2124 # docstring inherited 

2125 return self 

2126 

2127 __str__ = _make_str_method() 

2128 

2129 def get_matrix(self): 

2130 # docstring inherited 

2131 return self._mtx 

2132 

2133 @_api.rename_parameter("3.8", "points", "values") 

2134 def transform(self, values): 

2135 # docstring inherited 

2136 return np.asanyarray(values) 

2137 

2138 @_api.rename_parameter("3.8", "points", "values") 

2139 def transform_affine(self, values): 

2140 # docstring inherited 

2141 return np.asanyarray(values) 

2142 

2143 @_api.rename_parameter("3.8", "points", "values") 

2144 def transform_non_affine(self, values): 

2145 # docstring inherited 

2146 return np.asanyarray(values) 

2147 

2148 def transform_path(self, path): 

2149 # docstring inherited 

2150 return path 

2151 

2152 def transform_path_affine(self, path): 

2153 # docstring inherited 

2154 return path 

2155 

2156 def transform_path_non_affine(self, path): 

2157 # docstring inherited 

2158 return path 

2159 

2160 def get_affine(self): 

2161 # docstring inherited 

2162 return self 

2163 

2164 def inverted(self): 

2165 # docstring inherited 

2166 return self 

2167 

2168 

2169class _BlendedMixin: 

2170 """Common methods for `BlendedGenericTransform` and `BlendedAffine2D`.""" 

2171 

2172 def __eq__(self, other): 

2173 if isinstance(other, (BlendedAffine2D, BlendedGenericTransform)): 

2174 return (self._x == other._x) and (self._y == other._y) 

2175 elif self._x == self._y: 

2176 return self._x == other 

2177 else: 

2178 return NotImplemented 

2179 

2180 def contains_branch_seperately(self, transform): 

2181 return (self._x.contains_branch(transform), 

2182 self._y.contains_branch(transform)) 

2183 

2184 __str__ = _make_str_method("_x", "_y") 

2185 

2186 

2187class BlendedGenericTransform(_BlendedMixin, Transform): 

2188 """ 

2189 A "blended" transform uses one transform for the *x*-direction, and 

2190 another transform for the *y*-direction. 

2191 

2192 This "generic" version can handle any given child transform in the 

2193 *x*- and *y*-directions. 

2194 """ 

2195 input_dims = 2 

2196 output_dims = 2 

2197 is_separable = True 

2198 pass_through = True 

2199 

2200 def __init__(self, x_transform, y_transform, **kwargs): 

2201 """ 

2202 Create a new "blended" transform using *x_transform* to transform the 

2203 *x*-axis and *y_transform* to transform the *y*-axis. 

2204 

2205 You will generally not call this constructor directly but use the 

2206 `blended_transform_factory` function instead, which can determine 

2207 automatically which kind of blended transform to create. 

2208 """ 

2209 Transform.__init__(self, **kwargs) 

2210 self._x = x_transform 

2211 self._y = y_transform 

2212 self.set_children(x_transform, y_transform) 

2213 self._affine = None 

2214 

2215 @property 

2216 def depth(self): 

2217 return max(self._x.depth, self._y.depth) 

2218 

2219 def contains_branch(self, other): 

2220 # A blended transform cannot possibly contain a branch from two 

2221 # different transforms. 

2222 return False 

2223 

2224 is_affine = property(lambda self: self._x.is_affine and self._y.is_affine) 

2225 has_inverse = property( 

2226 lambda self: self._x.has_inverse and self._y.has_inverse) 

2227 

2228 def frozen(self): 

2229 # docstring inherited 

2230 return blended_transform_factory(self._x.frozen(), self._y.frozen()) 

2231 

2232 @_api.rename_parameter("3.8", "points", "values") 

2233 def transform_non_affine(self, values): 

2234 # docstring inherited 

2235 if self._x.is_affine and self._y.is_affine: 

2236 return values 

2237 x = self._x 

2238 y = self._y 

2239 

2240 if x == y and x.input_dims == 2: 

2241 return x.transform_non_affine(values) 

2242 

2243 if x.input_dims == 2: 

2244 x_points = x.transform_non_affine(values)[:, 0:1] 

2245 else: 

2246 x_points = x.transform_non_affine(values[:, 0]) 

2247 x_points = x_points.reshape((len(x_points), 1)) 

2248 

2249 if y.input_dims == 2: 

2250 y_points = y.transform_non_affine(values)[:, 1:] 

2251 else: 

2252 y_points = y.transform_non_affine(values[:, 1]) 

2253 y_points = y_points.reshape((len(y_points), 1)) 

2254 

2255 if (isinstance(x_points, np.ma.MaskedArray) or 

2256 isinstance(y_points, np.ma.MaskedArray)): 

2257 return np.ma.concatenate((x_points, y_points), 1) 

2258 else: 

2259 return np.concatenate((x_points, y_points), 1) 

2260 

2261 def inverted(self): 

2262 # docstring inherited 

2263 return BlendedGenericTransform(self._x.inverted(), self._y.inverted()) 

2264 

2265 def get_affine(self): 

2266 # docstring inherited 

2267 if self._invalid or self._affine is None: 

2268 if self._x == self._y: 

2269 self._affine = self._x.get_affine() 

2270 else: 

2271 x_mtx = self._x.get_affine().get_matrix() 

2272 y_mtx = self._y.get_affine().get_matrix() 

2273 # We already know the transforms are separable, so we can skip 

2274 # setting b and c to zero. 

2275 mtx = np.array([x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0]]) 

2276 self._affine = Affine2D(mtx) 

2277 self._invalid = 0 

2278 return self._affine 

2279 

2280 

2281class BlendedAffine2D(_BlendedMixin, Affine2DBase): 

2282 """ 

2283 A "blended" transform uses one transform for the *x*-direction, and 

2284 another transform for the *y*-direction. 

2285 

2286 This version is an optimization for the case where both child 

2287 transforms are of type `Affine2DBase`. 

2288 """ 

2289 

2290 is_separable = True 

2291 

2292 def __init__(self, x_transform, y_transform, **kwargs): 

2293 """ 

2294 Create a new "blended" transform using *x_transform* to transform the 

2295 *x*-axis and *y_transform* to transform the *y*-axis. 

2296 

2297 Both *x_transform* and *y_transform* must be 2D affine transforms. 

2298 

2299 You will generally not call this constructor directly but use the 

2300 `blended_transform_factory` function instead, which can determine 

2301 automatically which kind of blended transform to create. 

2302 """ 

2303 is_affine = x_transform.is_affine and y_transform.is_affine 

2304 is_separable = x_transform.is_separable and y_transform.is_separable 

2305 is_correct = is_affine and is_separable 

2306 if not is_correct: 

2307 raise ValueError("Both *x_transform* and *y_transform* must be 2D " 

2308 "affine transforms") 

2309 

2310 Transform.__init__(self, **kwargs) 

2311 self._x = x_transform 

2312 self._y = y_transform 

2313 self.set_children(x_transform, y_transform) 

2314 

2315 Affine2DBase.__init__(self) 

2316 self._mtx = None 

2317 

2318 def get_matrix(self): 

2319 # docstring inherited 

2320 if self._invalid: 

2321 if self._x == self._y: 

2322 self._mtx = self._x.get_matrix() 

2323 else: 

2324 x_mtx = self._x.get_matrix() 

2325 y_mtx = self._y.get_matrix() 

2326 # We already know the transforms are separable, so we can skip 

2327 # setting b and c to zero. 

2328 self._mtx = np.array([x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0]]) 

2329 self._inverted = None 

2330 self._invalid = 0 

2331 return self._mtx 

2332 

2333 

2334def blended_transform_factory(x_transform, y_transform): 

2335 """ 

2336 Create a new "blended" transform using *x_transform* to transform 

2337 the *x*-axis and *y_transform* to transform the *y*-axis. 

2338 

2339 A faster version of the blended transform is returned for the case 

2340 where both child transforms are affine. 

2341 """ 

2342 if (isinstance(x_transform, Affine2DBase) and 

2343 isinstance(y_transform, Affine2DBase)): 

2344 return BlendedAffine2D(x_transform, y_transform) 

2345 return BlendedGenericTransform(x_transform, y_transform) 

2346 

2347 

2348class CompositeGenericTransform(Transform): 

2349 """ 

2350 A composite transform formed by applying transform *a* then 

2351 transform *b*. 

2352 

2353 This "generic" version can handle any two arbitrary 

2354 transformations. 

2355 """ 

2356 pass_through = True 

2357 

2358 def __init__(self, a, b, **kwargs): 

2359 """ 

2360 Create a new composite transform that is the result of 

2361 applying transform *a* then transform *b*. 

2362 

2363 You will generally not call this constructor directly but write ``a + 

2364 b`` instead, which will automatically choose the best kind of composite 

2365 transform instance to create. 

2366 """ 

2367 if a.output_dims != b.input_dims: 

2368 raise ValueError("The output dimension of 'a' must be equal to " 

2369 "the input dimensions of 'b'") 

2370 self.input_dims = a.input_dims 

2371 self.output_dims = b.output_dims 

2372 

2373 super().__init__(**kwargs) 

2374 self._a = a 

2375 self._b = b 

2376 self.set_children(a, b) 

2377 

2378 def frozen(self): 

2379 # docstring inherited 

2380 self._invalid = 0 

2381 frozen = composite_transform_factory( 

2382 self._a.frozen(), self._b.frozen()) 

2383 if not isinstance(frozen, CompositeGenericTransform): 

2384 return frozen.frozen() 

2385 return frozen 

2386 

2387 def _invalidate_internal(self, level, invalidating_node): 

2388 # When the left child is invalidated at AFFINE_ONLY level and the right child is 

2389 # non-affine, the composite transform is FULLY invalidated. 

2390 if invalidating_node is self._a and not self._b.is_affine: 

2391 level = Transform._INVALID_FULL 

2392 super()._invalidate_internal(level, invalidating_node) 

2393 

2394 def __eq__(self, other): 

2395 if isinstance(other, (CompositeGenericTransform, CompositeAffine2D)): 

2396 return self is other or (self._a == other._a 

2397 and self._b == other._b) 

2398 else: 

2399 return False 

2400 

2401 def _iter_break_from_left_to_right(self): 

2402 for left, right in self._a._iter_break_from_left_to_right(): 

2403 yield left, right + self._b 

2404 for left, right in self._b._iter_break_from_left_to_right(): 

2405 yield self._a + left, right 

2406 

2407 def contains_branch_seperately(self, other_transform): 

2408 # docstring inherited 

2409 if self.output_dims != 2: 

2410 raise ValueError('contains_branch_seperately only supports ' 

2411 'transforms with 2 output dimensions') 

2412 if self == other_transform: 

2413 return (True, True) 

2414 return self._b.contains_branch_seperately(other_transform) 

2415 

2416 depth = property(lambda self: self._a.depth + self._b.depth) 

2417 is_affine = property(lambda self: self._a.is_affine and self._b.is_affine) 

2418 is_separable = property( 

2419 lambda self: self._a.is_separable and self._b.is_separable) 

2420 has_inverse = property( 

2421 lambda self: self._a.has_inverse and self._b.has_inverse) 

2422 

2423 __str__ = _make_str_method("_a", "_b") 

2424 

2425 @_api.rename_parameter("3.8", "points", "values") 

2426 def transform_affine(self, values): 

2427 # docstring inherited 

2428 return self.get_affine().transform(values) 

2429 

2430 @_api.rename_parameter("3.8", "points", "values") 

2431 def transform_non_affine(self, values): 

2432 # docstring inherited 

2433 if self._a.is_affine and self._b.is_affine: 

2434 return values 

2435 elif not self._a.is_affine and self._b.is_affine: 

2436 return self._a.transform_non_affine(values) 

2437 else: 

2438 return self._b.transform_non_affine(self._a.transform(values)) 

2439 

2440 def transform_path_non_affine(self, path): 

2441 # docstring inherited 

2442 if self._a.is_affine and self._b.is_affine: 

2443 return path 

2444 elif not self._a.is_affine and self._b.is_affine: 

2445 return self._a.transform_path_non_affine(path) 

2446 else: 

2447 return self._b.transform_path_non_affine( 

2448 self._a.transform_path(path)) 

2449 

2450 def get_affine(self): 

2451 # docstring inherited 

2452 if not self._b.is_affine: 

2453 return self._b.get_affine() 

2454 else: 

2455 return Affine2D(np.dot(self._b.get_affine().get_matrix(), 

2456 self._a.get_affine().get_matrix())) 

2457 

2458 def inverted(self): 

2459 # docstring inherited 

2460 return CompositeGenericTransform( 

2461 self._b.inverted(), self._a.inverted()) 

2462 

2463 

2464class CompositeAffine2D(Affine2DBase): 

2465 """ 

2466 A composite transform formed by applying transform *a* then transform *b*. 

2467 

2468 This version is an optimization that handles the case where both *a* 

2469 and *b* are 2D affines. 

2470 """ 

2471 def __init__(self, a, b, **kwargs): 

2472 """ 

2473 Create a new composite transform that is the result of 

2474 applying `Affine2DBase` *a* then `Affine2DBase` *b*. 

2475 

2476 You will generally not call this constructor directly but write ``a + 

2477 b`` instead, which will automatically choose the best kind of composite 

2478 transform instance to create. 

2479 """ 

2480 if not a.is_affine or not b.is_affine: 

2481 raise ValueError("'a' and 'b' must be affine transforms") 

2482 if a.output_dims != b.input_dims: 

2483 raise ValueError("The output dimension of 'a' must be equal to " 

2484 "the input dimensions of 'b'") 

2485 self.input_dims = a.input_dims 

2486 self.output_dims = b.output_dims 

2487 

2488 super().__init__(**kwargs) 

2489 self._a = a 

2490 self._b = b 

2491 self.set_children(a, b) 

2492 self._mtx = None 

2493 

2494 @property 

2495 def depth(self): 

2496 return self._a.depth + self._b.depth 

2497 

2498 def _iter_break_from_left_to_right(self): 

2499 for left, right in self._a._iter_break_from_left_to_right(): 

2500 yield left, right + self._b 

2501 for left, right in self._b._iter_break_from_left_to_right(): 

2502 yield self._a + left, right 

2503 

2504 __str__ = _make_str_method("_a", "_b") 

2505 

2506 def get_matrix(self): 

2507 # docstring inherited 

2508 if self._invalid: 

2509 self._mtx = np.dot( 

2510 self._b.get_matrix(), 

2511 self._a.get_matrix()) 

2512 self._inverted = None 

2513 self._invalid = 0 

2514 return self._mtx 

2515 

2516 

2517def composite_transform_factory(a, b): 

2518 """ 

2519 Create a new composite transform that is the result of applying 

2520 transform a then transform b. 

2521 

2522 Shortcut versions of the blended transform are provided for the 

2523 case where both child transforms are affine, or one or the other 

2524 is the identity transform. 

2525 

2526 Composite transforms may also be created using the '+' operator, 

2527 e.g.:: 

2528 

2529 c = a + b 

2530 """ 

2531 # check to see if any of a or b are IdentityTransforms. We use 

2532 # isinstance here to guarantee that the transforms will *always* 

2533 # be IdentityTransforms. Since TransformWrappers are mutable, 

2534 # use of equality here would be wrong. 

2535 if isinstance(a, IdentityTransform): 

2536 return b 

2537 elif isinstance(b, IdentityTransform): 

2538 return a 

2539 elif isinstance(a, Affine2D) and isinstance(b, Affine2D): 

2540 return CompositeAffine2D(a, b) 

2541 return CompositeGenericTransform(a, b) 

2542 

2543 

2544class BboxTransform(Affine2DBase): 

2545 """ 

2546 `BboxTransform` linearly transforms points from one `Bbox` to another. 

2547 """ 

2548 

2549 is_separable = True 

2550 

2551 def __init__(self, boxin, boxout, **kwargs): 

2552 """ 

2553 Create a new `BboxTransform` that linearly transforms 

2554 points from *boxin* to *boxout*. 

2555 """ 

2556 _api.check_isinstance(BboxBase, boxin=boxin, boxout=boxout) 

2557 

2558 super().__init__(**kwargs) 

2559 self._boxin = boxin 

2560 self._boxout = boxout 

2561 self.set_children(boxin, boxout) 

2562 self._mtx = None 

2563 self._inverted = None 

2564 

2565 __str__ = _make_str_method("_boxin", "_boxout") 

2566 

2567 def get_matrix(self): 

2568 # docstring inherited 

2569 if self._invalid: 

2570 inl, inb, inw, inh = self._boxin.bounds 

2571 outl, outb, outw, outh = self._boxout.bounds 

2572 x_scale = outw / inw 

2573 y_scale = outh / inh 

2574 if DEBUG and (x_scale == 0 or y_scale == 0): 

2575 raise ValueError( 

2576 "Transforming from or to a singular bounding box") 

2577 self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale+outl)], 

2578 [0.0 , y_scale, (-inb*y_scale+outb)], 

2579 [0.0 , 0.0 , 1.0 ]], 

2580 float) 

2581 self._inverted = None 

2582 self._invalid = 0 

2583 return self._mtx 

2584 

2585 

2586class BboxTransformTo(Affine2DBase): 

2587 """ 

2588 `BboxTransformTo` is a transformation that linearly transforms points from 

2589 the unit bounding box to a given `Bbox`. 

2590 """ 

2591 

2592 is_separable = True 

2593 

2594 def __init__(self, boxout, **kwargs): 

2595 """ 

2596 Create a new `BboxTransformTo` that linearly transforms 

2597 points from the unit bounding box to *boxout*. 

2598 """ 

2599 _api.check_isinstance(BboxBase, boxout=boxout) 

2600 

2601 super().__init__(**kwargs) 

2602 self._boxout = boxout 

2603 self.set_children(boxout) 

2604 self._mtx = None 

2605 self._inverted = None 

2606 

2607 __str__ = _make_str_method("_boxout") 

2608 

2609 def get_matrix(self): 

2610 # docstring inherited 

2611 if self._invalid: 

2612 outl, outb, outw, outh = self._boxout.bounds 

2613 if DEBUG and (outw == 0 or outh == 0): 

2614 raise ValueError("Transforming to a singular bounding box.") 

2615 self._mtx = np.array([[outw, 0.0, outl], 

2616 [ 0.0, outh, outb], 

2617 [ 0.0, 0.0, 1.0]], 

2618 float) 

2619 self._inverted = None 

2620 self._invalid = 0 

2621 return self._mtx 

2622 

2623 

2624@_api.deprecated("3.9") 

2625class BboxTransformToMaxOnly(BboxTransformTo): 

2626 """ 

2627 `BboxTransformToMaxOnly` is a transformation that linearly transforms points from 

2628 the unit bounding box to a given `Bbox` with a fixed upper left of (0, 0). 

2629 """ 

2630 def get_matrix(self): 

2631 # docstring inherited 

2632 if self._invalid: 

2633 xmax, ymax = self._boxout.max 

2634 if DEBUG and (xmax == 0 or ymax == 0): 

2635 raise ValueError("Transforming to a singular bounding box.") 

2636 self._mtx = np.array([[xmax, 0.0, 0.0], 

2637 [ 0.0, ymax, 0.0], 

2638 [ 0.0, 0.0, 1.0]], 

2639 float) 

2640 self._inverted = None 

2641 self._invalid = 0 

2642 return self._mtx 

2643 

2644 

2645class BboxTransformFrom(Affine2DBase): 

2646 """ 

2647 `BboxTransformFrom` linearly transforms points from a given `Bbox` to the 

2648 unit bounding box. 

2649 """ 

2650 is_separable = True 

2651 

2652 def __init__(self, boxin, **kwargs): 

2653 _api.check_isinstance(BboxBase, boxin=boxin) 

2654 

2655 super().__init__(**kwargs) 

2656 self._boxin = boxin 

2657 self.set_children(boxin) 

2658 self._mtx = None 

2659 self._inverted = None 

2660 

2661 __str__ = _make_str_method("_boxin") 

2662 

2663 def get_matrix(self): 

2664 # docstring inherited 

2665 if self._invalid: 

2666 inl, inb, inw, inh = self._boxin.bounds 

2667 if DEBUG and (inw == 0 or inh == 0): 

2668 raise ValueError("Transforming from a singular bounding box.") 

2669 x_scale = 1.0 / inw 

2670 y_scale = 1.0 / inh 

2671 self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale)], 

2672 [0.0 , y_scale, (-inb*y_scale)], 

2673 [0.0 , 0.0 , 1.0 ]], 

2674 float) 

2675 self._inverted = None 

2676 self._invalid = 0 

2677 return self._mtx 

2678 

2679 

2680class ScaledTranslation(Affine2DBase): 

2681 """ 

2682 A transformation that translates by *xt* and *yt*, after *xt* and *yt* 

2683 have been transformed by *scale_trans*. 

2684 """ 

2685 def __init__(self, xt, yt, scale_trans, **kwargs): 

2686 super().__init__(**kwargs) 

2687 self._t = (xt, yt) 

2688 self._scale_trans = scale_trans 

2689 self.set_children(scale_trans) 

2690 self._mtx = None 

2691 self._inverted = None 

2692 

2693 __str__ = _make_str_method("_t") 

2694 

2695 def get_matrix(self): 

2696 # docstring inherited 

2697 if self._invalid: 

2698 # A bit faster than np.identity(3). 

2699 self._mtx = IdentityTransform._mtx.copy() 

2700 self._mtx[:2, 2] = self._scale_trans.transform(self._t) 

2701 self._invalid = 0 

2702 self._inverted = None 

2703 return self._mtx 

2704 

2705 

2706class AffineDeltaTransform(Affine2DBase): 

2707 r""" 

2708 A transform wrapper for transforming displacements between pairs of points. 

2709 

2710 This class is intended to be used to transform displacements ("position 

2711 deltas") between pairs of points (e.g., as the ``offset_transform`` 

2712 of `.Collection`\s): given a transform ``t`` such that ``t = 

2713 AffineDeltaTransform(t) + offset``, ``AffineDeltaTransform`` 

2714 satisfies ``AffineDeltaTransform(a - b) == AffineDeltaTransform(a) - 

2715 AffineDeltaTransform(b)``. 

2716 

2717 This is implemented by forcing the offset components of the transform 

2718 matrix to zero. 

2719 

2720 This class is experimental as of 3.3, and the API may change. 

2721 """ 

2722 

2723 def __init__(self, transform, **kwargs): 

2724 super().__init__(**kwargs) 

2725 self._base_transform = transform 

2726 

2727 __str__ = _make_str_method("_base_transform") 

2728 

2729 def get_matrix(self): 

2730 if self._invalid: 

2731 self._mtx = self._base_transform.get_matrix().copy() 

2732 self._mtx[:2, -1] = 0 

2733 return self._mtx 

2734 

2735 

2736class TransformedPath(TransformNode): 

2737 """ 

2738 A `TransformedPath` caches a non-affine transformed copy of the 

2739 `~.path.Path`. This cached copy is automatically updated when the 

2740 non-affine part of the transform changes. 

2741 

2742 .. note:: 

2743 

2744 Paths are considered immutable by this class. Any update to the 

2745 path's vertices/codes will not trigger a transform recomputation. 

2746 

2747 """ 

2748 def __init__(self, path, transform): 

2749 """ 

2750 Parameters 

2751 ---------- 

2752 path : `~.path.Path` 

2753 transform : `Transform` 

2754 """ 

2755 _api.check_isinstance(Transform, transform=transform) 

2756 super().__init__() 

2757 self._path = path 

2758 self._transform = transform 

2759 self.set_children(transform) 

2760 self._transformed_path = None 

2761 self._transformed_points = None 

2762 

2763 def _revalidate(self): 

2764 # only recompute if the invalidation includes the non_affine part of 

2765 # the transform 

2766 if (self._invalid == self._INVALID_FULL 

2767 or self._transformed_path is None): 

2768 self._transformed_path = \ 

2769 self._transform.transform_path_non_affine(self._path) 

2770 self._transformed_points = \ 

2771 Path._fast_from_codes_and_verts( 

2772 self._transform.transform_non_affine(self._path.vertices), 

2773 None, self._path) 

2774 self._invalid = 0 

2775 

2776 def get_transformed_points_and_affine(self): 

2777 """ 

2778 Return a copy of the child path, with the non-affine part of 

2779 the transform already applied, along with the affine part of 

2780 the path necessary to complete the transformation. Unlike 

2781 :meth:`get_transformed_path_and_affine`, no interpolation will 

2782 be performed. 

2783 """ 

2784 self._revalidate() 

2785 return self._transformed_points, self.get_affine() 

2786 

2787 def get_transformed_path_and_affine(self): 

2788 """ 

2789 Return a copy of the child path, with the non-affine part of 

2790 the transform already applied, along with the affine part of 

2791 the path necessary to complete the transformation. 

2792 """ 

2793 self._revalidate() 

2794 return self._transformed_path, self.get_affine() 

2795 

2796 def get_fully_transformed_path(self): 

2797 """ 

2798 Return a fully-transformed copy of the child path. 

2799 """ 

2800 self._revalidate() 

2801 return self._transform.transform_path_affine(self._transformed_path) 

2802 

2803 def get_affine(self): 

2804 return self._transform.get_affine() 

2805 

2806 

2807class TransformedPatchPath(TransformedPath): 

2808 """ 

2809 A `TransformedPatchPath` caches a non-affine transformed copy of the 

2810 `~.patches.Patch`. This cached copy is automatically updated when the 

2811 non-affine part of the transform or the patch changes. 

2812 """ 

2813 

2814 def __init__(self, patch): 

2815 """ 

2816 Parameters 

2817 ---------- 

2818 patch : `~.patches.Patch` 

2819 """ 

2820 # Defer to TransformedPath.__init__. 

2821 super().__init__(patch.get_path(), patch.get_transform()) 

2822 self._patch = patch 

2823 

2824 def _revalidate(self): 

2825 patch_path = self._patch.get_path() 

2826 # Force invalidation if the patch path changed; otherwise, let base 

2827 # class check invalidation. 

2828 if patch_path != self._path: 

2829 self._path = patch_path 

2830 self._transformed_path = None 

2831 super()._revalidate() 

2832 

2833 

2834def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): 

2835 """ 

2836 Modify the endpoints of a range as needed to avoid singularities. 

2837 

2838 Parameters 

2839 ---------- 

2840 vmin, vmax : float 

2841 The initial endpoints. 

2842 expander : float, default: 0.001 

2843 Fractional amount by which *vmin* and *vmax* are expanded if 

2844 the original interval is too small, based on *tiny*. 

2845 tiny : float, default: 1e-15 

2846 Threshold for the ratio of the interval to the maximum absolute 

2847 value of its endpoints. If the interval is smaller than 

2848 this, it will be expanded. This value should be around 

2849 1e-15 or larger; otherwise the interval will be approaching 

2850 the double precision resolution limit. 

2851 increasing : bool, default: True 

2852 If True, swap *vmin*, *vmax* if *vmin* > *vmax*. 

2853 

2854 Returns 

2855 ------- 

2856 vmin, vmax : float 

2857 Endpoints, expanded and/or swapped if necessary. 

2858 If either input is inf or NaN, or if both inputs are 0 or very 

2859 close to zero, it returns -*expander*, *expander*. 

2860 """ 

2861 

2862 if (not np.isfinite(vmin)) or (not np.isfinite(vmax)): 

2863 return -expander, expander 

2864 

2865 swapped = False 

2866 if vmax < vmin: 

2867 vmin, vmax = vmax, vmin 

2868 swapped = True 

2869 

2870 # Expand vmin, vmax to float: if they were integer types, they can wrap 

2871 # around in abs (abs(np.int8(-128)) == -128) and vmax - vmin can overflow. 

2872 vmin, vmax = map(float, [vmin, vmax]) 

2873 

2874 maxabsvalue = max(abs(vmin), abs(vmax)) 

2875 if maxabsvalue < (1e6 / tiny) * np.finfo(float).tiny: 

2876 vmin = -expander 

2877 vmax = expander 

2878 

2879 elif vmax - vmin <= maxabsvalue * tiny: 

2880 if vmax == 0 and vmin == 0: 

2881 vmin = -expander 

2882 vmax = expander 

2883 else: 

2884 vmin -= expander*abs(vmin) 

2885 vmax += expander*abs(vmax) 

2886 

2887 if swapped and not increasing: 

2888 vmin, vmax = vmax, vmin 

2889 return vmin, vmax 

2890 

2891 

2892def interval_contains(interval, val): 

2893 """ 

2894 Check, inclusively, whether an interval includes a given value. 

2895 

2896 Parameters 

2897 ---------- 

2898 interval : (float, float) 

2899 The endpoints of the interval. 

2900 val : float 

2901 Value to check is within interval. 

2902 

2903 Returns 

2904 ------- 

2905 bool 

2906 Whether *val* is within the *interval*. 

2907 """ 

2908 a, b = interval 

2909 if a > b: 

2910 a, b = b, a 

2911 return a <= val <= b 

2912 

2913 

2914def _interval_contains_close(interval, val, rtol=1e-10): 

2915 """ 

2916 Check, inclusively, whether an interval includes a given value, with the 

2917 interval expanded by a small tolerance to admit floating point errors. 

2918 

2919 Parameters 

2920 ---------- 

2921 interval : (float, float) 

2922 The endpoints of the interval. 

2923 val : float 

2924 Value to check is within interval. 

2925 rtol : float, default: 1e-10 

2926 Relative tolerance slippage allowed outside of the interval. 

2927 For an interval ``[a, b]``, values 

2928 ``a - rtol * (b - a) <= val <= b + rtol * (b - a)`` are considered 

2929 inside the interval. 

2930 

2931 Returns 

2932 ------- 

2933 bool 

2934 Whether *val* is within the *interval* (with tolerance). 

2935 """ 

2936 a, b = interval 

2937 if a > b: 

2938 a, b = b, a 

2939 rtol = (b - a) * rtol 

2940 return a - rtol <= val <= b + rtol 

2941 

2942 

2943def interval_contains_open(interval, val): 

2944 """ 

2945 Check, excluding endpoints, whether an interval includes a given value. 

2946 

2947 Parameters 

2948 ---------- 

2949 interval : (float, float) 

2950 The endpoints of the interval. 

2951 val : float 

2952 Value to check is within interval. 

2953 

2954 Returns 

2955 ------- 

2956 bool 

2957 Whether *val* is within the *interval*. 

2958 """ 

2959 a, b = interval 

2960 return a < val < b or a > val > b 

2961 

2962 

2963def offset_copy(trans, fig=None, x=0.0, y=0.0, units='inches'): 

2964 """ 

2965 Return a new transform with an added offset. 

2966 

2967 Parameters 

2968 ---------- 

2969 trans : `Transform` subclass 

2970 Any transform, to which offset will be applied. 

2971 fig : `~matplotlib.figure.Figure`, default: None 

2972 Current figure. It can be None if *units* are 'dots'. 

2973 x, y : float, default: 0.0 

2974 The offset to apply. 

2975 units : {'inches', 'points', 'dots'}, default: 'inches' 

2976 Units of the offset. 

2977 

2978 Returns 

2979 ------- 

2980 `Transform` subclass 

2981 Transform with applied offset. 

2982 """ 

2983 _api.check_in_list(['dots', 'points', 'inches'], units=units) 

2984 if units == 'dots': 

2985 return trans + Affine2D().translate(x, y) 

2986 if fig is None: 

2987 raise ValueError('For units of inches or points a fig kwarg is needed') 

2988 if units == 'points': 

2989 x /= 72.0 

2990 y /= 72.0 

2991 # Default units are 'inches' 

2992 return trans + ScaledTranslation(x, y, fig.dpi_scale_trans)