Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/transforms.py : 28%

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