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 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""" 31 32# Note: There are a number of places in the code where we use `np.min` or 33# `np.minimum` instead of the builtin `min`, and likewise for `max`. This is 34# done so that `nan`s are propagated, instead of being silently dropped. 35 36import copy 37import functools 38import textwrap 39import weakref 40import math 41 42import numpy as np 43from numpy.linalg import inv 44 45from matplotlib import _api 46from matplotlib._path import ( 47 affine_transform, count_bboxes_overlapping_bbox, update_path_extents) 48from .path import Path 49 50DEBUG = False 51 52 53def _make_str_method(*args, **kwargs): 54 """ 55 Generate a ``__str__`` method for a `.Transform` subclass. 56 57 After :: 58 59 class T: 60 __str__ = _make_str_method("attr", key="other") 61 62 ``str(T(...))`` will be 63 64 .. code-block:: text 65 66 {type(T).__name__}( 67 {self.attr}, 68 key={self.other}) 69 """ 70 indent = functools.partial(textwrap.indent, prefix=" " * 4) 71 def strrepr(x): return repr(x) if isinstance(x, str) else str(x) 72 return lambda self: ( 73 type(self).__name__ + "(" 74 + ",".join([*(indent("\n" + strrepr(getattr(self, arg))) 75 for arg in args), 76 *(indent("\n" + k + "=" + strrepr(getattr(self, arg))) 77 for k, arg in kwargs.items())]) 78 + ")") 79 80 81class TransformNode: 82 """ 83 The base class for anything that participates in the transform tree 84 and needs to invalidate its parents or be invalidated. This includes 85 classes that are not really transforms, such as bounding boxes, since some 86 transforms depend on bounding boxes to compute their values. 87 """ 88 89 # Invalidation may affect only the affine part. If the 90 # invalidation was "affine-only", the _invalid member is set to 91 # INVALID_AFFINE_ONLY 92 INVALID_NON_AFFINE = 1 93 INVALID_AFFINE = 2 94 INVALID = INVALID_NON_AFFINE | INVALID_AFFINE 95 96 # Some metadata about the transform, used to determine whether an 97 # invalidation is affine-only 98 is_affine = False 99 is_bbox = False 100 101 pass_through = False 102 """ 103 If pass_through is True, all ancestors will always be 104 invalidated, even if 'self' is already invalid. 105 """ 106 107 def __init__(self, shorthand_name=None): 108 """ 109 Parameters 110 ---------- 111 shorthand_name : str 112 A string representing the "name" of the transform. The name carries 113 no significance other than to improve the readability of 114 ``str(transform)`` when DEBUG=True. 115 """ 116 self._parents = {} 117 118 # TransformNodes start out as invalid until their values are 119 # computed for the first time. 120 self._invalid = 1 121 self._shorthand_name = shorthand_name or '' 122 123 if DEBUG: 124 def __str__(self): 125 # either just return the name of this TransformNode, or its repr 126 return self._shorthand_name or repr(self) 127 128 def __getstate__(self): 129 # turn the dictionary with weak values into a normal dictionary 130 return {**self.__dict__, 131 '_parents': {k: v() for k, v in self._parents.items()}} 132 133 def __setstate__(self, data_dict): 134 self.__dict__ = data_dict 135 # turn the normal dictionary back into a dictionary with weak values 136 # The extra lambda is to provide a callback to remove dead 137 # weakrefs from the dictionary when garbage collection is done. 138 self._parents = { 139 k: weakref.ref(v, lambda _, pop=self._parents.pop, k=k: pop(k)) 140 for k, v in self._parents.items() if v is not None} 141 142 def __copy__(self): 143 other = copy.copy(super()) 144 # If `c = a + b; a1 = copy(a)`, then modifications to `a1` do not 145 # propagate back to `c`, i.e. we need to clear the parents of `a1`. 146 other._parents = {} 147 # If `c = a + b; c1 = copy(c)`, then modifications to `a` also need to 148 # be propagated to `c1`. 149 for key, val in vars(self).items(): 150 if isinstance(val, TransformNode) and id(self) in val._parents: 151 other.set_children(val) # val == getattr(other, key) 152 return other 153 154 def __deepcopy__(self, memo): 155 # We could deepcopy the entire transform tree, but nothing except 156 # `self` is accessible publicly, so we may as well just freeze `self`. 157 other = self.frozen() 158 if other is not self: 159 return other 160 # Some classes implement frozen() as returning self, which is not 161 # acceptable for deepcopying, so we need to handle them separately. 162 other = copy.deepcopy(super(), memo) 163 # If `c = a + b; a1 = copy(a)`, then modifications to `a1` do not 164 # propagate back to `c`, i.e. we need to clear the parents of `a1`. 165 other._parents = {} 166 # If `c = a + b; c1 = copy(c)`, this creates a separate tree 167 # (`c1 = a1 + b1`) so nothing needs to be done. 168 return other 169 170 def invalidate(self): 171 """ 172 Invalidate this `TransformNode` and triggers an invalidation of its 173 ancestors. Should be called any time the transform changes. 174 """ 175 value = self.INVALID 176 if self.is_affine: 177 value = self.INVALID_AFFINE 178 return self._invalidate_internal(value, invalidating_node=self) 179 180 def _invalidate_internal(self, value, invalidating_node): 181 """ 182 Called by :meth:`invalidate` and subsequently ascends the transform 183 stack calling each TransformNode's _invalidate_internal method. 184 """ 185 # determine if this call will be an extension to the invalidation 186 # status. If not, then a shortcut means that we needn't invoke an 187 # invalidation up the transform stack as it will already have been 188 # invalidated. 189 190 # N.B This makes the invalidation sticky, once a transform has been 191 # invalidated as NON_AFFINE, then it will always be invalidated as 192 # NON_AFFINE even when triggered with a AFFINE_ONLY invalidation. 193 # In most cases this is not a problem (i.e. for interactive panning and 194 # zooming) and the only side effect will be on performance. 195 status_changed = self._invalid < value 196 197 if self.pass_through or status_changed: 198 self._invalid = value 199 200 for parent in list(self._parents.values()): 201 # Dereference the weak reference 202 parent = parent() 203 if parent is not None: 204 parent._invalidate_internal( 205 value=value, invalidating_node=self) 206 207 def set_children(self, *children): 208 """ 209 Set the children of the transform, to let the invalidation 210 system know which transforms can invalidate this transform. 211 Should be called from the constructor of any transforms that 212 depend on other transforms. 213 """ 214 # Parents are stored as weak references, so that if the 215 # parents are destroyed, references from the children won't 216 # keep them alive. 217 for child in children: 218 # Use weak references so this dictionary won't keep obsolete nodes 219 # alive; the callback deletes the dictionary entry. This is a 220 # performance improvement over using WeakValueDictionary. 221 ref = weakref.ref( 222 self, lambda _, pop=child._parents.pop, k=id(self): pop(k)) 223 child._parents[id(self)] = ref 224 225 def frozen(self): 226 """ 227 Return a frozen copy of this transform node. The frozen copy will not 228 be updated when its children change. Useful for storing a previously 229 known state of a transform where ``copy.deepcopy()`` might normally be 230 used. 231 """ 232 return self 233 234 235class BboxBase(TransformNode): 236 """ 237 The base class of all bounding boxes. 238 239 This class is immutable; `Bbox` is a mutable subclass. 240 241 The canonical representation is as two points, with no 242 restrictions on their ordering. Convenience properties are 243 provided to get the left, bottom, right and top edges and width 244 and height, but these are not stored explicitly. 245 """ 246 247 is_bbox = True 248 is_affine = True 249 250 if DEBUG: 251 @staticmethod 252 def _check(points): 253 if isinstance(points, np.ma.MaskedArray): 254 _api.warn_external("Bbox bounds are a masked array.") 255 points = np.asarray(points) 256 if any((points[1, :] - points[0, :]) == 0): 257 _api.warn_external("Singular Bbox.") 258 259 def frozen(self): 260 return Bbox(self.get_points().copy()) 261 frozen.__doc__ = TransformNode.__doc__ 262 263 def __array__(self, *args, **kwargs): 264 return self.get_points() 265 266 @property 267 def x0(self): 268 """ 269 The first of the pair of *x* coordinates that define the bounding box. 270 271 This is not guaranteed to be less than :attr:`x1` (for that, use 272 :attr:`xmin`). 273 """ 274 return self.get_points()[0, 0] 275 276 @property 277 def y0(self): 278 """ 279 The first of the pair of *y* coordinates that define the bounding box. 280 281 This is not guaranteed to be less than :attr:`y1` (for that, use 282 :attr:`ymin`). 283 """ 284 return self.get_points()[0, 1] 285 286 @property 287 def x1(self): 288 """ 289 The second of the pair of *x* coordinates that define the bounding box. 290 291 This is not guaranteed to be greater than :attr:`x0` (for that, use 292 :attr:`xmax`). 293 """ 294 return self.get_points()[1, 0] 295 296 @property 297 def y1(self): 298 """ 299 The second of the pair of *y* coordinates that define the bounding box. 300 301 This is not guaranteed to be greater than :attr:`y0` (for that, use 302 :attr:`ymax`). 303 """ 304 return self.get_points()[1, 1] 305 306 @property 307 def p0(self): 308 """ 309 The first pair of (*x*, *y*) coordinates that define the bounding box. 310 311 This is not guaranteed to be the bottom-left corner (for that, use 312 :attr:`min`). 313 """ 314 return self.get_points()[0] 315 316 @property 317 def p1(self): 318 """ 319 The second pair of (*x*, *y*) coordinates that define the bounding box. 320 321 This is not guaranteed to be the top-right corner (for that, use 322 :attr:`max`). 323 """ 324 return self.get_points()[1] 325 326 @property 327 def xmin(self): 328 """The left edge of the bounding box.""" 329 return np.min(self.get_points()[:, 0]) 330 331 @property 332 def ymin(self): 333 """The bottom edge of the bounding box.""" 334 return np.min(self.get_points()[:, 1]) 335 336 @property 337 def xmax(self): 338 """The right edge of the bounding box.""" 339 return np.max(self.get_points()[:, 0]) 340 341 @property 342 def ymax(self): 343 """The top edge of the bounding box.""" 344 return np.max(self.get_points()[:, 1]) 345 346 @property 347 def min(self): 348 """The bottom-left corner of the bounding box.""" 349 return np.min(self.get_points(), axis=0) 350 351 @property 352 def max(self): 353 """The top-right corner of the bounding box.""" 354 return np.max(self.get_points(), axis=0) 355 356 @property 357 def intervalx(self): 358 """ 359 The pair of *x* coordinates that define the bounding box. 360 361 This is not guaranteed to be sorted from left to right. 362 """ 363 return self.get_points()[:, 0] 364 365 @property 366 def intervaly(self): 367 """ 368 The pair of *y* coordinates that define the bounding box. 369 370 This is not guaranteed to be sorted from bottom to top. 371 """ 372 return self.get_points()[:, 1] 373 374 @property 375 def width(self): 376 """The (signed) width of the bounding box.""" 377 points = self.get_points() 378 return points[1, 0] - points[0, 0] 379 380 @property 381 def height(self): 382 """The (signed) height of the bounding box.""" 383 points = self.get_points() 384 return points[1, 1] - points[0, 1] 385 386 @property 387 def size(self): 388 """The (signed) width and height of the bounding box.""" 389 points = self.get_points() 390 return points[1] - points[0] 391 392 @property 393 def bounds(self): 394 """Return (:attr:`x0`, :attr:`y0`, :attr:`width`, :attr:`height`).""" 395 (x0, y0), (x1, y1) = self.get_points() 396 return (x0, y0, x1 - x0, y1 - y0) 397 398 @property 399 def extents(self): 400 """Return (:attr:`x0`, :attr:`y0`, :attr:`x1`, :attr:`y1`).""" 401 return self.get_points().flatten() # flatten returns a copy. 402 403 def get_points(self): 404 raise NotImplementedError 405 406 def containsx(self, x): 407 """ 408 Return whether *x* is in the closed (:attr:`x0`, :attr:`x1`) interval. 409 """ 410 x0, x1 = self.intervalx 411 return x0 <= x <= x1 or x0 >= x >= x1 412 413 def containsy(self, y): 414 """ 415 Return whether *y* is in the closed (:attr:`y0`, :attr:`y1`) interval. 416 """ 417 y0, y1 = self.intervaly 418 return y0 <= y <= y1 or y0 >= y >= y1 419 420 def contains(self, x, y): 421 """ 422 Return whether ``(x, y)`` is in the bounding box or on its edge. 423 """ 424 return self.containsx(x) and self.containsy(y) 425 426 def overlaps(self, other): 427 """ 428 Return whether this bounding box overlaps with the other bounding box. 429 430 Parameters 431 ---------- 432 other : `.BboxBase` 433 """ 434 ax1, ay1, ax2, ay2 = self.extents 435 bx1, by1, bx2, by2 = other.extents 436 if ax2 < ax1: 437 ax2, ax1 = ax1, ax2 438 if ay2 < ay1: 439 ay2, ay1 = ay1, ay2 440 if bx2 < bx1: 441 bx2, bx1 = bx1, bx2 442 if by2 < by1: 443 by2, by1 = by1, by2 444 return ax1 <= bx2 and bx1 <= ax2 and ay1 <= by2 and by1 <= ay2 445 446 def fully_containsx(self, x): 447 """ 448 Return whether *x* is in the open (:attr:`x0`, :attr:`x1`) interval. 449 """ 450 x0, x1 = self.intervalx 451 return x0 < x < x1 or x0 > x > x1 452 453 def fully_containsy(self, y): 454 """ 455 Return whether *y* is in the open (:attr:`y0`, :attr:`y1`) interval. 456 """ 457 y0, y1 = self.intervaly 458 return y0 < y < y1 or y0 > y > y1 459 460 def fully_contains(self, x, y): 461 """ 462 Return whether ``x, y`` is in the bounding box, but not on its edge. 463 """ 464 return self.fully_containsx(x) and self.fully_containsy(y) 465 466 def fully_overlaps(self, other): 467 """ 468 Return whether this bounding box overlaps with the other bounding box, 469 not including the edges. 470 471 Parameters 472 ---------- 473 other : `.BboxBase` 474 """ 475 ax1, ay1, ax2, ay2 = self.extents 476 bx1, by1, bx2, by2 = other.extents 477 if ax2 < ax1: 478 ax2, ax1 = ax1, ax2 479 if ay2 < ay1: 480 ay2, ay1 = ay1, ay2 481 if bx2 < bx1: 482 bx2, bx1 = bx1, bx2 483 if by2 < by1: 484 by2, by1 = by1, by2 485 return ax1 < bx2 and bx1 < ax2 and ay1 < by2 and by1 < ay2 486 487 def transformed(self, transform): 488 """ 489 Construct a `Bbox` by statically transforming this one by *transform*. 490 """ 491 pts = self.get_points() 492 ll, ul, lr = transform.transform(np.array( 493 [pts[0], [pts[0, 0], pts[1, 1]], [pts[1, 0], pts[0, 1]]])) 494 return Bbox([ll, [lr[0], ul[1]]]) 495 496 @_api.deprecated("3.3", alternative="transformed(transform.inverted())") 497 def inverse_transformed(self, transform): 498 """ 499 Construct a `Bbox` by statically transforming this one by the inverse 500 of *transform*. 501 """ 502 return self.transformed(transform.inverted()) 503 504 coefs = {'C': (0.5, 0.5), 505 'SW': (0, 0), 506 'S': (0.5, 0), 507 'SE': (1.0, 0), 508 'E': (1.0, 0.5), 509 'NE': (1.0, 1.0), 510 'N': (0.5, 1.0), 511 'NW': (0, 1.0), 512 'W': (0, 0.5)} 513 514 def anchored(self, c, container=None): 515 """ 516 Return a copy of the `Bbox` shifted to position *c* within *container*. 517 518 Parameters 519 ---------- 520 c : (float, float) or str 521 May be either: 522 523 * A sequence (*cx*, *cy*) where *cx* and *cy* range from 0 524 to 1, where 0 is left or bottom and 1 is right or top 525 526 * a string: 527 - 'C' for centered 528 - 'S' for bottom-center 529 - 'SE' for bottom-left 530 - 'E' for left 531 - etc. 532 533 container : `Bbox`, optional 534 The box within which the `Bbox` is positioned; it defaults 535 to the initial `Bbox`. 536 """ 537 if container is None: 538 container = self 539 l, b, w, h = container.bounds 540 if isinstance(c, str): 541 cx, cy = self.coefs[c] 542 else: 543 cx, cy = c 544 L, B, W, H = self.bounds 545 return Bbox(self._points + 546 [(l + cx * (w - W)) - L, 547 (b + cy * (h - H)) - B]) 548 549 def shrunk(self, mx, my): 550 """ 551 Return a copy of the `Bbox`, shrunk by the factor *mx* 552 in the *x* direction and the factor *my* in the *y* direction. 553 The lower left corner of the box remains unchanged. Normally 554 *mx* and *my* will be less than 1, but this is not enforced. 555 """ 556 w, h = self.size 557 return Bbox([self._points[0], 558 self._points[0] + [mx * w, my * h]]) 559 560 def shrunk_to_aspect(self, box_aspect, container=None, fig_aspect=1.0): 561 """ 562 Return a copy of the `Bbox`, shrunk so that it is as 563 large as it can be while having the desired aspect ratio, 564 *box_aspect*. If the box coordinates are relative (i.e. 565 fractions of a larger box such as a figure) then the 566 physical aspect ratio of that figure is specified with 567 *fig_aspect*, so that *box_aspect* can also be given as a 568 ratio of the absolute dimensions, not the relative dimensions. 569 """ 570 if box_aspect <= 0 or fig_aspect <= 0: 571 raise ValueError("'box_aspect' and 'fig_aspect' must be positive") 572 if container is None: 573 container = self 574 w, h = container.size 575 H = w * box_aspect / fig_aspect 576 if H <= h: 577 W = w 578 else: 579 W = h * fig_aspect / box_aspect 580 H = h 581 return Bbox([self._points[0], 582 self._points[0] + (W, H)]) 583 584 def splitx(self, *args): 585 """ 586 Return a list of new `Bbox` objects formed by splitting the original 587 one with vertical lines at fractional positions given by *args*. 588 """ 589 xf = [0, *args, 1] 590 x0, y0, x1, y1 = self.extents 591 w = x1 - x0 592 return [Bbox([[x0 + xf0 * w, y0], [x0 + xf1 * w, y1]]) 593 for xf0, xf1 in zip(xf[:-1], xf[1:])] 594 595 def splity(self, *args): 596 """ 597 Return a list of new `Bbox` objects formed by splitting the original 598 one with horizontal lines at fractional positions given by *args*. 599 """ 600 yf = [0, *args, 1] 601 x0, y0, x1, y1 = self.extents 602 h = y1 - y0 603 return [Bbox([[x0, y0 + yf0 * h], [x1, y0 + yf1 * h]]) 604 for yf0, yf1 in zip(yf[:-1], yf[1:])] 605 606 def count_contains(self, vertices): 607 """ 608 Count the number of vertices contained in the `Bbox`. 609 Any vertices with a non-finite x or y value are ignored. 610 611 Parameters 612 ---------- 613 vertices : Nx2 Numpy array. 614 """ 615 if len(vertices) == 0: 616 return 0 617 vertices = np.asarray(vertices) 618 with np.errstate(invalid='ignore'): 619 return (((self.min < vertices) & 620 (vertices < self.max)).all(axis=1).sum()) 621 622 def count_overlaps(self, bboxes): 623 """ 624 Count the number of bounding boxes that overlap this one. 625 626 Parameters 627 ---------- 628 bboxes : sequence of `.BboxBase` 629 """ 630 return count_bboxes_overlapping_bbox( 631 self, np.atleast_3d([np.array(x) for x in bboxes])) 632 633 def expanded(self, sw, sh): 634 """ 635 Construct a `Bbox` by expanding this one around its center by the 636 factors *sw* and *sh*. 637 """ 638 width = self.width 639 height = self.height 640 deltaw = (sw * width - width) / 2.0 641 deltah = (sh * height - height) / 2.0 642 a = np.array([[-deltaw, -deltah], [deltaw, deltah]]) 643 return Bbox(self._points + a) 644 645 def padded(self, p): 646 """Construct a `Bbox` by padding this one on all four sides by *p*.""" 647 points = self.get_points() 648 return Bbox(points + [[-p, -p], [p, p]]) 649 650 def translated(self, tx, ty): 651 """Construct a `Bbox` by translating this one by *tx* and *ty*.""" 652 return Bbox(self._points + (tx, ty)) 653 654 def corners(self): 655 """ 656 Return the corners of this rectangle as an array of points. 657 658 Specifically, this returns the array 659 ``[[x0, y0], [x0, y1], [x1, y0], [x1, y1]]``. 660 """ 661 (x0, y0), (x1, y1) = self.get_points() 662 return np.array([[x0, y0], [x0, y1], [x1, y0], [x1, y1]]) 663 664 def rotated(self, radians): 665 """ 666 Return the axes-aligned bounding box that bounds the result of rotating 667 this `Bbox` by an angle of *radians*. 668 """ 669 corners = self.corners() 670 corners_rotated = Affine2D().rotate(radians).transform(corners) 671 bbox = Bbox.unit() 672 bbox.update_from_data_xy(corners_rotated, ignore=True) 673 return bbox 674 675 @staticmethod 676 def union(bboxes): 677 """Return a `Bbox` that contains all of the given *bboxes*.""" 678 if not len(bboxes): 679 raise ValueError("'bboxes' cannot be empty") 680 x0 = np.min([bbox.xmin for bbox in bboxes]) 681 x1 = np.max([bbox.xmax for bbox in bboxes]) 682 y0 = np.min([bbox.ymin for bbox in bboxes]) 683 y1 = np.max([bbox.ymax for bbox in bboxes]) 684 return Bbox([[x0, y0], [x1, y1]]) 685 686 @staticmethod 687 def intersection(bbox1, bbox2): 688 """ 689 Return the intersection of *bbox1* and *bbox2* if they intersect, or 690 None if they don't. 691 """ 692 x0 = np.maximum(bbox1.xmin, bbox2.xmin) 693 x1 = np.minimum(bbox1.xmax, bbox2.xmax) 694 y0 = np.maximum(bbox1.ymin, bbox2.ymin) 695 y1 = np.minimum(bbox1.ymax, bbox2.ymax) 696 return Bbox([[x0, y0], [x1, y1]]) if x0 <= x1 and y0 <= y1 else None 697 698 699class Bbox(BboxBase): 700 """ 701 A mutable bounding box. 702 703 Examples 704 -------- 705 **Create from known bounds** 706 707 The default constructor takes the boundary "points" ``[[xmin, ymin], 708 [xmax, ymax]]``. 709 710 >>> Bbox([[1, 1], [3, 7]]) 711 Bbox([[1.0, 1.0], [3.0, 7.0]]) 712 713 Alternatively, a Bbox can be created from the flattened points array, the 714 so-called "extents" ``(xmin, ymin, xmax, ymax)`` 715 716 >>> Bbox.from_extents(1, 1, 3, 7) 717 Bbox([[1.0, 1.0], [3.0, 7.0]]) 718 719 or from the "bounds" ``(xmin, ymin, width, height)``. 720 721 >>> Bbox.from_bounds(1, 1, 2, 6) 722 Bbox([[1.0, 1.0], [3.0, 7.0]]) 723 724 **Create from collections of points** 725 726 The "empty" object for accumulating Bboxs is the null bbox, which is a 727 stand-in for the empty set. 728 729 >>> Bbox.null() 730 Bbox([[inf, inf], [-inf, -inf]]) 731 732 Adding points to the null bbox will give you the bbox of those points. 733 734 >>> box = Bbox.null() 735 >>> box.update_from_data_xy([[1, 1]]) 736 >>> box 737 Bbox([[1.0, 1.0], [1.0, 1.0]]) 738 >>> box.update_from_data_xy([[2, 3], [3, 2]], ignore=False) 739 >>> box 740 Bbox([[1.0, 1.0], [3.0, 3.0]]) 741 742 Setting ``ignore=True`` is equivalent to starting over from a null bbox. 743 744 >>> box.update_from_data_xy([[1, 1]], ignore=True) 745 >>> box 746 Bbox([[1.0, 1.0], [1.0, 1.0]]) 747 748 .. warning:: 749 750 It is recommended to always specify ``ignore`` explicitly. If not, the 751 default value of ``ignore`` can be changed at any time by code with 752 access to your Bbox, for example using the method `~.Bbox.ignore`. 753 754 **Properties of the ``null`` bbox** 755 756 .. note:: 757 758 The current behavior of `Bbox.null()` may be surprising as it does 759 not have all of the properties of the "empty set", and as such does 760 not behave like a "zero" object in the mathematical sense. We may 761 change that in the future (with a deprecation period). 762 763 The null bbox is the identity for intersections 764 765 >>> Bbox.intersection(Bbox([[1, 1], [3, 7]]), Bbox.null()) 766 Bbox([[1.0, 1.0], [3.0, 7.0]]) 767 768 except with itself, where it returns the full space. 769 770 >>> Bbox.intersection(Bbox.null(), Bbox.null()) 771 Bbox([[-inf, -inf], [inf, inf]]) 772 773 A union containing null will always return the full space (not the other 774 set!) 775 776 >>> Bbox.union([Bbox([[0, 0], [0, 0]]), Bbox.null()]) 777 Bbox([[-inf, -inf], [inf, inf]]) 778 """ 779 780 def __init__(self, points, **kwargs): 781 """ 782 Parameters 783 ---------- 784 points : ndarray 785 A 2x2 numpy array of the form ``[[x0, y0], [x1, y1]]``. 786 """ 787 super().__init__(**kwargs) 788 points = np.asarray(points, float) 789 if points.shape != (2, 2): 790 raise ValueError('Bbox points must be of the form ' 791 '"[[x0, y0], [x1, y1]]".') 792 self._points = points 793 self._minpos = np.array([np.inf, np.inf]) 794 self._ignore = True 795 # it is helpful in some contexts to know if the bbox is a 796 # default or has been mutated; we store the orig points to 797 # support the mutated methods 798 self._points_orig = self._points.copy() 799 if DEBUG: 800 ___init__ = __init__ 801 802 def __init__(self, points, **kwargs): 803 self._check(points) 804 self.___init__(points, **kwargs) 805 806 def invalidate(self): 807 self._check(self._points) 808 super().invalidate() 809 810 @staticmethod 811 def unit(): 812 """Create a new unit `Bbox` from (0, 0) to (1, 1).""" 813 return Bbox([[0, 0], [1, 1]]) 814 815 @staticmethod 816 def null(): 817 """Create a new null `Bbox` from (inf, inf) to (-inf, -inf).""" 818 return Bbox([[np.inf, np.inf], [-np.inf, -np.inf]]) 819 820 @staticmethod 821 def from_bounds(x0, y0, width, height): 822 """ 823 Create a new `Bbox` from *x0*, *y0*, *width* and *height*. 824 825 *width* and *height* may be negative. 826 """ 827 return Bbox.from_extents(x0, y0, x0 + width, y0 + height) 828 829 @staticmethod 830 def from_extents(*args, minpos=None): 831 """ 832 Create a new Bbox from *left*, *bottom*, *right* and *top*. 833 834 The *y*-axis increases upwards. 835 836 Parameters 837 ---------- 838 left, bottom, right, top : float 839 The four extents of the bounding box. 840 841 minpos : float or None 842 If this is supplied, the Bbox will have a minimum positive value 843 set. This is useful when dealing with logarithmic scales and other 844 scales where negative bounds result in floating point errors. 845 """ 846 bbox = Bbox(np.reshape(args, (2, 2))) 847 if minpos is not None: 848 bbox._minpos[:] = minpos 849 return bbox 850 851 def __format__(self, fmt): 852 return ( 853 'Bbox(x0={0.x0:{1}}, y0={0.y0:{1}}, x1={0.x1:{1}}, y1={0.y1:{1}})'. 854 format(self, fmt)) 855 856 def __str__(self): 857 return format(self, '') 858 859 def __repr__(self): 860 return 'Bbox([[{0.x0}, {0.y0}], [{0.x1}, {0.y1}]])'.format(self) 861 862 def ignore(self, value): 863 """ 864 Set whether the existing bounds of the box should be ignored 865 by subsequent calls to :meth:`update_from_data_xy`. 866 867 value : bool 868 - When ``True``, subsequent calls to :meth:`update_from_data_xy` 869 will ignore the existing bounds of the `Bbox`. 870 871 - When ``False``, subsequent calls to :meth:`update_from_data_xy` 872 will include the existing bounds of the `Bbox`. 873 """ 874 self._ignore = value 875 876 def update_from_path(self, path, ignore=None, updatex=True, updatey=True): 877 """ 878 Update the bounds of the `Bbox` to contain the vertices of the 879 provided path. After updating, the bounds will have positive *width* 880 and *height*; *x0* and *y0* will be the minimal values. 881 882 Parameters 883 ---------- 884 path : `~matplotlib.path.Path` 885 886 ignore : bool, optional 887 - when ``True``, ignore the existing bounds of the `Bbox`. 888 - when ``False``, include the existing bounds of the `Bbox`. 889 - when ``None``, use the last value passed to :meth:`ignore`. 890 891 updatex, updatey : bool, default: True 892 When ``True``, update the x/y values. 893 """ 894 if ignore is None: 895 ignore = self._ignore 896 897 if path.vertices.size == 0: 898 return 899 900 points, minpos, changed = update_path_extents( 901 path, None, self._points, self._minpos, ignore) 902 903 if changed: 904 self.invalidate() 905 if updatex: 906 self._points[:, 0] = points[:, 0] 907 self._minpos[0] = minpos[0] 908 if updatey: 909 self._points[:, 1] = points[:, 1] 910 self._minpos[1] = minpos[1] 911 912 def update_from_data_xy(self, xy, ignore=None, updatex=True, updatey=True): 913 """ 914 Update the bounds of the `Bbox` based on the passed in 915 data. After updating, the bounds will have positive *width* 916 and *height*; *x0* and *y0* will be the minimal values. 917 918 Parameters 919 ---------- 920 xy : ndarray 921 A numpy array of 2D points. 922 923 ignore : bool, optional 924 - When ``True``, ignore the existing bounds of the `Bbox`. 925 - When ``False``, include the existing bounds of the `Bbox`. 926 - When ``None``, use the last value passed to :meth:`ignore`. 927 928 updatex, updatey : bool, default: True 929 When ``True``, update the x/y values. 930 """ 931 if len(xy) == 0: 932 return 933 934 path = Path(xy) 935 self.update_from_path(path, ignore=ignore, 936 updatex=updatex, updatey=updatey) 937 938 @BboxBase.x0.setter 939 def x0(self, val): 940 self._points[0, 0] = val 941 self.invalidate() 942 943 @BboxBase.y0.setter 944 def y0(self, val): 945 self._points[0, 1] = val 946 self.invalidate() 947 948 @BboxBase.x1.setter 949 def x1(self, val): 950 self._points[1, 0] = val 951 self.invalidate() 952 953 @BboxBase.y1.setter 954 def y1(self, val): 955 self._points[1, 1] = val 956 self.invalidate() 957 958 @BboxBase.p0.setter 959 def p0(self, val): 960 self._points[0] = val 961 self.invalidate() 962 963 @BboxBase.p1.setter 964 def p1(self, val): 965 self._points[1] = val 966 self.invalidate() 967 968 @BboxBase.intervalx.setter 969 def intervalx(self, interval): 970 self._points[:, 0] = interval 971 self.invalidate() 972 973 @BboxBase.intervaly.setter 974 def intervaly(self, interval): 975 self._points[:, 1] = interval 976 self.invalidate() 977 978 @BboxBase.bounds.setter 979 def bounds(self, bounds): 980 l, b, w, h = bounds 981 points = np.array([[l, b], [l + w, b + h]], float) 982 if np.any(self._points != points): 983 self._points = points 984 self.invalidate() 985 986 @property 987 def minpos(self): 988 """ 989 The minimum positive value in both directions within the Bbox. 990 991 This is useful when dealing with logarithmic scales and other scales 992 where negative bounds result in floating point errors, and will be used 993 as the minimum extent instead of *p0*. 994 """ 995 return self._minpos 996 997 @property 998 def minposx(self): 999 """ 1000 The minimum positive value in the *x*-direction within the Bbox. 1001 1002 This is useful when dealing with logarithmic scales and other scales 1003 where negative bounds result in floating point errors, and will be used 1004 as the minimum *x*-extent instead of *x0*. 1005 """ 1006 return self._minpos[0] 1007 1008 @property 1009 def minposy(self): 1010 """ 1011 The minimum positive value in the *y*-direction within the Bbox. 1012 1013 This is useful when dealing with logarithmic scales and other scales 1014 where negative bounds result in floating point errors, and will be used 1015 as the minimum *y*-extent instead of *y0*. 1016 """ 1017 return self._minpos[1] 1018 1019 def get_points(self): 1020 """ 1021 Get the points of the bounding box directly as a numpy array 1022 of the form: ``[[x0, y0], [x1, y1]]``. 1023 """ 1024 self._invalid = 0 1025 return self._points 1026 1027 def set_points(self, points): 1028 """ 1029 Set the points of the bounding box directly from a numpy array 1030 of the form: ``[[x0, y0], [x1, y1]]``. No error checking is 1031 performed, as this method is mainly for internal use. 1032 """ 1033 if np.any(self._points != points): 1034 self._points = points 1035 self.invalidate() 1036 1037 def set(self, other): 1038 """ 1039 Set this bounding box from the "frozen" bounds of another `Bbox`. 1040 """ 1041 if np.any(self._points != other.get_points()): 1042 self._points = other.get_points() 1043 self.invalidate() 1044 1045 def mutated(self): 1046 """Return whether the bbox has changed since init.""" 1047 return self.mutatedx() or self.mutatedy() 1048 1049 def mutatedx(self): 1050 """Return whether the x-limits have changed since init.""" 1051 return (self._points[0, 0] != self._points_orig[0, 0] or 1052 self._points[1, 0] != self._points_orig[1, 0]) 1053 1054 def mutatedy(self): 1055 """Return whether the y-limits have changed since init.""" 1056 return (self._points[0, 1] != self._points_orig[0, 1] or 1057 self._points[1, 1] != self._points_orig[1, 1]) 1058 1059 1060class TransformedBbox(BboxBase): 1061 """ 1062 A `Bbox` that is automatically transformed by a given 1063 transform. When either the child bounding box or transform 1064 changes, the bounds of this bbox will update accordingly. 1065 """ 1066 1067 def __init__(self, bbox, transform, **kwargs): 1068 """ 1069 Parameters 1070 ---------- 1071 bbox : `Bbox` 1072 transform : `Transform` 1073 """ 1074 if not bbox.is_bbox: 1075 raise ValueError("'bbox' is not a bbox") 1076 _api.check_isinstance(Transform, transform=transform) 1077 if transform.input_dims != 2 or transform.output_dims != 2: 1078 raise ValueError( 1079 "The input and output dimensions of 'transform' must be 2") 1080 1081 super().__init__(**kwargs) 1082 self._bbox = bbox 1083 self._transform = transform 1084 self.set_children(bbox, transform) 1085 self._points = None 1086 1087 __str__ = _make_str_method("_bbox", "_transform") 1088 1089 def get_points(self): 1090 # docstring inherited 1091 if self._invalid: 1092 p = self._bbox.get_points() 1093 # Transform all four points, then make a new bounding box 1094 # from the result, taking care to make the orientation the 1095 # same. 1096 points = self._transform.transform( 1097 [[p[0, 0], p[0, 1]], 1098 [p[1, 0], p[0, 1]], 1099 [p[0, 0], p[1, 1]], 1100 [p[1, 0], p[1, 1]]]) 1101 points = np.ma.filled(points, 0.0) 1102 1103 xs = min(points[:, 0]), max(points[:, 0]) 1104 if p[0, 0] > p[1, 0]: 1105 xs = xs[::-1] 1106 1107 ys = min(points[:, 1]), max(points[:, 1]) 1108 if p[0, 1] > p[1, 1]: 1109 ys = ys[::-1] 1110 1111 self._points = np.array([ 1112 [xs[0], ys[0]], 1113 [xs[1], ys[1]] 1114 ]) 1115 1116 self._invalid = 0 1117 return self._points 1118 1119 if DEBUG: 1120 _get_points = get_points 1121 1122 def get_points(self): 1123 points = self._get_points() 1124 self._check(points) 1125 return points 1126 1127 1128class LockableBbox(BboxBase): 1129 """ 1130 A `Bbox` where some elements may be locked at certain values. 1131 1132 When the child bounding box changes, the bounds of this bbox will update 1133 accordingly with the exception of the locked elements. 1134 """ 1135 def __init__(self, bbox, x0=None, y0=None, x1=None, y1=None, **kwargs): 1136 """ 1137 Parameters 1138 ---------- 1139 bbox : `Bbox` 1140 The child bounding box to wrap. 1141 1142 x0 : float or None 1143 The locked value for x0, or None to leave unlocked. 1144 1145 y0 : float or None 1146 The locked value for y0, or None to leave unlocked. 1147 1148 x1 : float or None 1149 The locked value for x1, or None to leave unlocked. 1150 1151 y1 : float or None 1152 The locked value for y1, or None to leave unlocked. 1153 1154 """ 1155 if not bbox.is_bbox: 1156 raise ValueError("'bbox' is not a bbox") 1157 1158 super().__init__(**kwargs) 1159 self._bbox = bbox 1160 self.set_children(bbox) 1161 self._points = None 1162 fp = [x0, y0, x1, y1] 1163 mask = [val is None for val in fp] 1164 self._locked_points = np.ma.array(fp, float, mask=mask).reshape((2, 2)) 1165 1166 __str__ = _make_str_method("_bbox", "_locked_points") 1167 1168 def get_points(self): 1169 # docstring inherited 1170 if self._invalid: 1171 points = self._bbox.get_points() 1172 self._points = np.where(self._locked_points.mask, 1173 points, 1174 self._locked_points) 1175 self._invalid = 0 1176 return self._points 1177 1178 if DEBUG: 1179 _get_points = get_points 1180 1181 def get_points(self): 1182 points = self._get_points() 1183 self._check(points) 1184 return points 1185 1186 @property 1187 def locked_x0(self): 1188 """ 1189 float or None: The value used for the locked x0. 1190 """ 1191 if self._locked_points.mask[0, 0]: 1192 return None 1193 else: 1194 return self._locked_points[0, 0] 1195 1196 @locked_x0.setter 1197 def locked_x0(self, x0): 1198 self._locked_points.mask[0, 0] = x0 is None 1199 self._locked_points.data[0, 0] = x0 1200 self.invalidate() 1201 1202 @property 1203 def locked_y0(self): 1204 """ 1205 float or None: The value used for the locked y0. 1206 """ 1207 if self._locked_points.mask[0, 1]: 1208 return None 1209 else: 1210 return self._locked_points[0, 1] 1211 1212 @locked_y0.setter 1213 def locked_y0(self, y0): 1214 self._locked_points.mask[0, 1] = y0 is None 1215 self._locked_points.data[0, 1] = y0 1216 self.invalidate() 1217 1218 @property 1219 def locked_x1(self): 1220 """ 1221 float or None: The value used for the locked x1. 1222 """ 1223 if self._locked_points.mask[1, 0]: 1224 return None 1225 else: 1226 return self._locked_points[1, 0] 1227 1228 @locked_x1.setter 1229 def locked_x1(self, x1): 1230 self._locked_points.mask[1, 0] = x1 is None 1231 self._locked_points.data[1, 0] = x1 1232 self.invalidate() 1233 1234 @property 1235 def locked_y1(self): 1236 """ 1237 float or None: The value used for the locked y1. 1238 """ 1239 if self._locked_points.mask[1, 1]: 1240 return None 1241 else: 1242 return self._locked_points[1, 1] 1243 1244 @locked_y1.setter 1245 def locked_y1(self, y1): 1246 self._locked_points.mask[1, 1] = y1 is None 1247 self._locked_points.data[1, 1] = y1 1248 self.invalidate() 1249 1250 1251class Transform(TransformNode): 1252 """ 1253 The base class of all `TransformNode` instances that 1254 actually perform a transformation. 1255 1256 All non-affine transformations should be subclasses of this class. 1257 New affine transformations should be subclasses of `Affine2D`. 1258 1259 Subclasses of this class should override the following members (at 1260 minimum): 1261 1262 - :attr:`input_dims` 1263 - :attr:`output_dims` 1264 - :meth:`transform` 1265 - :meth:`inverted` (if an inverse exists) 1266 1267 The following attributes may be overridden if the default is unsuitable: 1268 1269 - :attr:`is_separable` (defaults to True for 1D -> 1D transforms, False 1270 otherwise) 1271 - :attr:`has_inverse` (defaults to True if :meth:`inverted` is overridden, 1272 False otherwise) 1273 1274 If the transform needs to do something non-standard with 1275 `matplotlib.path.Path` objects, such as adding curves 1276 where there were once line segments, it should override: 1277 1278 - :meth:`transform_path` 1279 """ 1280 1281 input_dims = None 1282 """ 1283 The number of input dimensions of this transform. 1284 Must be overridden (with integers) in the subclass. 1285 """ 1286 1287 output_dims = None 1288 """ 1289 The number of output dimensions of this transform. 1290 Must be overridden (with integers) in the subclass. 1291 """ 1292 1293 is_separable = False 1294 """True if this transform is separable in the x- and y- dimensions.""" 1295 1296 has_inverse = False 1297 """True if this transform has a corresponding inverse transform.""" 1298 1299 def __init_subclass__(cls): 1300 # 1d transforms are always separable; we assume higher-dimensional ones 1301 # are not but subclasses can also directly set is_separable -- this is 1302 # verified by checking whether "is_separable" appears more than once in 1303 # the class's MRO (it appears once in Transform). 1304 if (sum("is_separable" in vars(parent) for parent in cls.__mro__) == 1 1305 and cls.input_dims == cls.output_dims == 1): 1306 cls.is_separable = True 1307 # Transform.inverted raises NotImplementedError; we assume that if this 1308 # is overridden then the transform is invertible but subclass can also 1309 # directly set has_inverse. 1310 if (sum("has_inverse" in vars(parent) for parent in cls.__mro__) == 1 1311 and hasattr(cls, "inverted") 1312 and cls.inverted is not Transform.inverted): 1313 cls.has_inverse = True 1314 1315 def __add__(self, other): 1316 """ 1317 Compose two transforms together so that *self* is followed by *other*. 1318 1319 ``A + B`` returns a transform ``C`` so that 1320 ``C.transform(x) == B.transform(A.transform(x))``. 1321 """ 1322 return (composite_transform_factory(self, other) 1323 if isinstance(other, Transform) else 1324 NotImplemented) 1325 1326 # Equality is based on object identity for `Transform`s (so we don't 1327 # override `__eq__`), but some subclasses, such as TransformWrapper & 1328 # AffineBase, override this behavior. 1329 1330 def _iter_break_from_left_to_right(self): 1331 """ 1332 Return an iterator breaking down this transform stack from left to 1333 right recursively. If self == ((A, N), A) then the result will be an 1334 iterator which yields I : ((A, N), A), followed by A : (N, A), 1335 followed by (A, N) : (A), but not ((A, N), A) : I. 1336 1337 This is equivalent to flattening the stack then yielding 1338 ``flat_stack[:i], flat_stack[i:]`` where i=0..(n-1). 1339 """ 1340 yield IdentityTransform(), self 1341 1342 @property 1343 def depth(self): 1344 """ 1345 Return the number of transforms which have been chained 1346 together to form this Transform instance. 1347 1348 .. note:: 1349 1350 For the special case of a Composite transform, the maximum depth 1351 of the two is returned. 1352 1353 """ 1354 return 1 1355 1356 def contains_branch(self, other): 1357 """ 1358 Return whether the given transform is a sub-tree of this transform. 1359 1360 This routine uses transform equality to identify sub-trees, therefore 1361 in many situations it is object id which will be used. 1362 1363 For the case where the given transform represents the whole 1364 of this transform, returns True. 1365 """ 1366 if self.depth < other.depth: 1367 return False 1368 1369 # check that a subtree is equal to other (starting from self) 1370 for _, sub_tree in self._iter_break_from_left_to_right(): 1371 if sub_tree == other: 1372 return True 1373 return False 1374 1375 def contains_branch_seperately(self, other_transform): 1376 """ 1377 Return whether the given branch is a sub-tree of this transform on 1378 each separate dimension. 1379 1380 A common use for this method is to identify if a transform is a blended 1381 transform containing an axes' data transform. e.g.:: 1382 1383 x_isdata, y_isdata = trans.contains_branch_seperately(ax.transData) 1384 1385 """ 1386 if self.output_dims != 2: 1387 raise ValueError('contains_branch_seperately only supports ' 1388 'transforms with 2 output dimensions') 1389 # for a non-blended transform each separate dimension is the same, so 1390 # just return the appropriate shape. 1391 return [self.contains_branch(other_transform)] * 2 1392 1393 def __sub__(self, other): 1394 """ 1395 Compose *self* with the inverse of *other*, cancelling identical terms 1396 if any:: 1397 1398 # In general: 1399 A - B == A + B.inverted() 1400 # (but see note regarding frozen transforms below). 1401 1402 # If A "ends with" B (i.e. A == A' + B for some A') we can cancel 1403 # out B: 1404 (A' + B) - B == A' 1405 1406 # Likewise, if B "starts with" A (B = A + B'), we can cancel out A: 1407 A - (A + B') == B'.inverted() == B'^-1 1408 1409 Cancellation (rather than naively returning ``A + B.inverted()``) is 1410 important for multiple reasons: 1411 1412 - It avoids floating-point inaccuracies when computing the inverse of 1413 B: ``B - B`` is guaranteed to cancel out exactly (resulting in the 1414 identity transform), whereas ``B + B.inverted()`` may differ by a 1415 small epsilon. 1416 - ``B.inverted()`` always returns a frozen transform: if one computes 1417 ``A + B + B.inverted()`` and later mutates ``B``, then 1418 ``B.inverted()`` won't be updated and the last two terms won't cancel 1419 out anymore; on the other hand, ``A + B - B`` will always be equal to 1420 ``A`` even if ``B`` is mutated. 1421 """ 1422 # we only know how to do this operation if other is a Transform. 1423 if not isinstance(other, Transform): 1424 return NotImplemented 1425 for remainder, sub_tree in self._iter_break_from_left_to_right(): 1426 if sub_tree == other: 1427 return remainder 1428 for remainder, sub_tree in other._iter_break_from_left_to_right(): 1429 if sub_tree == self: 1430 if not remainder.has_inverse: 1431 raise ValueError( 1432 "The shortcut cannot be computed since 'other' " 1433 "includes a non-invertible component") 1434 return remainder.inverted() 1435 # if we have got this far, then there was no shortcut possible 1436 if other.has_inverse: 1437 return self + other.inverted() 1438 else: 1439 raise ValueError('It is not possible to compute transA - transB ' 1440 'since transB cannot be inverted and there is no ' 1441 'shortcut possible.') 1442 1443 def __array__(self, *args, **kwargs): 1444 """Array interface to get at this Transform's affine matrix.""" 1445 return self.get_affine().get_matrix() 1446 1447 def transform(self, values): 1448 """ 1449 Apply this transformation on the given array of *values*. 1450 1451 Parameters 1452 ---------- 1453 values : array 1454 The input values as NumPy array of length :attr:`input_dims` or 1455 shape (N x :attr:`input_dims`). 1456 1457 Returns 1458 ------- 1459 array 1460 The output values as NumPy array of length :attr:`input_dims` or 1461 shape (N x :attr:`output_dims`), depending on the input. 1462 """ 1463 # Ensure that values is a 2d array (but remember whether 1464 # we started with a 1d or 2d array). 1465 values = np.asanyarray(values) 1466 ndim = values.ndim 1467 values = values.reshape((-1, self.input_dims)) 1468 1469 # Transform the values 1470 res = self.transform_affine(self.transform_non_affine(values)) 1471 1472 # Convert the result back to the shape of the input values. 1473 if ndim == 0: 1474 assert not np.ma.is_masked(res) # just to be on the safe side 1475 return res[0, 0] 1476 if ndim == 1: 1477 return res.reshape(-1) 1478 elif ndim == 2: 1479 return res 1480 raise ValueError( 1481 "Input values must have shape (N x {dims}) " 1482 "or ({dims}).".format(dims=self.input_dims)) 1483 1484 def transform_affine(self, values): 1485 """ 1486 Apply only the affine part of this transformation on the 1487 given array of values. 1488 1489 ``transform(values)`` is always equivalent to 1490 ``transform_affine(transform_non_affine(values))``. 1491 1492 In non-affine transformations, this is generally a no-op. In 1493 affine transformations, this is equivalent to 1494 ``transform(values)``. 1495 1496 Parameters 1497 ---------- 1498 values : array 1499 The input values as NumPy array of length :attr:`input_dims` or 1500 shape (N x :attr:`input_dims`). 1501 1502 Returns 1503 ------- 1504 array 1505 The output values as NumPy array of length :attr:`input_dims` or 1506 shape (N x :attr:`output_dims`), depending on the input. 1507 """ 1508 return self.get_affine().transform(values) 1509 1510 def transform_non_affine(self, values): 1511 """ 1512 Apply only the non-affine part of this transformation. 1513 1514 ``transform(values)`` is always equivalent to 1515 ``transform_affine(transform_non_affine(values))``. 1516 1517 In non-affine transformations, this is generally equivalent to 1518 ``transform(values)``. In affine transformations, this is 1519 always a no-op. 1520 1521 Parameters 1522 ---------- 1523 values : array 1524 The input values as NumPy array of length :attr:`input_dims` or 1525 shape (N x :attr:`input_dims`). 1526 1527 Returns 1528 ------- 1529 array 1530 The output values as NumPy array of length :attr:`input_dims` or 1531 shape (N x :attr:`output_dims`), depending on the input. 1532 """ 1533 return values 1534 1535 def transform_bbox(self, bbox): 1536 """ 1537 Transform the given bounding box. 1538 1539 For smarter transforms including caching (a common requirement in 1540 Matplotlib), see `TransformedBbox`. 1541 """ 1542 return Bbox(self.transform(bbox.get_points())) 1543 1544 def get_affine(self): 1545 """Get the affine part of this transform.""" 1546 return IdentityTransform() 1547 1548 def get_matrix(self): 1549 """Get the matrix for the affine part of this transform.""" 1550 return self.get_affine().get_matrix() 1551 1552 def transform_point(self, point): 1553 """ 1554 Return a transformed point. 1555 1556 This function is only kept for backcompatibility; the more general 1557 `.transform` method is capable of transforming both a list of points 1558 and a single point. 1559 1560 The point is given as a sequence of length :attr:`input_dims`. 1561 The transformed point is returned as a sequence of length 1562 :attr:`output_dims`. 1563 """ 1564 if len(point) != self.input_dims: 1565 raise ValueError("The length of 'point' must be 'self.input_dims'") 1566 return self.transform(point) 1567 1568 def transform_path(self, path): 1569 """ 1570 Apply the transform to `.Path` *path*, returning a new `.Path`. 1571 1572 In some cases, this transform may insert curves into the path 1573 that began as line segments. 1574 """ 1575 return self.transform_path_affine(self.transform_path_non_affine(path)) 1576 1577 def transform_path_affine(self, path): 1578 """ 1579 Apply the affine part of this transform to `.Path` *path*, returning a 1580 new `.Path`. 1581 1582 ``transform_path(path)`` is equivalent to 1583 ``transform_path_affine(transform_path_non_affine(values))``. 1584 """ 1585 return self.get_affine().transform_path_affine(path) 1586 1587 def transform_path_non_affine(self, path): 1588 """ 1589 Apply the non-affine part of this transform to `.Path` *path*, 1590 returning a new `.Path`. 1591 1592 ``transform_path(path)`` is equivalent to 1593 ``transform_path_affine(transform_path_non_affine(values))``. 1594 """ 1595 x = self.transform_non_affine(path.vertices) 1596 return Path._fast_from_codes_and_verts(x, path.codes, path) 1597 1598 def transform_angles(self, angles, pts, radians=False, pushoff=1e-5): 1599 """ 1600 Transform a set of angles anchored at specific locations. 1601 1602 Parameters 1603 ---------- 1604 angles : (N,) array-like 1605 The angles to transform. 1606 pts : (N, 2) array-like 1607 The points where the angles are anchored. 1608 radians : bool, default: False 1609 Whether *angles* are radians or degrees. 1610 pushoff : float 1611 For each point in *pts* and angle in *angles*, the transformed 1612 angle is computed by transforming a segment of length *pushoff* 1613 starting at that point and making that angle relative to the 1614 horizontal axis, and measuring the angle between the horizontal 1615 axis and the transformed segment. 1616 1617 Returns 1618 ------- 1619 (N,) array 1620 """ 1621 # Must be 2D 1622 if self.input_dims != 2 or self.output_dims != 2: 1623 raise NotImplementedError('Only defined in 2D') 1624 angles = np.asarray(angles) 1625 pts = np.asarray(pts) 1626 if angles.ndim != 1 or angles.shape[0] != pts.shape[0]: 1627 raise ValueError("'angles' must be a column vector and have same " 1628 "number of rows as 'pts'") 1629 if pts.shape[1] != 2: 1630 raise ValueError("'pts' must be array with 2 columns for x, y") 1631 # Convert to radians if desired 1632 if not radians: 1633 angles = np.deg2rad(angles) 1634 # Move a short distance away 1635 pts2 = pts + pushoff * np.column_stack([np.cos(angles), 1636 np.sin(angles)]) 1637 # Transform both sets of points 1638 tpts = self.transform(pts) 1639 tpts2 = self.transform(pts2) 1640 # Calculate transformed angles 1641 d = tpts2 - tpts 1642 a = np.arctan2(d[:, 1], d[:, 0]) 1643 # Convert back to degrees if desired 1644 if not radians: 1645 a = np.rad2deg(a) 1646 return a 1647 1648 def inverted(self): 1649 """ 1650 Return the corresponding inverse transformation. 1651 1652 It holds ``x == self.inverted().transform(self.transform(x))``. 1653 1654 The return value of this method should be treated as 1655 temporary. An update to *self* does not cause a corresponding 1656 update to its inverted copy. 1657 """ 1658 raise NotImplementedError() 1659 1660 1661class TransformWrapper(Transform): 1662 """ 1663 A helper class that holds a single child transform and acts 1664 equivalently to it. 1665 1666 This is useful if a node of the transform tree must be replaced at 1667 run time with a transform of a different type. This class allows 1668 that replacement to correctly trigger invalidation. 1669 1670 `TransformWrapper` instances must have the same input and output dimensions 1671 during their entire lifetime, so the child transform may only be replaced 1672 with another child transform of the same dimensions. 1673 """ 1674 1675 pass_through = True 1676 1677 def __init__(self, child): 1678 """ 1679 *child*: A `Transform` instance. This child may later 1680 be replaced with :meth:`set`. 1681 """ 1682 _api.check_isinstance(Transform, child=child) 1683 self._init(child) 1684 self.set_children(child) 1685 1686 def _init(self, child): 1687 Transform.__init__(self) 1688 self.input_dims = child.input_dims 1689 self.output_dims = child.output_dims 1690 self._set(child) 1691 self._invalid = 0 1692 1693 def __eq__(self, other): 1694 return self._child.__eq__(other) 1695 1696 __str__ = _make_str_method("_child") 1697 1698 def frozen(self): 1699 # docstring inherited 1700 return self._child.frozen() 1701 1702 def _set(self, child): 1703 self._child = child 1704 1705 self.transform = child.transform 1706 self.transform_affine = child.transform_affine 1707 self.transform_non_affine = child.transform_non_affine 1708 self.transform_path = child.transform_path 1709 self.transform_path_affine = child.transform_path_affine 1710 self.transform_path_non_affine = child.transform_path_non_affine 1711 self.get_affine = child.get_affine 1712 self.inverted = child.inverted 1713 self.get_matrix = child.get_matrix 1714 1715 # note we do not wrap other properties here since the transform's 1716 # child can be changed with WrappedTransform.set and so checking 1717 # is_affine and other such properties may be dangerous. 1718 1719 def set(self, child): 1720 """ 1721 Replace the current child of this transform with another one. 1722 1723 The new child must have the same number of input and output 1724 dimensions as the current child. 1725 """ 1726 if (child.input_dims != self.input_dims or 1727 child.output_dims != self.output_dims): 1728 raise ValueError( 1729 "The new child must have the same number of input and output " 1730 "dimensions as the current child") 1731 1732 self.set_children(child) 1733 self._set(child) 1734 1735 self._invalid = 0 1736 self.invalidate() 1737 self._invalid = 0 1738 1739 is_affine = property(lambda self: self._child.is_affine) 1740 is_separable = property(lambda self: self._child.is_separable) 1741 has_inverse = property(lambda self: self._child.has_inverse) 1742 1743 1744class AffineBase(Transform): 1745 """ 1746 The base class of all affine transformations of any number of dimensions. 1747 """ 1748 is_affine = True 1749 1750 def __init__(self, *args, **kwargs): 1751 super().__init__(*args, **kwargs) 1752 self._inverted = None 1753 1754 def __array__(self, *args, **kwargs): 1755 # optimises the access of the transform matrix vs. the superclass 1756 return self.get_matrix() 1757 1758 def __eq__(self, other): 1759 if getattr(other, "is_affine", False) and hasattr(other, "get_matrix"): 1760 return np.all(self.get_matrix() == other.get_matrix()) 1761 return NotImplemented 1762 1763 def transform(self, values): 1764 # docstring inherited 1765 return self.transform_affine(values) 1766 1767 def transform_affine(self, values): 1768 # docstring inherited 1769 raise NotImplementedError('Affine subclasses should override this ' 1770 'method.') 1771 1772 def transform_non_affine(self, points): 1773 # docstring inherited 1774 return points 1775 1776 def transform_path(self, path): 1777 # docstring inherited 1778 return self.transform_path_affine(path) 1779 1780 def transform_path_affine(self, path): 1781 # docstring inherited 1782 return Path(self.transform_affine(path.vertices), 1783 path.codes, path._interpolation_steps) 1784 1785 def transform_path_non_affine(self, path): 1786 # docstring inherited 1787 return path 1788 1789 def get_affine(self): 1790 # docstring inherited 1791 return self 1792 1793 1794class Affine2DBase(AffineBase): 1795 """ 1796 The base class of all 2D affine transformations. 1797 1798 2D affine transformations are performed using a 3x3 numpy array:: 1799 1800 a c e 1801 b d f 1802 0 0 1 1803 1804 This class provides the read-only interface. For a mutable 2D 1805 affine transformation, use `Affine2D`. 1806 1807 Subclasses of this class will generally only need to override a 1808 constructor and :meth:`get_matrix` that generates a custom 3x3 matrix. 1809 """ 1810 input_dims = 2 1811 output_dims = 2 1812 1813 def frozen(self): 1814 # docstring inherited 1815 return Affine2D(self.get_matrix().copy()) 1816 1817 @property 1818 def is_separable(self): 1819 mtx = self.get_matrix() 1820 return mtx[0, 1] == mtx[1, 0] == 0.0 1821 1822 def to_values(self): 1823 """ 1824 Return the values of the matrix as an ``(a, b, c, d, e, f)`` tuple. 1825 """ 1826 mtx = self.get_matrix() 1827 return tuple(mtx[:2].swapaxes(0, 1).flat) 1828 1829 def transform_affine(self, points): 1830 mtx = self.get_matrix() 1831 if isinstance(points, np.ma.MaskedArray): 1832 tpoints = affine_transform(points.data, mtx) 1833 return np.ma.MaskedArray(tpoints, mask=np.ma.getmask(points)) 1834 return affine_transform(points, mtx) 1835 1836 if DEBUG: 1837 _transform_affine = transform_affine 1838 1839 def transform_affine(self, points): 1840 # docstring inherited 1841 # The major speed trap here is just converting to the 1842 # points to an array in the first place. If we can use 1843 # more arrays upstream, that should help here. 1844 if not isinstance(points, (np.ma.MaskedArray, np.ndarray)): 1845 _api.warn_external( 1846 f'A non-numpy array of type {type(points)} was passed in ' 1847 f'for transformation, which results in poor performance.') 1848 return self._transform_affine(points) 1849 1850 def inverted(self): 1851 # docstring inherited 1852 if self._inverted is None or self._invalid: 1853 mtx = self.get_matrix() 1854 shorthand_name = None 1855 if self._shorthand_name: 1856 shorthand_name = '(%s)-1' % self._shorthand_name 1857 self._inverted = Affine2D(inv(mtx), shorthand_name=shorthand_name) 1858 self._invalid = 0 1859 return self._inverted 1860 1861 1862class Affine2D(Affine2DBase): 1863 """ 1864 A mutable 2D affine transformation. 1865 """ 1866 1867 def __init__(self, matrix=None, **kwargs): 1868 """ 1869 Initialize an Affine transform from a 3x3 numpy float array:: 1870 1871 a c e 1872 b d f 1873 0 0 1 1874 1875 If *matrix* is None, initialize with the identity transform. 1876 """ 1877 super().__init__(**kwargs) 1878 if matrix is None: 1879 # A bit faster than np.identity(3). 1880 matrix = IdentityTransform._mtx.copy() 1881 self._mtx = matrix.copy() 1882 self._invalid = 0 1883 1884 __str__ = _make_str_method("_mtx") 1885 1886 @staticmethod 1887 def from_values(a, b, c, d, e, f): 1888 """ 1889 Create a new Affine2D instance from the given values:: 1890 1891 a c e 1892 b d f 1893 0 0 1 1894 1895 . 1896 """ 1897 return Affine2D( 1898 np.array([a, c, e, b, d, f, 0.0, 0.0, 1.0], float).reshape((3, 3))) 1899 1900 def get_matrix(self): 1901 """ 1902 Get the underlying transformation matrix as a 3x3 numpy array:: 1903 1904 a c e 1905 b d f 1906 0 0 1 1907 1908 . 1909 """ 1910 if self._invalid: 1911 self._inverted = None 1912 self._invalid = 0 1913 return self._mtx 1914 1915 def set_matrix(self, mtx): 1916 """ 1917 Set the underlying transformation matrix from a 3x3 numpy array:: 1918 1919 a c e 1920 b d f 1921 0 0 1 1922 1923 . 1924 """ 1925 self._mtx = mtx 1926 self.invalidate() 1927 1928 def set(self, other): 1929 """ 1930 Set this transformation from the frozen copy of another 1931 `Affine2DBase` object. 1932 """ 1933 _api.check_isinstance(Affine2DBase, other=other) 1934 self._mtx = other.get_matrix() 1935 self.invalidate() 1936 1937 @staticmethod 1938 def identity(): 1939 """ 1940 Return a new `Affine2D` object that is the identity transform. 1941 1942 Unless this transform will be mutated later on, consider using 1943 the faster `IdentityTransform` class instead. 1944 """ 1945 return Affine2D() 1946 1947 def clear(self): 1948 """ 1949 Reset the underlying matrix to the identity transform. 1950 """ 1951 # A bit faster than np.identity(3). 1952 self._mtx = IdentityTransform._mtx.copy() 1953 self.invalidate() 1954 return self 1955 1956 def rotate(self, theta): 1957 """ 1958 Add a rotation (in radians) to this transform in place. 1959 1960 Returns *self*, so this method can easily be chained with more 1961 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 1962 and :meth:`scale`. 1963 """ 1964 a = math.cos(theta) 1965 b = math.sin(theta) 1966 rotate_mtx = np.array([[a, -b, 0.0], [b, a, 0.0], [0.0, 0.0, 1.0]], 1967 float) 1968 self._mtx = np.dot(rotate_mtx, self._mtx) 1969 self.invalidate() 1970 return self 1971 1972 def rotate_deg(self, degrees): 1973 """ 1974 Add a rotation (in degrees) to this transform in place. 1975 1976 Returns *self*, so this method can easily be chained with more 1977 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 1978 and :meth:`scale`. 1979 """ 1980 return self.rotate(math.radians(degrees)) 1981 1982 def rotate_around(self, x, y, theta): 1983 """ 1984 Add a rotation (in radians) around the point (x, y) in place. 1985 1986 Returns *self*, so this method can easily be chained with more 1987 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 1988 and :meth:`scale`. 1989 """ 1990 return self.translate(-x, -y).rotate(theta).translate(x, y) 1991 1992 def rotate_deg_around(self, x, y, degrees): 1993 """ 1994 Add a rotation (in degrees) around the point (x, y) in place. 1995 1996 Returns *self*, so this method can easily be chained with more 1997 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 1998 and :meth:`scale`. 1999 """ 2000 # Cast to float to avoid wraparound issues with uint8's 2001 x, y = float(x), float(y) 2002 return self.translate(-x, -y).rotate_deg(degrees).translate(x, y) 2003 2004 def translate(self, tx, ty): 2005 """ 2006 Add a translation in place. 2007 2008 Returns *self*, so this method can easily be chained with more 2009 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 2010 and :meth:`scale`. 2011 """ 2012 self._mtx[0, 2] += tx 2013 self._mtx[1, 2] += ty 2014 self.invalidate() 2015 return self 2016 2017 def scale(self, sx, sy=None): 2018 """ 2019 Add a scale in place. 2020 2021 If *sy* is None, the same scale is applied in both the *x*- and 2022 *y*-directions. 2023 2024 Returns *self*, so this method can easily be chained with more 2025 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 2026 and :meth:`scale`. 2027 """ 2028 if sy is None: 2029 sy = sx 2030 # explicit element-wise scaling is fastest 2031 self._mtx[0, 0] *= sx 2032 self._mtx[0, 1] *= sx 2033 self._mtx[0, 2] *= sx 2034 self._mtx[1, 0] *= sy 2035 self._mtx[1, 1] *= sy 2036 self._mtx[1, 2] *= sy 2037 self.invalidate() 2038 return self 2039 2040 def skew(self, xShear, yShear): 2041 """ 2042 Add a skew in place. 2043 2044 *xShear* and *yShear* are the shear angles along the *x*- and 2045 *y*-axes, respectively, in radians. 2046 2047 Returns *self*, so this method can easily be chained with more 2048 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 2049 and :meth:`scale`. 2050 """ 2051 rotX = math.tan(xShear) 2052 rotY = math.tan(yShear) 2053 skew_mtx = np.array( 2054 [[1.0, rotX, 0.0], [rotY, 1.0, 0.0], [0.0, 0.0, 1.0]], float) 2055 self._mtx = np.dot(skew_mtx, self._mtx) 2056 self.invalidate() 2057 return self 2058 2059 def skew_deg(self, xShear, yShear): 2060 """ 2061 Add a skew in place. 2062 2063 *xShear* and *yShear* are the shear angles along the *x*- and 2064 *y*-axes, respectively, in degrees. 2065 2066 Returns *self*, so this method can easily be chained with more 2067 calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` 2068 and :meth:`scale`. 2069 """ 2070 return self.skew(math.radians(xShear), math.radians(yShear)) 2071 2072 2073class IdentityTransform(Affine2DBase): 2074 """ 2075 A special class that does one thing, the identity transform, in a 2076 fast way. 2077 """ 2078 _mtx = np.identity(3) 2079 2080 def frozen(self): 2081 # docstring inherited 2082 return self 2083 2084 __str__ = _make_str_method() 2085 2086 def get_matrix(self): 2087 # docstring inherited 2088 return self._mtx 2089 2090 def transform(self, points): 2091 # docstring inherited 2092 return np.asanyarray(points) 2093 2094 def transform_affine(self, points): 2095 # docstring inherited 2096 return np.asanyarray(points) 2097 2098 def transform_non_affine(self, points): 2099 # docstring inherited 2100 return np.asanyarray(points) 2101 2102 def transform_path(self, path): 2103 # docstring inherited 2104 return path 2105 2106 def transform_path_affine(self, path): 2107 # docstring inherited 2108 return path 2109 2110 def transform_path_non_affine(self, path): 2111 # docstring inherited 2112 return path 2113 2114 def get_affine(self): 2115 # docstring inherited 2116 return self 2117 2118 def inverted(self): 2119 # docstring inherited 2120 return self 2121 2122 2123class _BlendedMixin: 2124 """Common methods for `BlendedGenericTransform` and `BlendedAffine2D`.""" 2125 2126 def __eq__(self, other): 2127 if isinstance(other, (BlendedAffine2D, BlendedGenericTransform)): 2128 return (self._x == other._x) and (self._y == other._y) 2129 elif self._x == self._y: 2130 return self._x == other 2131 else: 2132 return NotImplemented 2133 2134 def contains_branch_seperately(self, transform): 2135 return (self._x.contains_branch(transform), 2136 self._y.contains_branch(transform)) 2137 2138 __str__ = _make_str_method("_x", "_y") 2139 2140 2141class BlendedGenericTransform(_BlendedMixin, Transform): 2142 """ 2143 A "blended" transform uses one transform for the *x*-direction, and 2144 another transform for the *y*-direction. 2145 2146 This "generic" version can handle any given child transform in the 2147 *x*- and *y*-directions. 2148 """ 2149 input_dims = 2 2150 output_dims = 2 2151 is_separable = True 2152 pass_through = True 2153 2154 def __init__(self, x_transform, y_transform, **kwargs): 2155 """ 2156 Create a new "blended" transform using *x_transform* to transform the 2157 *x*-axis and *y_transform* to transform the *y*-axis. 2158 2159 You will generally not call this constructor directly but use the 2160 `blended_transform_factory` function instead, which can determine 2161 automatically which kind of blended transform to create. 2162 """ 2163 Transform.__init__(self, **kwargs) 2164 self._x = x_transform 2165 self._y = y_transform 2166 self.set_children(x_transform, y_transform) 2167 self._affine = None 2168 2169 @property 2170 def depth(self): 2171 return max(self._x.depth, self._y.depth) 2172 2173 def contains_branch(self, other): 2174 # A blended transform cannot possibly contain a branch from two 2175 # different transforms. 2176 return False 2177 2178 is_affine = property(lambda self: self._x.is_affine and self._y.is_affine) 2179 has_inverse = property( 2180 lambda self: self._x.has_inverse and self._y.has_inverse) 2181 2182 def frozen(self): 2183 # docstring inherited 2184 return blended_transform_factory(self._x.frozen(), self._y.frozen()) 2185 2186 def transform_non_affine(self, points): 2187 # docstring inherited 2188 if self._x.is_affine and self._y.is_affine: 2189 return points 2190 x = self._x 2191 y = self._y 2192 2193 if x == y and x.input_dims == 2: 2194 return x.transform_non_affine(points) 2195 2196 if x.input_dims == 2: 2197 x_points = x.transform_non_affine(points)[:, 0:1] 2198 else: 2199 x_points = x.transform_non_affine(points[:, 0]) 2200 x_points = x_points.reshape((len(x_points), 1)) 2201 2202 if y.input_dims == 2: 2203 y_points = y.transform_non_affine(points)[:, 1:] 2204 else: 2205 y_points = y.transform_non_affine(points[:, 1]) 2206 y_points = y_points.reshape((len(y_points), 1)) 2207 2208 if (isinstance(x_points, np.ma.MaskedArray) or 2209 isinstance(y_points, np.ma.MaskedArray)): 2210 return np.ma.concatenate((x_points, y_points), 1) 2211 else: 2212 return np.concatenate((x_points, y_points), 1) 2213 2214 def inverted(self): 2215 # docstring inherited 2216 return BlendedGenericTransform(self._x.inverted(), self._y.inverted()) 2217 2218 def get_affine(self): 2219 # docstring inherited 2220 if self._invalid or self._affine is None: 2221 if self._x == self._y: 2222 self._affine = self._x.get_affine() 2223 else: 2224 x_mtx = self._x.get_affine().get_matrix() 2225 y_mtx = self._y.get_affine().get_matrix() 2226 # We already know the transforms are separable, so we can skip 2227 # setting b and c to zero. 2228 mtx = np.array([x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0]]) 2229 self._affine = Affine2D(mtx) 2230 self._invalid = 0 2231 return self._affine 2232 2233 2234class BlendedAffine2D(_BlendedMixin, Affine2DBase): 2235 """ 2236 A "blended" transform uses one transform for the *x*-direction, and 2237 another transform for the *y*-direction. 2238 2239 This version is an optimization for the case where both child 2240 transforms are of type `Affine2DBase`. 2241 """ 2242 2243 is_separable = True 2244 2245 def __init__(self, x_transform, y_transform, **kwargs): 2246 """ 2247 Create a new "blended" transform using *x_transform* to transform the 2248 *x*-axis and *y_transform* to transform the *y*-axis. 2249 2250 Both *x_transform* and *y_transform* must be 2D affine transforms. 2251 2252 You will generally not call this constructor directly but use the 2253 `blended_transform_factory` function instead, which can determine 2254 automatically which kind of blended transform to create. 2255 """ 2256 is_affine = x_transform.is_affine and y_transform.is_affine 2257 is_separable = x_transform.is_separable and y_transform.is_separable 2258 is_correct = is_affine and is_separable 2259 if not is_correct: 2260 raise ValueError("Both *x_transform* and *y_transform* must be 2D " 2261 "affine transforms") 2262 2263 Transform.__init__(self, **kwargs) 2264 self._x = x_transform 2265 self._y = y_transform 2266 self.set_children(x_transform, y_transform) 2267 2268 Affine2DBase.__init__(self) 2269 self._mtx = None 2270 2271 def get_matrix(self): 2272 # docstring inherited 2273 if self._invalid: 2274 if self._x == self._y: 2275 self._mtx = self._x.get_matrix() 2276 else: 2277 x_mtx = self._x.get_matrix() 2278 y_mtx = self._y.get_matrix() 2279 # We already know the transforms are separable, so we can skip 2280 # setting b and c to zero. 2281 self._mtx = np.array([x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0]]) 2282 self._inverted = None 2283 self._invalid = 0 2284 return self._mtx 2285 2286 2287def blended_transform_factory(x_transform, y_transform): 2288 """ 2289 Create a new "blended" transform using *x_transform* to transform 2290 the *x*-axis and *y_transform* to transform the *y*-axis. 2291 2292 A faster version of the blended transform is returned for the case 2293 where both child transforms are affine. 2294 """ 2295 if (isinstance(x_transform, Affine2DBase) and 2296 isinstance(y_transform, Affine2DBase)): 2297 return BlendedAffine2D(x_transform, y_transform) 2298 return BlendedGenericTransform(x_transform, y_transform) 2299 2300 2301class CompositeGenericTransform(Transform): 2302 """ 2303 A composite transform formed by applying transform *a* then 2304 transform *b*. 2305 2306 This "generic" version can handle any two arbitrary 2307 transformations. 2308 """ 2309 pass_through = True 2310 2311 def __init__(self, a, b, **kwargs): 2312 """ 2313 Create a new composite transform that is the result of 2314 applying transform *a* then transform *b*. 2315 2316 You will generally not call this constructor directly but write ``a + 2317 b`` instead, which will automatically choose the best kind of composite 2318 transform instance to create. 2319 """ 2320 if a.output_dims != b.input_dims: 2321 raise ValueError("The output dimension of 'a' must be equal to " 2322 "the input dimensions of 'b'") 2323 self.input_dims = a.input_dims 2324 self.output_dims = b.output_dims 2325 2326 super().__init__(**kwargs) 2327 self._a = a 2328 self._b = b 2329 self.set_children(a, b) 2330 2331 def frozen(self): 2332 # docstring inherited 2333 self._invalid = 0 2334 frozen = composite_transform_factory( 2335 self._a.frozen(), self._b.frozen()) 2336 if not isinstance(frozen, CompositeGenericTransform): 2337 return frozen.frozen() 2338 return frozen 2339 2340 def _invalidate_internal(self, value, invalidating_node): 2341 # In some cases for a composite transform, an invalidating call to 2342 # AFFINE_ONLY needs to be extended to invalidate the NON_AFFINE part 2343 # too. These cases are when the right hand transform is non-affine and 2344 # either: 2345 # (a) the left hand transform is non affine 2346 # (b) it is the left hand node which has triggered the invalidation 2347 if (value == Transform.INVALID_AFFINE and 2348 not self._b.is_affine and 2349 (not self._a.is_affine or invalidating_node is self._a)): 2350 value = Transform.INVALID 2351 2352 super()._invalidate_internal(value=value, 2353 invalidating_node=invalidating_node) 2354 2355 def __eq__(self, other): 2356 if isinstance(other, (CompositeGenericTransform, CompositeAffine2D)): 2357 return self is other or (self._a == other._a 2358 and self._b == other._b) 2359 else: 2360 return False 2361 2362 def _iter_break_from_left_to_right(self): 2363 for left, right in self._a._iter_break_from_left_to_right(): 2364 yield left, right + self._b 2365 for left, right in self._b._iter_break_from_left_to_right(): 2366 yield self._a + left, right 2367 2368 depth = property(lambda self: self._a.depth + self._b.depth) 2369 is_affine = property(lambda self: self._a.is_affine and self._b.is_affine) 2370 is_separable = property( 2371 lambda self: self._a.is_separable and self._b.is_separable) 2372 has_inverse = property( 2373 lambda self: self._a.has_inverse and self._b.has_inverse) 2374 2375 __str__ = _make_str_method("_a", "_b") 2376 2377 def transform_affine(self, points): 2378 # docstring inherited 2379 return self.get_affine().transform(points) 2380 2381 def transform_non_affine(self, points): 2382 # docstring inherited 2383 if self._a.is_affine and self._b.is_affine: 2384 return points 2385 elif not self._a.is_affine and self._b.is_affine: 2386 return self._a.transform_non_affine(points) 2387 else: 2388 return self._b.transform_non_affine( 2389 self._a.transform(points)) 2390 2391 def transform_path_non_affine(self, path): 2392 # docstring inherited 2393 if self._a.is_affine and self._b.is_affine: 2394 return path 2395 elif not self._a.is_affine and self._b.is_affine: 2396 return self._a.transform_path_non_affine(path) 2397 else: 2398 return self._b.transform_path_non_affine( 2399 self._a.transform_path(path)) 2400 2401 def get_affine(self): 2402 # docstring inherited 2403 if not self._b.is_affine: 2404 return self._b.get_affine() 2405 else: 2406 return Affine2D(np.dot(self._b.get_affine().get_matrix(), 2407 self._a.get_affine().get_matrix())) 2408 2409 def inverted(self): 2410 # docstring inherited 2411 return CompositeGenericTransform( 2412 self._b.inverted(), self._a.inverted()) 2413 2414 2415class CompositeAffine2D(Affine2DBase): 2416 """ 2417 A composite transform formed by applying transform *a* then transform *b*. 2418 2419 This version is an optimization that handles the case where both *a* 2420 and *b* are 2D affines. 2421 """ 2422 def __init__(self, a, b, **kwargs): 2423 """ 2424 Create a new composite transform that is the result of 2425 applying `Affine2DBase` *a* then `Affine2DBase` *b*. 2426 2427 You will generally not call this constructor directly but write ``a + 2428 b`` instead, which will automatically choose the best kind of composite 2429 transform instance to create. 2430 """ 2431 if not a.is_affine or not b.is_affine: 2432 raise ValueError("'a' and 'b' must be affine transforms") 2433 if a.output_dims != b.input_dims: 2434 raise ValueError("The output dimension of 'a' must be equal to " 2435 "the input dimensions of 'b'") 2436 self.input_dims = a.input_dims 2437 self.output_dims = b.output_dims 2438 2439 super().__init__(**kwargs) 2440 self._a = a 2441 self._b = b 2442 self.set_children(a, b) 2443 self._mtx = None 2444 2445 @property 2446 def depth(self): 2447 return self._a.depth + self._b.depth 2448 2449 def _iter_break_from_left_to_right(self): 2450 for left, right in self._a._iter_break_from_left_to_right(): 2451 yield left, right + self._b 2452 for left, right in self._b._iter_break_from_left_to_right(): 2453 yield self._a + left, right 2454 2455 __str__ = _make_str_method("_a", "_b") 2456 2457 def get_matrix(self): 2458 # docstring inherited 2459 if self._invalid: 2460 self._mtx = np.dot( 2461 self._b.get_matrix(), 2462 self._a.get_matrix()) 2463 self._inverted = None 2464 self._invalid = 0 2465 return self._mtx 2466 2467 2468def composite_transform_factory(a, b): 2469 """ 2470 Create a new composite transform that is the result of applying 2471 transform a then transform b. 2472 2473 Shortcut versions of the blended transform are provided for the 2474 case where both child transforms are affine, or one or the other 2475 is the identity transform. 2476 2477 Composite transforms may also be created using the '+' operator, 2478 e.g.:: 2479 2480 c = a + b 2481 """ 2482 # check to see if any of a or b are IdentityTransforms. We use 2483 # isinstance here to guarantee that the transforms will *always* 2484 # be IdentityTransforms. Since TransformWrappers are mutable, 2485 # use of equality here would be wrong. 2486 if isinstance(a, IdentityTransform): 2487 return b 2488 elif isinstance(b, IdentityTransform): 2489 return a 2490 elif isinstance(a, Affine2D) and isinstance(b, Affine2D): 2491 return CompositeAffine2D(a, b) 2492 return CompositeGenericTransform(a, b) 2493 2494 2495class BboxTransform(Affine2DBase): 2496 """ 2497 `BboxTransform` linearly transforms points from one `Bbox` to another. 2498 """ 2499 2500 is_separable = True 2501 2502 def __init__(self, boxin, boxout, **kwargs): 2503 """ 2504 Create a new `BboxTransform` that linearly transforms 2505 points from *boxin* to *boxout*. 2506 """ 2507 if not boxin.is_bbox or not boxout.is_bbox: 2508 raise ValueError("'boxin' and 'boxout' must be bbox") 2509 2510 super().__init__(**kwargs) 2511 self._boxin = boxin 2512 self._boxout = boxout 2513 self.set_children(boxin, boxout) 2514 self._mtx = None 2515 self._inverted = None 2516 2517 __str__ = _make_str_method("_boxin", "_boxout") 2518 2519 def get_matrix(self): 2520 # docstring inherited 2521 if self._invalid: 2522 inl, inb, inw, inh = self._boxin.bounds 2523 outl, outb, outw, outh = self._boxout.bounds 2524 x_scale = outw / inw 2525 y_scale = outh / inh 2526 if DEBUG and (x_scale == 0 or y_scale == 0): 2527 raise ValueError( 2528 "Transforming from or to a singular bounding box") 2529 self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale+outl)], 2530 [0.0 , y_scale, (-inb*y_scale+outb)], 2531 [0.0 , 0.0 , 1.0 ]], 2532 float) 2533 self._inverted = None 2534 self._invalid = 0 2535 return self._mtx 2536 2537 2538class BboxTransformTo(Affine2DBase): 2539 """ 2540 `BboxTransformTo` is a transformation that linearly transforms points from 2541 the unit bounding box to a given `Bbox`. 2542 """ 2543 2544 is_separable = True 2545 2546 def __init__(self, boxout, **kwargs): 2547 """ 2548 Create a new `BboxTransformTo` that linearly transforms 2549 points from the unit bounding box to *boxout*. 2550 """ 2551 if not boxout.is_bbox: 2552 raise ValueError("'boxout' must be bbox") 2553 2554 super().__init__(**kwargs) 2555 self._boxout = boxout 2556 self.set_children(boxout) 2557 self._mtx = None 2558 self._inverted = None 2559 2560 __str__ = _make_str_method("_boxout") 2561 2562 def get_matrix(self): 2563 # docstring inherited 2564 if self._invalid: 2565 outl, outb, outw, outh = self._boxout.bounds 2566 if DEBUG and (outw == 0 or outh == 0): 2567 raise ValueError("Transforming to a singular bounding box.") 2568 self._mtx = np.array([[outw, 0.0, outl], 2569 [ 0.0, outh, outb], 2570 [ 0.0, 0.0, 1.0]], 2571 float) 2572 self._inverted = None 2573 self._invalid = 0 2574 return self._mtx 2575 2576 2577class BboxTransformToMaxOnly(BboxTransformTo): 2578 """ 2579 `BboxTransformTo` is a transformation that linearly transforms points from 2580 the unit bounding box to a given `Bbox` with a fixed upper left of (0, 0). 2581 """ 2582 def get_matrix(self): 2583 # docstring inherited 2584 if self._invalid: 2585 xmax, ymax = self._boxout.max 2586 if DEBUG and (xmax == 0 or ymax == 0): 2587 raise ValueError("Transforming to a singular bounding box.") 2588 self._mtx = np.array([[xmax, 0.0, 0.0], 2589 [ 0.0, ymax, 0.0], 2590 [ 0.0, 0.0, 1.0]], 2591 float) 2592 self._inverted = None 2593 self._invalid = 0 2594 return self._mtx 2595 2596 2597class BboxTransformFrom(Affine2DBase): 2598 """ 2599 `BboxTransformFrom` linearly transforms points from a given `Bbox` to the 2600 unit bounding box. 2601 """ 2602 is_separable = True 2603 2604 def __init__(self, boxin, **kwargs): 2605 if not boxin.is_bbox: 2606 raise ValueError("'boxin' must be bbox") 2607 2608 super().__init__(**kwargs) 2609 self._boxin = boxin 2610 self.set_children(boxin) 2611 self._mtx = None 2612 self._inverted = None 2613 2614 __str__ = _make_str_method("_boxin") 2615 2616 def get_matrix(self): 2617 # docstring inherited 2618 if self._invalid: 2619 inl, inb, inw, inh = self._boxin.bounds 2620 if DEBUG and (inw == 0 or inh == 0): 2621 raise ValueError("Transforming from a singular bounding box.") 2622 x_scale = 1.0 / inw 2623 y_scale = 1.0 / inh 2624 self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale)], 2625 [0.0 , y_scale, (-inb*y_scale)], 2626 [0.0 , 0.0 , 1.0 ]], 2627 float) 2628 self._inverted = None 2629 self._invalid = 0 2630 return self._mtx 2631 2632 2633class ScaledTranslation(Affine2DBase): 2634 """ 2635 A transformation that translates by *xt* and *yt*, after *xt* and *yt* 2636 have been transformed by *scale_trans*. 2637 """ 2638 def __init__(self, xt, yt, scale_trans, **kwargs): 2639 super().__init__(**kwargs) 2640 self._t = (xt, yt) 2641 self._scale_trans = scale_trans 2642 self.set_children(scale_trans) 2643 self._mtx = None 2644 self._inverted = None 2645 2646 __str__ = _make_str_method("_t") 2647 2648 def get_matrix(self): 2649 # docstring inherited 2650 if self._invalid: 2651 # A bit faster than np.identity(3). 2652 self._mtx = IdentityTransform._mtx.copy() 2653 self._mtx[:2, 2] = self._scale_trans.transform(self._t) 2654 self._invalid = 0 2655 self._inverted = None 2656 return self._mtx 2657 2658 2659class AffineDeltaTransform(Affine2DBase): 2660 r""" 2661 A transform wrapper for transforming displacements between pairs of points. 2662 2663 This class is intended to be used to transform displacements ("position 2664 deltas") between pairs of points (e.g., as the ``offset_transform`` 2665 of `.Collection`\s): given a transform ``t`` such that ``t = 2666 AffineDeltaTransform(t) + offset``, ``AffineDeltaTransform`` 2667 satisfies ``AffineDeltaTransform(a - b) == AffineDeltaTransform(a) - 2668 AffineDeltaTransform(b)``. 2669 2670 This is implemented by forcing the offset components of the transform 2671 matrix to zero. 2672 2673 This class is experimental as of 3.3, and the API may change. 2674 """ 2675 2676 def __init__(self, transform, **kwargs): 2677 super().__init__(**kwargs) 2678 self._base_transform = transform 2679 2680 __str__ = _make_str_method("_base_transform") 2681 2682 def get_matrix(self): 2683 if self._invalid: 2684 self._mtx = self._base_transform.get_matrix().copy() 2685 self._mtx[:2, -1] = 0 2686 return self._mtx 2687 2688 2689class TransformedPath(TransformNode): 2690 """ 2691 A `TransformedPath` caches a non-affine transformed copy of the 2692 `~.path.Path`. This cached copy is automatically updated when the 2693 non-affine part of the transform changes. 2694 2695 .. note:: 2696 2697 Paths are considered immutable by this class. Any update to the 2698 path's vertices/codes will not trigger a transform recomputation. 2699 2700 """ 2701 def __init__(self, path, transform): 2702 """ 2703 Parameters 2704 ---------- 2705 path : `~.path.Path` 2706 transform : `Transform` 2707 """ 2708 _api.check_isinstance(Transform, transform=transform) 2709 super().__init__() 2710 self._path = path 2711 self._transform = transform 2712 self.set_children(transform) 2713 self._transformed_path = None 2714 self._transformed_points = None 2715 2716 def _revalidate(self): 2717 # only recompute if the invalidation includes the non_affine part of 2718 # the transform 2719 if (self._invalid & self.INVALID_NON_AFFINE == self.INVALID_NON_AFFINE 2720 or self._transformed_path is None): 2721 self._transformed_path = \ 2722 self._transform.transform_path_non_affine(self._path) 2723 self._transformed_points = \ 2724 Path._fast_from_codes_and_verts( 2725 self._transform.transform_non_affine(self._path.vertices), 2726 None, self._path) 2727 self._invalid = 0 2728 2729 def get_transformed_points_and_affine(self): 2730 """ 2731 Return a copy of the child path, with the non-affine part of 2732 the transform already applied, along with the affine part of 2733 the path necessary to complete the transformation. Unlike 2734 :meth:`get_transformed_path_and_affine`, no interpolation will 2735 be performed. 2736 """ 2737 self._revalidate() 2738 return self._transformed_points, self.get_affine() 2739 2740 def get_transformed_path_and_affine(self): 2741 """ 2742 Return a copy of the child path, with the non-affine part of 2743 the transform already applied, along with the affine part of 2744 the path necessary to complete the transformation. 2745 """ 2746 self._revalidate() 2747 return self._transformed_path, self.get_affine() 2748 2749 def get_fully_transformed_path(self): 2750 """ 2751 Return a fully-transformed copy of the child path. 2752 """ 2753 self._revalidate() 2754 return self._transform.transform_path_affine(self._transformed_path) 2755 2756 def get_affine(self): 2757 return self._transform.get_affine() 2758 2759 2760class TransformedPatchPath(TransformedPath): 2761 """ 2762 A `TransformedPatchPath` caches a non-affine transformed copy of the 2763 `~.patches.Patch`. This cached copy is automatically updated when the 2764 non-affine part of the transform or the patch changes. 2765 """ 2766 def __init__(self, patch): 2767 """ 2768 Parameters 2769 ---------- 2770 patch : `~.patches.Patch` 2771 """ 2772 TransformNode.__init__(self) 2773 2774 transform = patch.get_transform() 2775 self._patch = patch 2776 self._transform = transform 2777 self.set_children(transform) 2778 self._path = patch.get_path() 2779 self._transformed_path = None 2780 self._transformed_points = None 2781 2782 def _revalidate(self): 2783 patch_path = self._patch.get_path() 2784 # Only recompute if the invalidation includes the non_affine part of 2785 # the transform, or the Patch's Path has changed. 2786 if (self._transformed_path is None or self._path != patch_path or 2787 (self._invalid & self.INVALID_NON_AFFINE == 2788 self.INVALID_NON_AFFINE)): 2789 self._path = patch_path 2790 self._transformed_path = \ 2791 self._transform.transform_path_non_affine(patch_path) 2792 self._transformed_points = \ 2793 Path._fast_from_codes_and_verts( 2794 self._transform.transform_non_affine(patch_path.vertices), 2795 None, patch_path) 2796 self._invalid = 0 2797 2798 2799def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): 2800 """ 2801 Modify the endpoints of a range as needed to avoid singularities. 2802 2803 Parameters 2804 ---------- 2805 vmin, vmax : float 2806 The initial endpoints. 2807 expander : float, default: 0.001 2808 Fractional amount by which *vmin* and *vmax* are expanded if 2809 the original interval is too small, based on *tiny*. 2810 tiny : float, default: 1e-15 2811 Threshold for the ratio of the interval to the maximum absolute 2812 value of its endpoints. If the interval is smaller than 2813 this, it will be expanded. This value should be around 2814 1e-15 or larger; otherwise the interval will be approaching 2815 the double precision resolution limit. 2816 increasing : bool, default: True 2817 If True, swap *vmin*, *vmax* if *vmin* > *vmax*. 2818 2819 Returns 2820 ------- 2821 vmin, vmax : float 2822 Endpoints, expanded and/or swapped if necessary. 2823 If either input is inf or NaN, or if both inputs are 0 or very 2824 close to zero, it returns -*expander*, *expander*. 2825 """ 2826 2827 if (not np.isfinite(vmin)) or (not np.isfinite(vmax)): 2828 return -expander, expander 2829 2830 swapped = False 2831 if vmax < vmin: 2832 vmin, vmax = vmax, vmin 2833 swapped = True 2834 2835 # Expand vmin, vmax to float: if they were integer types, they can wrap 2836 # around in abs (abs(np.int8(-128)) == -128) and vmax - vmin can overflow. 2837 vmin, vmax = map(float, [vmin, vmax]) 2838 2839 maxabsvalue = max(abs(vmin), abs(vmax)) 2840 if maxabsvalue < (1e6 / tiny) * np.finfo(float).tiny: 2841 vmin = -expander 2842 vmax = expander 2843 2844 elif vmax - vmin <= maxabsvalue * tiny: 2845 if vmax == 0 and vmin == 0: 2846 vmin = -expander 2847 vmax = expander 2848 else: 2849 vmin -= expander*abs(vmin) 2850 vmax += expander*abs(vmax) 2851 2852 if swapped and not increasing: 2853 vmin, vmax = vmax, vmin 2854 return vmin, vmax 2855 2856 2857def interval_contains(interval, val): 2858 """ 2859 Check, inclusively, whether an interval includes a given value. 2860 2861 Parameters 2862 ---------- 2863 interval : (float, float) 2864 The endpoints of the interval. 2865 val : float 2866 Value to check is within interval. 2867 2868 Returns 2869 ------- 2870 bool 2871 Whether *val* is within the *interval*. 2872 """ 2873 a, b = interval 2874 if a > b: 2875 a, b = b, a 2876 return a <= val <= b 2877 2878 2879def _interval_contains_close(interval, val, rtol=1e-10): 2880 """ 2881 Check, inclusively, whether an interval includes a given value, with the 2882 interval expanded by a small tolerance to admit floating point errors. 2883 2884 Parameters 2885 ---------- 2886 interval : (float, float) 2887 The endpoints of the interval. 2888 val : float 2889 Value to check is within interval. 2890 rtol : float, default: 1e-10 2891 Relative tolerance slippage allowed outside of the interval. 2892 For an interval ``[a, b]``, values 2893 ``a - rtol * (b - a) <= val <= b + rtol * (b - a)`` are considered 2894 inside the interval. 2895 2896 Returns 2897 ------- 2898 bool 2899 Whether *val* is within the *interval* (with tolerance). 2900 """ 2901 a, b = interval 2902 if a > b: 2903 a, b = b, a 2904 rtol = (b - a) * rtol 2905 return a - rtol <= val <= b + rtol 2906 2907 2908def interval_contains_open(interval, val): 2909 """ 2910 Check, excluding endpoints, whether an interval includes a given value. 2911 2912 Parameters 2913 ---------- 2914 interval : (float, float) 2915 The endpoints of the interval. 2916 val : float 2917 Value to check is within interval. 2918 2919 Returns 2920 ------- 2921 bool 2922 Whether *val* is within the *interval*. 2923 """ 2924 a, b = interval 2925 return a < val < b or a > val > b 2926 2927 2928def offset_copy(trans, fig=None, x=0.0, y=0.0, units='inches'): 2929 """ 2930 Return a new transform with an added offset. 2931 2932 Parameters 2933 ---------- 2934 trans : `Transform` subclass 2935 Any transform, to which offset will be applied. 2936 fig : `~matplotlib.figure.Figure`, default: None 2937 Current figure. It can be None if *units* are 'dots'. 2938 x, y : float, default: 0.0 2939 The offset to apply. 2940 units : {'inches', 'points', 'dots'}, default: 'inches' 2941 Units of the offset. 2942 2943 Returns 2944 ------- 2945 `Transform` subclass 2946 Transform with applied offset. 2947 """ 2948 if units == 'dots': 2949 return trans + Affine2D().translate(x, y) 2950 if fig is None: 2951 raise ValueError('For units of inches or points a fig kwarg is needed') 2952 if units == 'points': 2953 x /= 72.0 2954 y /= 72.0 2955 elif units == 'inches': 2956 pass 2957 else: 2958 _api.check_in_list(['dots', 'points', 'inches'], units=units) 2959 return trans + ScaledTranslation(x, y, fig.dpi_scale_trans) 2960