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)