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