Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/transforms.py: 32%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 :ref:`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 = _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))
99 # Possible values for the _invalid attribute.
100 _VALID, _INVALID_AFFINE_ONLY, _INVALID_FULL = range(3)
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))
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 """
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 ''
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)
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()}}
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}
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
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)
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)
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
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
211class BboxBase(TransformNode):
212 """
213 The base class of all bounding boxes.
215 This class is immutable; `Bbox` is a mutable subclass.
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 """
223 is_bbox = _api.deprecated("3.9")(_api.classproperty(lambda cls: True))
224 is_affine = True
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.")
235 def frozen(self):
236 return Bbox(self.get_points().copy())
237 frozen.__doc__ = TransformNode.__doc__
239 def __array__(self, *args, **kwargs):
240 return self.get_points()
242 @property
243 def x0(self):
244 """
245 The first of the pair of *x* coordinates that define the bounding box.
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]
252 @property
253 def y0(self):
254 """
255 The first of the pair of *y* coordinates that define the bounding box.
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]
262 @property
263 def x1(self):
264 """
265 The second of the pair of *x* coordinates that define the bounding box.
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]
272 @property
273 def y1(self):
274 """
275 The second of the pair of *y* coordinates that define the bounding box.
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]
282 @property
283 def p0(self):
284 """
285 The first pair of (*x*, *y*) coordinates that define the bounding box.
287 This is not guaranteed to be the bottom-left corner (for that, use
288 :attr:`min`).
289 """
290 return self.get_points()[0]
292 @property
293 def p1(self):
294 """
295 The second pair of (*x*, *y*) coordinates that define the bounding box.
297 This is not guaranteed to be the top-right corner (for that, use
298 :attr:`max`).
299 """
300 return self.get_points()[1]
302 @property
303 def xmin(self):
304 """The left edge of the bounding box."""
305 return np.min(self.get_points()[:, 0])
307 @property
308 def ymin(self):
309 """The bottom edge of the bounding box."""
310 return np.min(self.get_points()[:, 1])
312 @property
313 def xmax(self):
314 """The right edge of the bounding box."""
315 return np.max(self.get_points()[:, 0])
317 @property
318 def ymax(self):
319 """The top edge of the bounding box."""
320 return np.max(self.get_points()[:, 1])
322 @property
323 def min(self):
324 """The bottom-left corner of the bounding box."""
325 return np.min(self.get_points(), axis=0)
327 @property
328 def max(self):
329 """The top-right corner of the bounding box."""
330 return np.max(self.get_points(), axis=0)
332 @property
333 def intervalx(self):
334 """
335 The pair of *x* coordinates that define the bounding box.
337 This is not guaranteed to be sorted from left to right.
338 """
339 return self.get_points()[:, 0]
341 @property
342 def intervaly(self):
343 """
344 The pair of *y* coordinates that define the bounding box.
346 This is not guaranteed to be sorted from bottom to top.
347 """
348 return self.get_points()[:, 1]
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]
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]
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]
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)
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.
379 def get_points(self):
380 raise NotImplementedError
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
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
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)
402 def overlaps(self, other):
403 """
404 Return whether this bounding box overlaps with the other bounding box.
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
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
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
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)
442 def fully_overlaps(self, other):
443 """
444 Return whether this bounding box overlaps with the other bounding box,
445 not including the edges.
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
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]]])
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)}
482 def anchored(self, c, container=None):
483 """
484 Return a copy of the `Bbox` anchored to *c* within *container*.
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.
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])
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]])
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)])
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:])]
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:])]
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.
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())
585 def count_overlaps(self, bboxes):
586 """
587 Count the number of bounding boxes that overlap this one.
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]))
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)
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.
613 Parameters
614 ----------
615 w_pad : float
616 Width pad
617 h_pad : float, optional
618 Height pad. Defaults to *w_pad*.
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]])
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))
630 def corners(self):
631 """
632 Return the corners of this rectangle as an array of points.
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]])
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
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]])
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
675_default_minpos = np.array([np.inf, np.inf])
678class Bbox(BboxBase):
679 """
680 A mutable bounding box.
682 Examples
683 --------
684 **Create from known bounds**
686 The default constructor takes the boundary "points" ``[[xmin, ymin],
687 [xmax, ymax]]``.
689 >>> Bbox([[1, 1], [3, 7]])
690 Bbox([[1.0, 1.0], [3.0, 7.0]])
692 Alternatively, a Bbox can be created from the flattened points array, the
693 so-called "extents" ``(xmin, ymin, xmax, ymax)``
695 >>> Bbox.from_extents(1, 1, 3, 7)
696 Bbox([[1.0, 1.0], [3.0, 7.0]])
698 or from the "bounds" ``(xmin, ymin, width, height)``.
700 >>> Bbox.from_bounds(1, 1, 2, 6)
701 Bbox([[1.0, 1.0], [3.0, 7.0]])
703 **Create from collections of points**
705 The "empty" object for accumulating Bboxs is the null bbox, which is a
706 stand-in for the empty set.
708 >>> Bbox.null()
709 Bbox([[inf, inf], [-inf, -inf]])
711 Adding points to the null bbox will give you the bbox of those points.
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]])
721 Setting ``ignore=True`` is equivalent to starting over from a null bbox.
723 >>> box.update_from_data_xy([[1, 1]], ignore=True)
724 >>> box
725 Bbox([[1.0, 1.0], [1.0, 1.0]])
727 .. warning::
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`.
733 **Properties of the ``null`` bbox**
735 .. note::
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).
742 The null bbox is the identity for intersections
744 >>> Bbox.intersection(Bbox([[1, 1], [3, 7]]), Bbox.null())
745 Bbox([[1.0, 1.0], [3.0, 7.0]])
747 except with itself, where it returns the full space.
749 >>> Bbox.intersection(Bbox.null(), Bbox.null())
750 Bbox([[-inf, -inf], [inf, inf]])
752 A union containing null will always return the full space (not the other
753 set!)
755 >>> Bbox.union([Bbox([[0, 0], [0, 0]]), Bbox.null()])
756 Bbox([[-inf, -inf], [inf, inf]])
757 """
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__
781 def __init__(self, points, **kwargs):
782 self._check(points)
783 self.___init__(points, **kwargs)
785 def invalidate(self):
786 self._check(self._points)
787 super().invalidate()
789 def frozen(self):
790 # docstring inherited
791 frozen_bbox = super().frozen()
792 frozen_bbox._minpos = self.minpos.copy()
793 return frozen_bbox
795 @staticmethod
796 def unit():
797 """Create a new unit `Bbox` from (0, 0) to (1, 1)."""
798 return Bbox([[0, 0], [1, 1]])
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]])
805 @staticmethod
806 def from_bounds(x0, y0, width, height):
807 """
808 Create a new `Bbox` from *x0*, *y0*, *width* and *height*.
810 *width* and *height* may be negative.
811 """
812 return Bbox.from_extents(x0, y0, x0 + width, y0 + height)
814 @staticmethod
815 def from_extents(*args, minpos=None):
816 """
817 Create a new Bbox from *left*, *bottom*, *right* and *top*.
819 The *y*-axis increases upwards.
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
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))
840 def __str__(self):
841 return format(self, '')
843 def __repr__(self):
844 return 'Bbox([[{0.x0}, {0.y0}], [{0.x1}, {0.y1}]])'.format(self)
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`.
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
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.
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
878 if path.vertices.size == 0:
879 return
881 points, minpos, changed = update_path_extents(
882 path, None, self._points, self._minpos, ignore)
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]
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.
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)
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 : `~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)
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.
935 After updating, the bounds will have positive *width* and *height*;
936 *x0* and *y0* will be the minimal values.
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
952 path = Path(xy)
953 self.update_from_path(path, ignore=ignore,
954 updatex=updatex, updatey=updatey)
956 @BboxBase.x0.setter
957 def x0(self, val):
958 self._points[0, 0] = val
959 self.invalidate()
961 @BboxBase.y0.setter
962 def y0(self, val):
963 self._points[0, 1] = val
964 self.invalidate()
966 @BboxBase.x1.setter
967 def x1(self, val):
968 self._points[1, 0] = val
969 self.invalidate()
971 @BboxBase.y1.setter
972 def y1(self, val):
973 self._points[1, 1] = val
974 self.invalidate()
976 @BboxBase.p0.setter
977 def p0(self, val):
978 self._points[0] = val
979 self.invalidate()
981 @BboxBase.p1.setter
982 def p1(self, val):
983 self._points[1] = val
984 self.invalidate()
986 @BboxBase.intervalx.setter
987 def intervalx(self, interval):
988 self._points[:, 0] = interval
989 self.invalidate()
991 @BboxBase.intervaly.setter
992 def intervaly(self, interval):
993 self._points[:, 1] = interval
994 self.invalidate()
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()
1004 @property
1005 def minpos(self):
1006 """
1007 The minimum positive value in both directions within the Bbox.
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
1015 @minpos.setter
1016 def minpos(self, val):
1017 self._minpos[:] = val
1019 @property
1020 def minposx(self):
1021 """
1022 The minimum positive value in the *x*-direction within the Bbox.
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]
1030 @minposx.setter
1031 def minposx(self, val):
1032 self._minpos[0] = val
1034 @property
1035 def minposy(self):
1036 """
1037 The minimum positive value in the *y*-direction within the Bbox.
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]
1045 @minposy.setter
1046 def minposy(self, val):
1047 self._minpos[1] = val
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
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()
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()
1075 def mutated(self):
1076 """Return whether the bbox has changed since init."""
1077 return self.mutatedx() or self.mutatedy()
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])
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])
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 """
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")
1110 super().__init__(**kwargs)
1111 self._bbox = bbox
1112 self._transform = transform
1113 self.set_children(bbox, transform)
1114 self._points = None
1116 __str__ = _make_str_method("_bbox", "_transform")
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)
1132 xs = min(points[:, 0]), max(points[:, 0])
1133 if p[0, 0] > p[1, 0]:
1134 xs = xs[::-1]
1136 ys = min(points[:, 1]), max(points[:, 1])
1137 if p[0, 1] > p[1, 1]:
1138 ys = ys[::-1]
1140 self._points = np.array([
1141 [xs[0], ys[0]],
1142 [xs[1], ys[1]]
1143 ])
1145 self._invalid = 0
1146 return self._points
1148 if DEBUG:
1149 _get_points = get_points
1151 def get_points(self):
1152 points = self._get_points()
1153 self._check(points)
1154 return points
1156 def contains(self, x, y):
1157 # Docstring inherited.
1158 return self._bbox.contains(*self._transform.inverted().transform((x, y)))
1160 def fully_contains(self, x, y):
1161 # Docstring inherited.
1162 return self._bbox.fully_contains(*self._transform.inverted().transform((x, y)))
1165class LockableBbox(BboxBase):
1166 """
1167 A `Bbox` where some elements may be locked at certain values.
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.
1179 x0 : float or None
1180 The locked value for x0, or None to leave unlocked.
1182 y0 : float or None
1183 The locked value for y0, or None to leave unlocked.
1185 x1 : float or None
1186 The locked value for x1, or None to leave unlocked.
1188 y1 : float or None
1189 The locked value for y1, or None to leave unlocked.
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))
1201 __str__ = _make_str_method("_bbox", "_locked_points")
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
1213 if DEBUG:
1214 _get_points = get_points
1216 def get_points(self):
1217 points = self._get_points()
1218 self._check(points)
1219 return points
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]
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()
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]
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()
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]
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()
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]
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()
1286class Transform(TransformNode):
1287 """
1288 The base class of all `TransformNode` instances that
1289 actually perform a transformation.
1291 All non-affine transformations should be subclasses of this class.
1292 New affine transformations should be subclasses of `Affine2D`.
1294 Subclasses of this class should override the following members (at
1295 minimum):
1297 - :attr:`input_dims`
1298 - :attr:`output_dims`
1299 - :meth:`transform`
1300 - :meth:`inverted` (if an inverse exists)
1302 The following attributes may be overridden if the default is unsuitable:
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)
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:
1313 - :meth:`transform_path`
1314 """
1316 input_dims = None
1317 """
1318 The number of input dimensions of this transform.
1319 Must be overridden (with integers) in the subclass.
1320 """
1322 output_dims = None
1323 """
1324 The number of output dimensions of this transform.
1325 Must be overridden (with integers) in the subclass.
1326 """
1328 is_separable = False
1329 """True if this transform is separable in the x- and y- dimensions."""
1331 has_inverse = False
1332 """True if this transform has a corresponding inverse transform."""
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
1350 def __add__(self, other):
1351 """
1352 Compose two transforms together so that *self* is followed by *other*.
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)
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.
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.
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
1377 @property
1378 def depth(self):
1379 """
1380 Return the number of transforms which have been chained
1381 together to form this Transform instance.
1383 .. note::
1385 For the special case of a Composite transform, the maximum depth
1386 of the two is returned.
1388 """
1389 return 1
1391 def contains_branch(self, other):
1392 """
1393 Return whether the given transform is a sub-tree of this transform.
1395 This routine uses transform equality to identify sub-trees, therefore
1396 in many situations it is object id which will be used.
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
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
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.
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.::
1418 x_isdata, y_isdata = trans.contains_branch_seperately(ax.transData)
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
1428 def __sub__(self, other):
1429 """
1430 Compose *self* with the inverse of *other*, cancelling identical terms
1431 if any::
1433 # In general:
1434 A - B == A + B.inverted()
1435 # (but see note regarding frozen transforms below).
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'
1441 # Likewise, if B "starts with" A (B = A + B'), we can cancel out A:
1442 A - (A + B') == B'.inverted() == B'^-1
1444 Cancellation (rather than naively returning ``A + B.inverted()``) is
1445 important for multiple reasons:
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.')
1478 def __array__(self, *args, **kwargs):
1479 """Array interface to get at this Transform's affine matrix."""
1480 return self.get_affine().get_matrix()
1482 def transform(self, values):
1483 """
1484 Apply this transformation on the given array of *values*.
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`).
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))
1504 # Transform the values
1505 res = self.transform_affine(self.transform_non_affine(values))
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))
1519 def transform_affine(self, values):
1520 """
1521 Apply only the affine part of this transformation on the
1522 given array of values.
1524 ``transform(values)`` is always equivalent to
1525 ``transform_affine(transform_non_affine(values))``.
1527 In non-affine transformations, this is generally a no-op. In
1528 affine transformations, this is equivalent to
1529 ``transform(values)``.
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`).
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)
1545 def transform_non_affine(self, values):
1546 """
1547 Apply only the non-affine part of this transformation.
1549 ``transform(values)`` is always equivalent to
1550 ``transform_affine(transform_non_affine(values))``.
1552 In non-affine transformations, this is generally equivalent to
1553 ``transform(values)``. In affine transformations, this is
1554 always a no-op.
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`).
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
1570 def transform_bbox(self, bbox):
1571 """
1572 Transform the given bounding box.
1574 For smarter transforms including caching (a common requirement in
1575 Matplotlib), see `TransformedBbox`.
1576 """
1577 return Bbox(self.transform(bbox.get_points()))
1579 def get_affine(self):
1580 """Get the affine part of this transform."""
1581 return IdentityTransform()
1583 def get_matrix(self):
1584 """Get the matrix for the affine part of this transform."""
1585 return self.get_affine().get_matrix()
1587 def transform_point(self, point):
1588 """
1589 Return a transformed point.
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.
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)
1603 def transform_path(self, path):
1604 """
1605 Apply the transform to `.Path` *path*, returning a new `.Path`.
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))
1612 def transform_path_affine(self, path):
1613 """
1614 Apply the affine part of this transform to `.Path` *path*, returning a
1615 new `.Path`.
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)
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`.
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)
1633 def transform_angles(self, angles, pts, radians=False, pushoff=1e-5):
1634 """
1635 Transform a set of angles anchored at specific locations.
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.
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
1682 def inverted(self):
1683 """
1684 Return the corresponding inverse transformation.
1686 It holds ``x == self.inverted().transform(self.transform(x))``.
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()
1695class TransformWrapper(Transform):
1696 """
1697 A helper class that holds a single child transform and acts
1698 equivalently to it.
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.
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 """
1709 pass_through = True
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)
1720 def __eq__(self, other):
1721 return self._child.__eq__(other)
1723 __str__ = _make_str_method("_child")
1725 def frozen(self):
1726 # docstring inherited
1727 return self._child.frozen()
1729 def set(self, child):
1730 """
1731 Replace the current child of this transform with another one.
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)
1746 self._child = child
1747 self.set_children(child)
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.
1762 self._invalid = 0
1763 self.invalidate()
1764 self._invalid = 0
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)
1773class AffineBase(Transform):
1774 """
1775 The base class of all affine transformations of any number of dimensions.
1776 """
1777 is_affine = True
1779 def __init__(self, *args, **kwargs):
1780 super().__init__(*args, **kwargs)
1781 self._inverted = None
1783 def __array__(self, *args, **kwargs):
1784 # optimises the access of the transform matrix vs. the superclass
1785 return self.get_matrix()
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
1792 def transform(self, values):
1793 # docstring inherited
1794 return self.transform_affine(values)
1796 def transform_affine(self, values):
1797 # docstring inherited
1798 raise NotImplementedError('Affine subclasses should override this '
1799 'method.')
1801 @_api.rename_parameter("3.8", "points", "values")
1802 def transform_non_affine(self, values):
1803 # docstring inherited
1804 return values
1806 def transform_path(self, path):
1807 # docstring inherited
1808 return self.transform_path_affine(path)
1810 def transform_path_affine(self, path):
1811 # docstring inherited
1812 return Path(self.transform_affine(path.vertices),
1813 path.codes, path._interpolation_steps)
1815 def transform_path_non_affine(self, path):
1816 # docstring inherited
1817 return path
1819 def get_affine(self):
1820 # docstring inherited
1821 return self
1824class Affine2DBase(AffineBase):
1825 """
1826 The base class of all 2D affine transformations.
1828 2D affine transformations are performed using a 3x3 numpy array::
1830 a c e
1831 b d f
1832 0 0 1
1834 This class provides the read-only interface. For a mutable 2D
1835 affine transformation, use `Affine2D`.
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
1843 def frozen(self):
1844 # docstring inherited
1845 return Affine2D(self.get_matrix().copy())
1847 @property
1848 def is_separable(self):
1849 mtx = self.get_matrix()
1850 return mtx[0, 1] == mtx[1, 0] == 0.0
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)
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)
1867 if DEBUG:
1868 _transform_affine = transform_affine
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)
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
1894class Affine2D(Affine2DBase):
1895 """
1896 A mutable 2D affine transformation.
1897 """
1899 def __init__(self, matrix=None, **kwargs):
1900 """
1901 Initialize an Affine transform from a 3x3 numpy float array::
1903 a c e
1904 b d f
1905 0 0 1
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
1916 _base_str = _make_str_method("_mtx")
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]})")
1925 @staticmethod
1926 def from_values(a, b, c, d, e, f):
1927 """
1928 Create a new Affine2D instance from the given values::
1930 a c e
1931 b d f
1932 0 0 1
1934 .
1935 """
1936 return Affine2D(
1937 np.array([a, c, e, b, d, f, 0.0, 0.0, 1.0], float).reshape((3, 3)))
1939 def get_matrix(self):
1940 """
1941 Get the underlying transformation matrix as a 3x3 array::
1943 a c e
1944 b d f
1945 0 0 1
1947 .
1948 """
1949 if self._invalid:
1950 self._inverted = None
1951 self._invalid = 0
1952 return self._mtx
1954 def set_matrix(self, mtx):
1955 """
1956 Set the underlying transformation matrix from a 3x3 array::
1958 a c e
1959 b d f
1960 0 0 1
1962 .
1963 """
1964 self._mtx = mtx
1965 self.invalidate()
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()
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
1985 def rotate(self, theta):
1986 """
1987 Add a rotation (in radians) to this transform in place.
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
2008 def rotate_deg(self, degrees):
2009 """
2010 Add a rotation (in degrees) to this transform in place.
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))
2018 def rotate_around(self, x, y, theta):
2019 """
2020 Add a rotation (in radians) around the point (x, y) in place.
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)
2028 def rotate_deg_around(self, x, y, degrees):
2029 """
2030 Add a rotation (in degrees) around the point (x, y) in place.
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)
2040 def translate(self, tx, ty):
2041 """
2042 Add a translation in place.
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
2053 def scale(self, sx, sy=None):
2054 """
2055 Add a scale in place.
2057 If *sy* is None, the same scale is applied in both the *x*- and
2058 *y*-directions.
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
2076 def skew(self, xShear, yShear):
2077 """
2078 Add a skew in place.
2080 *xShear* and *yShear* are the shear angles along the *x*- and
2081 *y*-axes, respectively, in radians.
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
2102 def skew_deg(self, xShear, yShear):
2103 """
2104 Add a skew in place.
2106 *xShear* and *yShear* are the shear angles along the *x*- and
2107 *y*-axes, respectively, in degrees.
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))
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)
2123 def frozen(self):
2124 # docstring inherited
2125 return self
2127 __str__ = _make_str_method()
2129 def get_matrix(self):
2130 # docstring inherited
2131 return self._mtx
2133 @_api.rename_parameter("3.8", "points", "values")
2134 def transform(self, values):
2135 # docstring inherited
2136 return np.asanyarray(values)
2138 @_api.rename_parameter("3.8", "points", "values")
2139 def transform_affine(self, values):
2140 # docstring inherited
2141 return np.asanyarray(values)
2143 @_api.rename_parameter("3.8", "points", "values")
2144 def transform_non_affine(self, values):
2145 # docstring inherited
2146 return np.asanyarray(values)
2148 def transform_path(self, path):
2149 # docstring inherited
2150 return path
2152 def transform_path_affine(self, path):
2153 # docstring inherited
2154 return path
2156 def transform_path_non_affine(self, path):
2157 # docstring inherited
2158 return path
2160 def get_affine(self):
2161 # docstring inherited
2162 return self
2164 def inverted(self):
2165 # docstring inherited
2166 return self
2169class _BlendedMixin:
2170 """Common methods for `BlendedGenericTransform` and `BlendedAffine2D`."""
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
2180 def contains_branch_seperately(self, transform):
2181 return (self._x.contains_branch(transform),
2182 self._y.contains_branch(transform))
2184 __str__ = _make_str_method("_x", "_y")
2187class BlendedGenericTransform(_BlendedMixin, Transform):
2188 """
2189 A "blended" transform uses one transform for the *x*-direction, and
2190 another transform for the *y*-direction.
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
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.
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
2215 @property
2216 def depth(self):
2217 return max(self._x.depth, self._y.depth)
2219 def contains_branch(self, other):
2220 # A blended transform cannot possibly contain a branch from two
2221 # different transforms.
2222 return False
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)
2228 def frozen(self):
2229 # docstring inherited
2230 return blended_transform_factory(self._x.frozen(), self._y.frozen())
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
2240 if x == y and x.input_dims == 2:
2241 return x.transform_non_affine(values)
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))
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))
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)
2261 def inverted(self):
2262 # docstring inherited
2263 return BlendedGenericTransform(self._x.inverted(), self._y.inverted())
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
2281class BlendedAffine2D(_BlendedMixin, Affine2DBase):
2282 """
2283 A "blended" transform uses one transform for the *x*-direction, and
2284 another transform for the *y*-direction.
2286 This version is an optimization for the case where both child
2287 transforms are of type `Affine2DBase`.
2288 """
2290 is_separable = True
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.
2297 Both *x_transform* and *y_transform* must be 2D affine transforms.
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")
2310 Transform.__init__(self, **kwargs)
2311 self._x = x_transform
2312 self._y = y_transform
2313 self.set_children(x_transform, y_transform)
2315 Affine2DBase.__init__(self)
2316 self._mtx = None
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
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.
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)
2348class CompositeGenericTransform(Transform):
2349 """
2350 A composite transform formed by applying transform *a* then
2351 transform *b*.
2353 This "generic" version can handle any two arbitrary
2354 transformations.
2355 """
2356 pass_through = True
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*.
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
2373 super().__init__(**kwargs)
2374 self._a = a
2375 self._b = b
2376 self.set_children(a, b)
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
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)
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
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
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)
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)
2423 __str__ = _make_str_method("_a", "_b")
2425 @_api.rename_parameter("3.8", "points", "values")
2426 def transform_affine(self, values):
2427 # docstring inherited
2428 return self.get_affine().transform(values)
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))
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))
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()))
2458 def inverted(self):
2459 # docstring inherited
2460 return CompositeGenericTransform(
2461 self._b.inverted(), self._a.inverted())
2464class CompositeAffine2D(Affine2DBase):
2465 """
2466 A composite transform formed by applying transform *a* then transform *b*.
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*.
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
2488 super().__init__(**kwargs)
2489 self._a = a
2490 self._b = b
2491 self.set_children(a, b)
2492 self._mtx = None
2494 @property
2495 def depth(self):
2496 return self._a.depth + self._b.depth
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
2504 __str__ = _make_str_method("_a", "_b")
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
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.
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.
2526 Composite transforms may also be created using the '+' operator,
2527 e.g.::
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)
2544class BboxTransform(Affine2DBase):
2545 """
2546 `BboxTransform` linearly transforms points from one `Bbox` to another.
2547 """
2549 is_separable = True
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)
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
2565 __str__ = _make_str_method("_boxin", "_boxout")
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
2586class BboxTransformTo(Affine2DBase):
2587 """
2588 `BboxTransformTo` is a transformation that linearly transforms points from
2589 the unit bounding box to a given `Bbox`.
2590 """
2592 is_separable = True
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)
2601 super().__init__(**kwargs)
2602 self._boxout = boxout
2603 self.set_children(boxout)
2604 self._mtx = None
2605 self._inverted = None
2607 __str__ = _make_str_method("_boxout")
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
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
2645class BboxTransformFrom(Affine2DBase):
2646 """
2647 `BboxTransformFrom` linearly transforms points from a given `Bbox` to the
2648 unit bounding box.
2649 """
2650 is_separable = True
2652 def __init__(self, boxin, **kwargs):
2653 _api.check_isinstance(BboxBase, boxin=boxin)
2655 super().__init__(**kwargs)
2656 self._boxin = boxin
2657 self.set_children(boxin)
2658 self._mtx = None
2659 self._inverted = None
2661 __str__ = _make_str_method("_boxin")
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
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
2693 __str__ = _make_str_method("_t")
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
2706class AffineDeltaTransform(Affine2DBase):
2707 r"""
2708 A transform wrapper for transforming displacements between pairs of points.
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)``.
2717 This is implemented by forcing the offset components of the transform
2718 matrix to zero.
2720 This class is experimental as of 3.3, and the API may change.
2721 """
2723 def __init__(self, transform, **kwargs):
2724 super().__init__(**kwargs)
2725 self._base_transform = transform
2727 __str__ = _make_str_method("_base_transform")
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
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.
2742 .. note::
2744 Paths are considered immutable by this class. Any update to the
2745 path's vertices/codes will not trigger a transform recomputation.
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
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
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()
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()
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)
2803 def get_affine(self):
2804 return self._transform.get_affine()
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 """
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
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()
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.
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*.
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 """
2862 if (not np.isfinite(vmin)) or (not np.isfinite(vmax)):
2863 return -expander, expander
2865 swapped = False
2866 if vmax < vmin:
2867 vmin, vmax = vmax, vmin
2868 swapped = True
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])
2874 maxabsvalue = max(abs(vmin), abs(vmax))
2875 if maxabsvalue < (1e6 / tiny) * np.finfo(float).tiny:
2876 vmin = -expander
2877 vmax = expander
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)
2887 if swapped and not increasing:
2888 vmin, vmax = vmax, vmin
2889 return vmin, vmax
2892def interval_contains(interval, val):
2893 """
2894 Check, inclusively, whether an interval includes a given value.
2896 Parameters
2897 ----------
2898 interval : (float, float)
2899 The endpoints of the interval.
2900 val : float
2901 Value to check is within interval.
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
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.
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.
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
2943def interval_contains_open(interval, val):
2944 """
2945 Check, excluding endpoints, whether an interval includes a given value.
2947 Parameters
2948 ----------
2949 interval : (float, float)
2950 The endpoints of the interval.
2951 val : float
2952 Value to check is within interval.
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
2963def offset_copy(trans, fig=None, x=0.0, y=0.0, units='inches'):
2964 """
2965 Return a new transform with an added offset.
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.
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)