123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937 |
- """
- matplotlib includes a framework for arbitrary geometric
- transformations that is used determine the final position of all
- elements drawn on the canvas.
- Transforms are composed into trees of `TransformNode` objects
- whose actual value depends on their children. When the contents of
- children change, their parents are automatically invalidated. The
- next time an invalidated transform is accessed, it is recomputed to
- reflect those changes. This invalidation/caching approach prevents
- unnecessary recomputations of transforms, and contributes to better
- interactive performance.
- For example, here is a graph of the transform tree used to plot data
- to the graph:
- .. image:: ../_static/transforms.png
- The framework can be used for both affine and non-affine
- transformations. However, for speed, we want use the backend
- renderers to perform affine transformations whenever possible.
- Therefore, it is possible to perform just the affine or non-affine
- part of a transformation on a set of data. The affine is always
- assumed to occur after the non-affine. For any transform::
- full transform == non-affine part + affine part
- The backends are not expected to handle non-affine transformations
- themselves.
- """
- # Note: There are a number of places in the code where we use `np.min` or
- # `np.minimum` instead of the builtin `min`, and likewise for `max`. This is
- # done so that `nan`s are propagated, instead of being silently dropped.
- import re
- import weakref
- import numpy as np
- from numpy.linalg import inv
- from matplotlib import cbook
- from matplotlib._path import (
- affine_transform, count_bboxes_overlapping_bbox, update_path_extents)
- from .path import Path
- DEBUG = False
- def _indent_str(obj): # textwrap.indent(str(obj), 4) on Py3.
- return re.sub("(^|\n)", r"\1 ", str(obj))
- class TransformNode:
- """
- :class:`TransformNode` is the base class for anything that
- participates in the transform tree and needs to invalidate its
- parents or be invalidated. This includes classes that are not
- really transforms, such as bounding boxes, since some transforms
- depend on bounding boxes to compute their values.
- """
- _gid = 0
- # Invalidation may affect only the affine part. If the
- # invalidation was "affine-only", the _invalid member is set to
- # INVALID_AFFINE_ONLY
- INVALID_NON_AFFINE = 1
- INVALID_AFFINE = 2
- INVALID = INVALID_NON_AFFINE | INVALID_AFFINE
- # Some metadata about the transform, used to determine whether an
- # invalidation is affine-only
- is_affine = False
- is_bbox = False
- pass_through = False
- """
- If pass_through is True, all ancestors will always be
- invalidated, even if 'self' is already invalid.
- """
- def __init__(self, shorthand_name=None):
- """
- Creates a new :class:`TransformNode`.
- Parameters
- ----------
- shorthand_name : str
- A string representing the "name" of the transform. The name carries
- no significance other than to improve the readability of
- ``str(transform)`` when DEBUG=True.
- """
- self._parents = {}
- # TransformNodes start out as invalid until their values are
- # computed for the first time.
- self._invalid = 1
- self._shorthand_name = shorthand_name or ''
- if DEBUG:
- def __str__(self):
- # either just return the name of this TransformNode, or its repr
- return self._shorthand_name or repr(self)
- def __getstate__(self):
- # turn the dictionary with weak values into a normal dictionary
- return {**self.__dict__,
- '_parents': {k: v() for k, v in self._parents.items()}}
- def __setstate__(self, data_dict):
- self.__dict__ = data_dict
- # turn the normal dictionary back into a dictionary with weak values
- # The extra lambda is to provide a callback to remove dead
- # weakrefs from the dictionary when garbage collection is done.
- self._parents = {k: weakref.ref(v, lambda ref, sid=k,
- target=self._parents:
- target.pop(sid))
- for k, v in self._parents.items() if v is not None}
- def __copy__(self, *args):
- raise NotImplementedError(
- "TransformNode instances can not be copied. "
- "Consider using frozen() instead.")
- __deepcopy__ = __copy__
- def invalidate(self):
- """
- Invalidate this `TransformNode` and triggers an invalidation of its
- ancestors. Should be called any time the transform changes.
- """
- value = self.INVALID
- if self.is_affine:
- value = self.INVALID_AFFINE
- return self._invalidate_internal(value, invalidating_node=self)
- def _invalidate_internal(self, value, invalidating_node):
- """
- Called by :meth:`invalidate` and subsequently ascends the transform
- stack calling each TransformNode's _invalidate_internal method.
- """
- # determine if this call will be an extension to the invalidation
- # status. If not, then a shortcut means that we needn't invoke an
- # invalidation up the transform stack as it will already have been
- # invalidated.
- # N.B This makes the invalidation sticky, once a transform has been
- # invalidated as NON_AFFINE, then it will always be invalidated as
- # NON_AFFINE even when triggered with a AFFINE_ONLY invalidation.
- # In most cases this is not a problem (i.e. for interactive panning and
- # zooming) and the only side effect will be on performance.
- status_changed = self._invalid < value
- if self.pass_through or status_changed:
- self._invalid = value
- for parent in list(self._parents.values()):
- # Dereference the weak reference
- parent = parent()
- if parent is not None:
- parent._invalidate_internal(
- value=value, invalidating_node=self)
- def set_children(self, *children):
- """
- Set the children of the transform, to let the invalidation
- system know which transforms can invalidate this transform.
- Should be called from the constructor of any transforms that
- depend on other transforms.
- """
- # Parents are stored as weak references, so that if the
- # parents are destroyed, references from the children won't
- # keep them alive.
- for child in children:
- # Use weak references so this dictionary won't keep obsolete nodes
- # alive; the callback deletes the dictionary entry. This is a
- # performance improvement over using WeakValueDictionary.
- ref = weakref.ref(self, lambda ref, sid=id(self),
- target=child._parents: target.pop(sid))
- child._parents[id(self)] = ref
- if DEBUG:
- _set_children = set_children
- def set_children(self, *children):
- self._set_children(*children)
- self._children = children
- set_children.__doc__ = _set_children.__doc__
- def frozen(self):
- """
- Returns a frozen copy of this transform node. The frozen copy
- will not update when its children change. Useful for storing
- a previously known state of a transform where
- ``copy.deepcopy()`` might normally be used.
- """
- return self
- if DEBUG:
- def write_graphviz(self, fobj, highlight=[]):
- """
- For debugging purposes.
- Writes the transform tree rooted at 'self' to a graphviz "dot"
- format file. This file can be run through the "dot" utility
- to produce a graph of the transform tree.
- Affine transforms are marked in blue. Bounding boxes are
- marked in yellow.
- *fobj*: A Python file-like object
- Once the "dot" file has been created, it can be turned into a
- png easily with::
- $> dot -Tpng -o $OUTPUT_FILE $DOT_FILE
- """
- seen = set()
- def recurse(root):
- if root in seen:
- return
- seen.add(root)
- props = {}
- label = root.__class__.__name__
- if root._invalid:
- label = '[%s]' % label
- if root in highlight:
- props['style'] = 'bold'
- props['shape'] = 'box'
- props['label'] = '"%s"' % label
- props = ' '.join(map('{0[0]}={0[1]}'.format, props.items()))
- fobj.write('%s [%s];\n' % (hash(root), props))
- if hasattr(root, '_children'):
- for child in root._children:
- name = next((key for key, val in root.__dict__.items()
- if val is child), '?')
- fobj.write('"%s" -> "%s" [label="%s", fontsize=10];\n'
- % (hash(root),
- hash(child),
- name))
- recurse(child)
- fobj.write("digraph G {\n")
- recurse(self)
- fobj.write("}\n")
- class BboxBase(TransformNode):
- """
- This is the base class of all bounding boxes, and provides read-only access
- to its data. A mutable bounding box is provided by the `Bbox` class.
- The canonical representation is as two points, with no
- restrictions on their ordering. Convenience properties are
- provided to get the left, bottom, right and top edges and width
- and height, but these are not stored explicitly.
- """
- is_bbox = True
- is_affine = True
- if DEBUG:
- @staticmethod
- def _check(points):
- if isinstance(points, np.ma.MaskedArray):
- cbook._warn_external("Bbox bounds are a masked array.")
- points = np.asarray(points)
- if (points[1, 0] - points[0, 0] == 0 or
- points[1, 1] - points[0, 1] == 0):
- cbook._warn_external("Singular Bbox.")
- def frozen(self):
- return Bbox(self.get_points().copy())
- frozen.__doc__ = TransformNode.__doc__
- def __array__(self, *args, **kwargs):
- return self.get_points()
- @cbook.deprecated("3.2")
- def is_unit(self):
- """Return whether this is the unit box (from (0, 0) to (1, 1))."""
- return self.get_points().tolist() == [[0., 0.], [1., 1.]]
- @property
- def x0(self):
- """
- The first of the pair of *x* coordinates that define the bounding box.
- This is not guaranteed to be less than :attr:`x1` (for that, use
- :attr:`xmin`).
- """
- return self.get_points()[0, 0]
- @property
- def y0(self):
- """
- The first of the pair of *y* coordinates that define the bounding box.
- This is not guaranteed to be less than :attr:`y1` (for that, use
- :attr:`ymin`).
- """
- return self.get_points()[0, 1]
- @property
- def x1(self):
- """
- The second of the pair of *x* coordinates that define the bounding box.
- This is not guaranteed to be greater than :attr:`x0` (for that, use
- :attr:`xmax`).
- """
- return self.get_points()[1, 0]
- @property
- def y1(self):
- """
- The second of the pair of *y* coordinates that define the bounding box.
- This is not guaranteed to be greater than :attr:`y0` (for that, use
- :attr:`ymax`).
- """
- return self.get_points()[1, 1]
- @property
- def p0(self):
- """
- The first pair of (*x*, *y*) coordinates that define the bounding box.
- This is not guaranteed to be the bottom-left corner (for that, use
- :attr:`min`).
- """
- return self.get_points()[0]
- @property
- def p1(self):
- """
- The second pair of (*x*, *y*) coordinates that define the bounding box.
- This is not guaranteed to be the top-right corner (for that, use
- :attr:`max`).
- """
- return self.get_points()[1]
- @property
- def xmin(self):
- """The left edge of the bounding box."""
- return np.min(self.get_points()[:, 0])
- @property
- def ymin(self):
- """The bottom edge of the bounding box."""
- return np.min(self.get_points()[:, 1])
- @property
- def xmax(self):
- """The right edge of the bounding box."""
- return np.max(self.get_points()[:, 0])
- @property
- def ymax(self):
- """The top edge of the bounding box."""
- return np.max(self.get_points()[:, 1])
- @property
- def min(self):
- """The bottom-left corner of the bounding box."""
- return np.min(self.get_points(), axis=0)
- @property
- def max(self):
- """The top-right corner of the bounding box."""
- return np.max(self.get_points(), axis=0)
- @property
- def intervalx(self):
- """
- The pair of *x* coordinates that define the bounding box.
- This is not guaranteed to be sorted from left to right.
- """
- return self.get_points()[:, 0]
- @property
- def intervaly(self):
- """
- The pair of *y* coordinates that define the bounding box.
- This is not guaranteed to be sorted from bottom to top.
- """
- return self.get_points()[:, 1]
- @property
- def width(self):
- """The (signed) width of the bounding box."""
- points = self.get_points()
- return points[1, 0] - points[0, 0]
- @property
- def height(self):
- """The (signed) height of the bounding box."""
- points = self.get_points()
- return points[1, 1] - points[0, 1]
- @property
- def size(self):
- """The (signed) width and height of the bounding box."""
- points = self.get_points()
- return points[1] - points[0]
- @property
- def bounds(self):
- """Return (:attr:`x0`, :attr:`y0`, :attr:`width`, :attr:`height`)."""
- (x0, y0), (x1, y1) = self.get_points()
- return (x0, y0, x1 - x0, y1 - y0)
- @property
- def extents(self):
- """Return (:attr:`x0`, :attr:`y0`, :attr:`x1`, :attr:`y1`)."""
- return self.get_points().flatten() # flatten returns a copy.
- def get_points(self):
- raise NotImplementedError
- def containsx(self, x):
- """
- Return whether *x* is in the closed (:attr:`x0`, :attr:`x1`) interval.
- """
- x0, x1 = self.intervalx
- return x0 <= x <= x1 or x0 >= x >= x1
- def containsy(self, y):
- """
- Return whether *y* is in the closed (:attr:`y0`, :attr:`y1`) interval.
- """
- y0, y1 = self.intervaly
- return y0 <= y <= y1 or y0 >= y >= y1
- def contains(self, x, y):
- """
- Return whether ``(x, y)`` is in the bounding box or on its edge.
- """
- return self.containsx(x) and self.containsy(y)
- def overlaps(self, other):
- """
- Return whether this bounding box overlaps with the other bounding box.
- Parameters
- ----------
- other : `.BboxBase`
- """
- ax1, ay1, ax2, ay2 = self.extents
- bx1, by1, bx2, by2 = other.extents
- if ax2 < ax1:
- ax2, ax1 = ax1, ax2
- if ay2 < ay1:
- ay2, ay1 = ay1, ay2
- if bx2 < bx1:
- bx2, bx1 = bx1, bx2
- if by2 < by1:
- by2, by1 = by1, by2
- return ax1 <= bx2 and bx1 <= ax2 and ay1 <= by2 and by1 <= ay2
- def fully_containsx(self, x):
- """
- Return whether *x* is in the open (:attr:`x0`, :attr:`x1`) interval.
- """
- x0, x1 = self.intervalx
- return x0 < x < x1 or x0 > x > x1
- def fully_containsy(self, y):
- """
- Return whether *y* is in the open (:attr:`y0`, :attr:`y1`) interval.
- """
- y0, y1 = self.intervaly
- return y0 < y < y1 or y0 > y > y1
- def fully_contains(self, x, y):
- """
- Return whether ``x, y`` is in the bounding box, but not on its edge.
- """
- return self.fully_containsx(x) and self.fully_containsy(y)
- def fully_overlaps(self, other):
- """
- Return whether this bounding box overlaps with the other bounding box,
- not including the edges.
- Parameters
- ----------
- other : `.BboxBase`
- """
- ax1, ay1, ax2, ay2 = self.extents
- bx1, by1, bx2, by2 = other.extents
- if ax2 < ax1:
- ax2, ax1 = ax1, ax2
- if ay2 < ay1:
- ay2, ay1 = ay1, ay2
- if bx2 < bx1:
- bx2, bx1 = bx1, bx2
- if by2 < by1:
- by2, by1 = by1, by2
- return ax1 < bx2 and bx1 < ax2 and ay1 < by2 and by1 < ay2
- def transformed(self, transform):
- """
- Construct a `Bbox` by statically transforming this one by *transform*.
- """
- pts = self.get_points()
- ll, ul, lr = transform.transform(np.array([pts[0],
- [pts[0, 0], pts[1, 1]], [pts[1, 0], pts[0, 1]]]))
- return Bbox([ll, [lr[0], ul[1]]])
- def inverse_transformed(self, transform):
- """
- Construct a `Bbox` by statically transforming this one by the inverse
- of *transform*.
- """
- return self.transformed(transform.inverted())
- coefs = {'C': (0.5, 0.5),
- 'SW': (0, 0),
- 'S': (0.5, 0),
- 'SE': (1.0, 0),
- 'E': (1.0, 0.5),
- 'NE': (1.0, 1.0),
- 'N': (0.5, 1.0),
- 'NW': (0, 1.0),
- 'W': (0, 0.5)}
- def anchored(self, c, container=None):
- """
- Return a copy of the `Bbox` shifted to position *c* within *container*.
- Parameters
- ----------
- c : (float, float) or str
- May be either:
- * A sequence (*cx*, *cy*) where *cx* and *cy* range from 0
- to 1, where 0 is left or bottom and 1 is right or top
- * a string:
- - 'C' for centered
- - 'S' for bottom-center
- - 'SE' for bottom-left
- - 'E' for left
- - etc.
- container : Bbox, optional
- The box within which the :class:`Bbox` is positioned; it defaults
- to the initial :class:`Bbox`.
- """
- if container is None:
- container = self
- l, b, w, h = container.bounds
- if isinstance(c, str):
- cx, cy = self.coefs[c]
- else:
- cx, cy = c
- L, B, W, H = self.bounds
- return Bbox(self._points +
- [(l + cx * (w - W)) - L,
- (b + cy * (h - H)) - B])
- def shrunk(self, mx, my):
- """
- Return a copy of the :class:`Bbox`, shrunk by the factor *mx*
- in the *x* direction and the factor *my* in the *y* direction.
- The lower left corner of the box remains unchanged. Normally
- *mx* and *my* will be less than 1, but this is not enforced.
- """
- w, h = self.size
- return Bbox([self._points[0],
- self._points[0] + [mx * w, my * h]])
- def shrunk_to_aspect(self, box_aspect, container=None, fig_aspect=1.0):
- """
- Return a copy of the :class:`Bbox`, shrunk so that it is as
- large as it can be while having the desired aspect ratio,
- *box_aspect*. If the box coordinates are relative---that
- is, fractions of a larger box such as a figure---then the
- physical aspect ratio of that figure is specified with
- *fig_aspect*, so that *box_aspect* can also be given as a
- ratio of the absolute dimensions, not the relative dimensions.
- """
- if box_aspect <= 0 or fig_aspect <= 0:
- raise ValueError("'box_aspect' and 'fig_aspect' must be positive")
- if container is None:
- container = self
- w, h = container.size
- H = w * box_aspect / fig_aspect
- if H <= h:
- W = w
- else:
- W = h * fig_aspect / box_aspect
- H = h
- return Bbox([self._points[0],
- self._points[0] + (W, H)])
- def splitx(self, *args):
- """
- Return a list of new `Bbox` objects formed by splitting the original
- one with vertical lines at fractional positions given by *args*.
- """
- xf = [0, *args, 1]
- x0, y0, x1, y1 = self.extents
- w = x1 - x0
- return [Bbox([[x0 + xf0 * w, y0], [x0 + xf1 * w, y1]])
- for xf0, xf1 in zip(xf[:-1], xf[1:])]
- def splity(self, *args):
- """
- Return a list of new `Bbox` objects formed by splitting the original
- one with horizontal lines at fractional positions given by *args*.
- """
- yf = [0, *args, 1]
- x0, y0, x1, y1 = self.extents
- h = y1 - y0
- return [Bbox([[x0, y0 + yf0 * h], [x1, y0 + yf1 * h]])
- for yf0, yf1 in zip(yf[:-1], yf[1:])]
- def count_contains(self, vertices):
- """
- Count the number of vertices contained in the :class:`Bbox`.
- Any vertices with a non-finite x or y value are ignored.
- Parameters
- ----------
- vertices : Nx2 Numpy array.
- """
- if len(vertices) == 0:
- return 0
- vertices = np.asarray(vertices)
- with np.errstate(invalid='ignore'):
- return (((self.min < vertices) &
- (vertices < self.max)).all(axis=1).sum())
- def count_overlaps(self, bboxes):
- """
- Count the number of bounding boxes that overlap this one.
- Parameters
- ----------
- bboxes : sequence of `.BboxBase`
- """
- return count_bboxes_overlapping_bbox(
- self, np.atleast_3d([np.array(x) for x in bboxes]))
- def expanded(self, sw, sh):
- """
- Construct a `Bbox` by expanding this one around its center by the
- factors *sw* and *sh*.
- """
- width = self.width
- height = self.height
- deltaw = (sw * width - width) / 2.0
- deltah = (sh * height - height) / 2.0
- a = np.array([[-deltaw, -deltah], [deltaw, deltah]])
- return Bbox(self._points + a)
- def padded(self, p):
- """Construct a `Bbox` by padding this one on all four sides by *p*."""
- points = self.get_points()
- return Bbox(points + [[-p, -p], [p, p]])
- def translated(self, tx, ty):
- """Construct a `Bbox` by translating this one by *tx* and *ty*."""
- return Bbox(self._points + (tx, ty))
- def corners(self):
- """
- Return the corners of this rectangle as an array of points.
- Specifically, this returns the array
- ``[[x0, y0], [x0, y1], [x1, y0], [x1, y1]]``.
- """
- (x0, y0), (x1, y1) = self.get_points()
- return np.array([[x0, y0], [x0, y1], [x1, y0], [x1, y1]])
- def rotated(self, radians):
- """
- Return a new bounding box that bounds a rotated version of
- this bounding box by the given radians. The new bounding box
- is still aligned with the axes, of course.
- """
- corners = self.corners()
- corners_rotated = Affine2D().rotate(radians).transform(corners)
- bbox = Bbox.unit()
- bbox.update_from_data_xy(corners_rotated, ignore=True)
- return bbox
- @staticmethod
- def union(bboxes):
- """Return a `Bbox` that contains all of the given *bboxes*."""
- if not len(bboxes):
- raise ValueError("'bboxes' cannot be empty")
- # needed for 1.14.4 < numpy_version < 1.15
- # can remove once we are at numpy >= 1.15
- with np.errstate(invalid='ignore'):
- x0 = np.min([bbox.xmin for bbox in bboxes])
- x1 = np.max([bbox.xmax for bbox in bboxes])
- y0 = np.min([bbox.ymin for bbox in bboxes])
- y1 = np.max([bbox.ymax for bbox in bboxes])
- return Bbox([[x0, y0], [x1, y1]])
- @staticmethod
- def intersection(bbox1, bbox2):
- """
- Return the intersection of *bbox1* and *bbox2* if they intersect, or
- None if they don't.
- """
- x0 = np.maximum(bbox1.xmin, bbox2.xmin)
- x1 = np.minimum(bbox1.xmax, bbox2.xmax)
- y0 = np.maximum(bbox1.ymin, bbox2.ymin)
- y1 = np.minimum(bbox1.ymax, bbox2.ymax)
- return Bbox([[x0, y0], [x1, y1]]) if x0 <= x1 and y0 <= y1 else None
- class Bbox(BboxBase):
- """
- A mutable bounding box.
- """
- def __init__(self, points, **kwargs):
- """
- Parameters
- ----------
- points : ndarray
- A 2x2 numpy array of the form ``[[x0, y0], [x1, y1]]``.
- Notes
- -----
- If you need to create a :class:`Bbox` object from another form
- of data, consider the static methods :meth:`unit`,
- :meth:`from_bounds` and :meth:`from_extents`.
- """
- BboxBase.__init__(self, **kwargs)
- points = np.asarray(points, float)
- if points.shape != (2, 2):
- raise ValueError('Bbox points must be of the form '
- '"[[x0, y0], [x1, y1]]".')
- self._points = points
- self._minpos = np.array([np.inf, np.inf])
- self._ignore = True
- # it is helpful in some contexts to know if the bbox is a
- # default or has been mutated; we store the orig points to
- # support the mutated methods
- self._points_orig = self._points.copy()
- if DEBUG:
- ___init__ = __init__
- def __init__(self, points, **kwargs):
- self._check(points)
- self.___init__(points, **kwargs)
- def invalidate(self):
- self._check(self._points)
- TransformNode.invalidate(self)
- @staticmethod
- def unit():
- """Create a new unit `Bbox` from (0, 0) to (1, 1)."""
- return Bbox(np.array([[0.0, 0.0], [1.0, 1.0]], float))
- @staticmethod
- def null():
- """Create a new null `Bbox` from (inf, inf) to (-inf, -inf)."""
- return Bbox(np.array([[np.inf, np.inf], [-np.inf, -np.inf]], float))
- @staticmethod
- def from_bounds(x0, y0, width, height):
- """
- Create a new `Bbox` from *x0*, *y0*, *width* and *height*.
- *width* and *height* may be negative.
- """
- return Bbox.from_extents(x0, y0, x0 + width, y0 + height)
- @staticmethod
- def from_extents(*args):
- """
- Create a new Bbox from *left*, *bottom*, *right* and *top*.
- The *y*-axis increases upwards.
- """
- points = np.array(args, dtype=float).reshape(2, 2)
- return Bbox(points)
- def __format__(self, fmt):
- return (
- 'Bbox(x0={0.x0:{1}}, y0={0.y0:{1}}, x1={0.x1:{1}}, y1={0.y1:{1}})'.
- format(self, fmt))
- def __str__(self):
- return format(self, '')
- def __repr__(self):
- return 'Bbox([[{0.x0}, {0.y0}], [{0.x1}, {0.y1}]])'.format(self)
- def ignore(self, value):
- """
- Set whether the existing bounds of the box should be ignored
- by subsequent calls to :meth:`update_from_data_xy`.
- value : bool
- - When ``True``, subsequent calls to :meth:`update_from_data_xy`
- will ignore the existing bounds of the :class:`Bbox`.
- - When ``False``, subsequent calls to :meth:`update_from_data_xy`
- will include the existing bounds of the :class:`Bbox`.
- """
- self._ignore = value
- def update_from_path(self, path, ignore=None, updatex=True, updatey=True):
- """
- Update the bounds of the :class:`Bbox` based on the passed in
- data. After updating, the bounds will have positive *width*
- and *height*; *x0* and *y0* will be the minimal values.
- Parameters
- ----------
- path : :class:`~matplotlib.path.Path`
- ignore : bool, optional
- - when ``True``, ignore the existing bounds of the :class:`Bbox`.
- - when ``False``, include the existing bounds of the :class:`Bbox`.
- - when ``None``, use the last value passed to :meth:`ignore`.
- updatex, updatey : bool, optional
- When ``True``, update the x/y values.
- """
- if ignore is None:
- ignore = self._ignore
- if path.vertices.size == 0:
- return
- points, minpos, changed = update_path_extents(
- path, None, self._points, self._minpos, ignore)
- if changed:
- self.invalidate()
- if updatex:
- self._points[:, 0] = points[:, 0]
- self._minpos[0] = minpos[0]
- if updatey:
- self._points[:, 1] = points[:, 1]
- self._minpos[1] = minpos[1]
- def update_from_data_xy(self, xy, ignore=None, updatex=True, updatey=True):
- """
- Update the bounds of the :class:`Bbox` based on the passed in
- data. After updating, the bounds will have positive *width*
- and *height*; *x0* and *y0* will be the minimal values.
- Parameters
- ----------
- xy : ndarray
- A numpy array of 2D points.
- ignore : bool, optional
- - When ``True``, ignore the existing bounds of the :class:`Bbox`.
- - When ``False``, include the existing bounds of the :class:`Bbox`.
- - When ``None``, use the last value passed to :meth:`ignore`.
- updatex, updatey : bool, optional
- When ``True``, update the x/y values.
- """
- if len(xy) == 0:
- return
- path = Path(xy)
- self.update_from_path(path, ignore=ignore,
- updatex=updatex, updatey=updatey)
- @BboxBase.x0.setter
- def x0(self, val):
- self._points[0, 0] = val
- self.invalidate()
- @BboxBase.y0.setter
- def y0(self, val):
- self._points[0, 1] = val
- self.invalidate()
- @BboxBase.x1.setter
- def x1(self, val):
- self._points[1, 0] = val
- self.invalidate()
- @BboxBase.y1.setter
- def y1(self, val):
- self._points[1, 1] = val
- self.invalidate()
- @BboxBase.p0.setter
- def p0(self, val):
- self._points[0] = val
- self.invalidate()
- @BboxBase.p1.setter
- def p1(self, val):
- self._points[1] = val
- self.invalidate()
- @BboxBase.intervalx.setter
- def intervalx(self, interval):
- self._points[:, 0] = interval
- self.invalidate()
- @BboxBase.intervaly.setter
- def intervaly(self, interval):
- self._points[:, 1] = interval
- self.invalidate()
- @BboxBase.bounds.setter
- def bounds(self, bounds):
- l, b, w, h = bounds
- points = np.array([[l, b], [l + w, b + h]], float)
- if np.any(self._points != points):
- self._points = points
- self.invalidate()
- @property
- def minpos(self):
- return self._minpos
- @property
- def minposx(self):
- return self._minpos[0]
- @property
- def minposy(self):
- return self._minpos[1]
- def get_points(self):
- """
- Get the points of the bounding box directly as a numpy array
- of the form: ``[[x0, y0], [x1, y1]]``.
- """
- self._invalid = 0
- return self._points
- def set_points(self, points):
- """
- Set the points of the bounding box directly from a numpy array
- of the form: ``[[x0, y0], [x1, y1]]``. No error checking is
- performed, as this method is mainly for internal use.
- """
- if np.any(self._points != points):
- self._points = points
- self.invalidate()
- def set(self, other):
- """
- Set this bounding box from the "frozen" bounds of another `Bbox`.
- """
- if np.any(self._points != other.get_points()):
- self._points = other.get_points()
- self.invalidate()
- def mutated(self):
- 'Return whether the bbox has changed since init.'
- return self.mutatedx() or self.mutatedy()
- def mutatedx(self):
- 'Return whether the x-limits have changed since init.'
- return (self._points[0, 0] != self._points_orig[0, 0] or
- self._points[1, 0] != self._points_orig[1, 0])
- def mutatedy(self):
- 'Return whether the y-limits have changed since init.'
- return (self._points[0, 1] != self._points_orig[0, 1] or
- self._points[1, 1] != self._points_orig[1, 1])
- class TransformedBbox(BboxBase):
- """
- A :class:`Bbox` that is automatically transformed by a given
- transform. When either the child bounding box or transform
- changes, the bounds of this bbox will update accordingly.
- """
- def __init__(self, bbox, transform, **kwargs):
- """
- Parameters
- ----------
- bbox : :class:`Bbox`
- transform : :class:`Transform`
- """
- if not bbox.is_bbox:
- raise ValueError("'bbox' is not a bbox")
- cbook._check_isinstance(Transform, transform=transform)
- if transform.input_dims != 2 or transform.output_dims != 2:
- raise ValueError(
- "The input and output dimensions of 'transform' must be 2")
- BboxBase.__init__(self, **kwargs)
- self._bbox = bbox
- self._transform = transform
- self.set_children(bbox, transform)
- self._points = None
- def __str__(self):
- return ("{}(\n"
- "{},\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._bbox),
- _indent_str(self._transform)))
- def get_points(self):
- # docstring inherited
- if self._invalid:
- p = self._bbox.get_points()
- # Transform all four points, then make a new bounding box
- # from the result, taking care to make the orientation the
- # same.
- points = self._transform.transform(
- [[p[0, 0], p[0, 1]],
- [p[1, 0], p[0, 1]],
- [p[0, 0], p[1, 1]],
- [p[1, 0], p[1, 1]]])
- points = np.ma.filled(points, 0.0)
- xs = min(points[:, 0]), max(points[:, 0])
- if p[0, 0] > p[1, 0]:
- xs = xs[::-1]
- ys = min(points[:, 1]), max(points[:, 1])
- if p[0, 1] > p[1, 1]:
- ys = ys[::-1]
- self._points = np.array([
- [xs[0], ys[0]],
- [xs[1], ys[1]]
- ])
- self._invalid = 0
- return self._points
- if DEBUG:
- _get_points = get_points
- def get_points(self):
- points = self._get_points()
- self._check(points)
- return points
- class LockableBbox(BboxBase):
- """
- A :class:`Bbox` where some elements may be locked at certain values.
- When the child bounding box changes, the bounds of this bbox will update
- accordingly with the exception of the locked elements.
- """
- def __init__(self, bbox, x0=None, y0=None, x1=None, y1=None, **kwargs):
- """
- Parameters
- ----------
- bbox : Bbox
- The child bounding box to wrap.
- x0 : float or None
- The locked value for x0, or None to leave unlocked.
- y0 : float or None
- The locked value for y0, or None to leave unlocked.
- x1 : float or None
- The locked value for x1, or None to leave unlocked.
- y1 : float or None
- The locked value for y1, or None to leave unlocked.
- """
- if not bbox.is_bbox:
- raise ValueError("'bbox' is not a bbox")
- BboxBase.__init__(self, **kwargs)
- self._bbox = bbox
- self.set_children(bbox)
- self._points = None
- fp = [x0, y0, x1, y1]
- mask = [val is None for val in fp]
- self._locked_points = np.ma.array(fp, float, mask=mask).reshape((2, 2))
- def __str__(self):
- return ("{}(\n"
- "{},\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._bbox),
- _indent_str(self._locked_points)))
- def get_points(self):
- # docstring inherited
- if self._invalid:
- points = self._bbox.get_points()
- self._points = np.where(self._locked_points.mask,
- points,
- self._locked_points)
- self._invalid = 0
- return self._points
- if DEBUG:
- _get_points = get_points
- def get_points(self):
- points = self._get_points()
- self._check(points)
- return points
- @property
- def locked_x0(self):
- """
- float or None: The value used for the locked x0.
- """
- if self._locked_points.mask[0, 0]:
- return None
- else:
- return self._locked_points[0, 0]
- @locked_x0.setter
- def locked_x0(self, x0):
- self._locked_points.mask[0, 0] = x0 is None
- self._locked_points.data[0, 0] = x0
- self.invalidate()
- @property
- def locked_y0(self):
- """
- float or None: The value used for the locked y0.
- """
- if self._locked_points.mask[0, 1]:
- return None
- else:
- return self._locked_points[0, 1]
- @locked_y0.setter
- def locked_y0(self, y0):
- self._locked_points.mask[0, 1] = y0 is None
- self._locked_points.data[0, 1] = y0
- self.invalidate()
- @property
- def locked_x1(self):
- """
- float or None: The value used for the locked x1.
- """
- if self._locked_points.mask[1, 0]:
- return None
- else:
- return self._locked_points[1, 0]
- @locked_x1.setter
- def locked_x1(self, x1):
- self._locked_points.mask[1, 0] = x1 is None
- self._locked_points.data[1, 0] = x1
- self.invalidate()
- @property
- def locked_y1(self):
- """
- float or None: The value used for the locked y1.
- """
- if self._locked_points.mask[1, 1]:
- return None
- else:
- return self._locked_points[1, 1]
- @locked_y1.setter
- def locked_y1(self, y1):
- self._locked_points.mask[1, 1] = y1 is None
- self._locked_points.data[1, 1] = y1
- self.invalidate()
- class Transform(TransformNode):
- """
- The base class of all :class:`TransformNode` instances that
- actually perform a transformation.
- All non-affine transformations should be subclasses of this class.
- New affine transformations should be subclasses of `Affine2D`.
- Subclasses of this class should override the following members (at
- minimum):
- - :attr:`input_dims`
- - :attr:`output_dims`
- - :meth:`transform`
- - :meth:`inverted` (if an inverse exists)
- The following attributes may be overridden if the default is unsuitable:
- - :attr:`is_separable` (defaults to True for 1d -> 1d transforms, False
- otherwise)
- - :attr:`has_inverse` (defaults to True if :meth:`inverted` is overridden,
- False otherwise)
- If the transform needs to do something non-standard with
- :class:`matplotlib.path.Path` objects, such as adding curves
- where there were once line segments, it should override:
- - :meth:`transform_path`
- """
- input_dims = None
- """
- The number of input dimensions of this transform.
- Must be overridden (with integers) in the subclass.
- """
- output_dims = None
- """
- The number of output dimensions of this transform.
- Must be overridden (with integers) in the subclass.
- """
- is_separable = False
- """True if this transform is separable in the x- and y- dimensions."""
- has_inverse = False
- """True if this transform has a corresponding inverse transform."""
- def __init_subclass__(cls):
- # 1d transforms are always separable; we assume higher-dimensional ones
- # are not but subclasses can also directly set is_separable -- this is
- # verified by checking whether "is_separable" appears more than once in
- # the class's MRO (it appears once in Transform).
- if (sum("is_separable" in vars(parent) for parent in cls.__mro__) == 1
- and cls.input_dims == cls.output_dims == 1):
- cls.is_separable = True
- # Transform.inverted raises NotImplementedError; we assume that if this
- # is overridden then the transform is invertible but subclass can also
- # directly set has_inverse.
- if (sum("has_inverse" in vars(parent) for parent in cls.__mro__) == 1
- and hasattr(cls, "inverted")
- and cls.inverted is not Transform.inverted):
- cls.has_inverse = True
- def __add__(self, other):
- """
- Composes two transforms together such that *self* is followed
- by *other*.
- """
- if isinstance(other, Transform):
- return composite_transform_factory(self, other)
- raise TypeError(
- "Can not add Transform to object of type '%s'" % type(other))
- def __radd__(self, other):
- """
- Composes two transforms together such that *self* is followed
- by *other*.
- """
- if isinstance(other, Transform):
- return composite_transform_factory(other, self)
- raise TypeError(
- "Can not add Transform to object of type '%s'" % type(other))
- # Equality is based on object identity for `Transform`s (so we don't
- # override `__eq__`), but some subclasses, such as TransformWrapper &
- # AffineBase, override this behavior.
- def _iter_break_from_left_to_right(self):
- """
- Returns an iterator breaking down this transform stack from left to
- right recursively. If self == ((A, N), A) then the result will be an
- iterator which yields I : ((A, N), A), followed by A : (N, A),
- followed by (A, N) : (A), but not ((A, N), A) : I.
- This is equivalent to flattening the stack then yielding
- ``flat_stack[:i], flat_stack[i:]`` where i=0..(n-1).
- """
- yield IdentityTransform(), self
- @property
- def depth(self):
- """
- Returns the number of transforms which have been chained
- together to form this Transform instance.
- .. note::
- For the special case of a Composite transform, the maximum depth
- of the two is returned.
- """
- return 1
- def contains_branch(self, other):
- """
- Return whether the given transform is a sub-tree of this transform.
- This routine uses transform equality to identify sub-trees, therefore
- in many situations it is object id which will be used.
- For the case where the given transform represents the whole
- of this transform, returns True.
- """
- if self.depth < other.depth:
- return False
- # check that a subtree is equal to other (starting from self)
- for _, sub_tree in self._iter_break_from_left_to_right():
- if sub_tree == other:
- return True
- return False
- def contains_branch_seperately(self, other_transform):
- """
- Returns whether the given branch is a sub-tree of this transform on
- each separate dimension.
- A common use for this method is to identify if a transform is a blended
- transform containing an axes' data transform. e.g.::
- x_isdata, y_isdata = trans.contains_branch_seperately(ax.transData)
- """
- if self.output_dims != 2:
- raise ValueError('contains_branch_seperately only supports '
- 'transforms with 2 output dimensions')
- # for a non-blended transform each separate dimension is the same, so
- # just return the appropriate shape.
- return [self.contains_branch(other_transform)] * 2
- def __sub__(self, other):
- """
- Returns a transform stack which goes all the way down self's transform
- stack, and then ascends back up other's stack. If it can, this is
- optimised::
- # normally
- A - B == a + b.inverted()
- # sometimes, when A contains the tree B there is no need to
- # descend all the way down to the base of A (via B), instead we
- # can just stop at B.
- (A + B) - (B)^-1 == A
- # similarly, when B contains tree A, we can avoid descending A at
- # all, basically:
- A - (A + B) == ((B + A) - A).inverted() or B^-1
- For clarity, the result of ``(A + B) - B + B == (A + B)``.
- """
- # we only know how to do this operation if other is a Transform.
- if not isinstance(other, Transform):
- return NotImplemented
- for remainder, sub_tree in self._iter_break_from_left_to_right():
- if sub_tree == other:
- return remainder
- for remainder, sub_tree in other._iter_break_from_left_to_right():
- if sub_tree == self:
- if not remainder.has_inverse:
- raise ValueError(
- "The shortcut cannot be computed since 'other' "
- "includes a non-invertible component")
- return remainder.inverted()
- # if we have got this far, then there was no shortcut possible
- if other.has_inverse:
- return self + other.inverted()
- else:
- raise ValueError('It is not possible to compute transA - transB '
- 'since transB cannot be inverted and there is no '
- 'shortcut possible.')
- def __array__(self, *args, **kwargs):
- """
- Array interface to get at this Transform's affine matrix.
- """
- return self.get_affine().get_matrix()
- def transform(self, values):
- """
- Performs the transformation on the given array of values.
- Accepts a numpy array of shape (N x :attr:`input_dims`) and
- returns a numpy array of shape (N x :attr:`output_dims`).
- Alternatively, accepts a numpy array of length :attr:`input_dims`
- and returns a numpy array of length :attr:`output_dims`.
- """
- # Ensure that values is a 2d array (but remember whether
- # we started with a 1d or 2d array).
- values = np.asanyarray(values)
- ndim = values.ndim
- values = values.reshape((-1, self.input_dims))
- # Transform the values
- res = self.transform_affine(self.transform_non_affine(values))
- # Convert the result back to the shape of the input values.
- if ndim == 0:
- assert not np.ma.is_masked(res) # just to be on the safe side
- return res[0, 0]
- if ndim == 1:
- return res.reshape(-1)
- elif ndim == 2:
- return res
- raise ValueError(
- "Input values must have shape (N x {dims}) "
- "or ({dims}).".format(dims=self.input_dims))
- def transform_affine(self, values):
- """
- Performs only the affine part of this transformation on the
- given array of values.
- ``transform(values)`` is always equivalent to
- ``transform_affine(transform_non_affine(values))``.
- In non-affine transformations, this is generally a no-op. In
- affine transformations, this is equivalent to
- ``transform(values)``.
- Parameters
- ----------
- values : array
- The input values as NumPy array of length :attr:`input_dims` or
- shape (N x :attr:`input_dims`).
- Returns
- -------
- values : array
- The output values as NumPy array of length :attr:`input_dims` or
- shape (N x :attr:`output_dims`), depending on the input.
- """
- return self.get_affine().transform(values)
- def transform_non_affine(self, values):
- """
- Performs only the non-affine part of the transformation.
- ``transform(values)`` is always equivalent to
- ``transform_affine(transform_non_affine(values))``.
- In non-affine transformations, this is generally equivalent to
- ``transform(values)``. In affine transformations, this is
- always a no-op.
- Parameters
- ----------
- values : array
- The input values as NumPy array of length :attr:`input_dims` or
- shape (N x :attr:`input_dims`).
- Returns
- -------
- values : array
- The output values as NumPy array of length :attr:`input_dims` or
- shape (N x :attr:`output_dims`), depending on the input.
- """
- return values
- def transform_bbox(self, bbox):
- """
- Transform the given bounding box.
- Note, for smarter transforms including caching (a common
- requirement for matplotlib figures), see :class:`TransformedBbox`.
- """
- return Bbox(self.transform(bbox.get_points()))
- def get_affine(self):
- """
- Get the affine part of this transform.
- """
- return IdentityTransform()
- def get_matrix(self):
- """
- Get the Affine transformation array for the affine part
- of this transform.
- """
- return self.get_affine().get_matrix()
- def transform_point(self, point):
- """
- Return a transformed point.
- This function is only kept for backcompatibility; the more general
- `.transform` method is capable of transforming both a list of points
- and a single point.
- The point is given as a sequence of length :attr:`input_dims`.
- The transformed point is returned as a sequence of length
- :attr:`output_dims`.
- """
- if len(point) != self.input_dims:
- raise ValueError("The length of 'point' must be 'self.input_dims'")
- return self.transform(point)
- def transform_path(self, path):
- """
- Returns a transformed path.
- *path*: a :class:`~matplotlib.path.Path` instance.
- In some cases, this transform may insert curves into the path
- that began as line segments.
- """
- return self.transform_path_affine(self.transform_path_non_affine(path))
- def transform_path_affine(self, path):
- """
- Returns a path, transformed only by the affine part of
- this transform.
- *path*: a :class:`~matplotlib.path.Path` instance.
- ``transform_path(path)`` is equivalent to
- ``transform_path_affine(transform_path_non_affine(values))``.
- """
- return self.get_affine().transform_path_affine(path)
- def transform_path_non_affine(self, path):
- """
- Returns a path, transformed only by the non-affine
- part of this transform.
- *path*: a :class:`~matplotlib.path.Path` instance.
- ``transform_path(path)`` is equivalent to
- ``transform_path_affine(transform_path_non_affine(values))``.
- """
- x = self.transform_non_affine(path.vertices)
- return Path._fast_from_codes_and_verts(x, path.codes, path)
- def transform_angles(self, angles, pts, radians=False, pushoff=1e-5):
- """
- Transforms a set of angles anchored at specific locations.
- Parameters
- ----------
- angles : (N,) array-like
- The angles to transform.
- pts : (N, 2) array-like
- The points where the angles are anchored.
- radians : bool, default: False
- Whether *angles* are radians or degrees.
- pushoff : float
- For each point in *pts* and angle in *angles*, the transformed
- angle is computed by transforming a segment of length *pushoff*
- starting at that point and making that angle relative to the
- horizontal axis, and measuring the angle between the horizontal
- axis and the transformed segment.
- Returns
- -------
- transformed_angles : (N,) array
- """
- # Must be 2D
- if self.input_dims != 2 or self.output_dims != 2:
- raise NotImplementedError('Only defined in 2D')
- angles = np.asarray(angles)
- pts = np.asarray(pts)
- if angles.ndim != 1 or angles.shape[0] != pts.shape[0]:
- raise ValueError("'angles' must be a column vector and have same "
- "number of rows as 'pts'")
- if pts.shape[1] != 2:
- raise ValueError("'pts' must be array with 2 columns for x, y")
- # Convert to radians if desired
- if not radians:
- angles = np.deg2rad(angles)
- # Move a short distance away
- pts2 = pts + pushoff * np.c_[np.cos(angles), np.sin(angles)]
- # Transform both sets of points
- tpts = self.transform(pts)
- tpts2 = self.transform(pts2)
- # Calculate transformed angles
- d = tpts2 - tpts
- a = np.arctan2(d[:, 1], d[:, 0])
- # Convert back to degrees if desired
- if not radians:
- a = np.rad2deg(a)
- return a
- def inverted(self):
- """
- Return the corresponding inverse transformation.
- It holds ``x == self.inverted().transform(self.transform(x))``.
- The return value of this method should be treated as
- temporary. An update to *self* does not cause a corresponding
- update to its inverted copy.
- """
- raise NotImplementedError()
- class TransformWrapper(Transform):
- """
- A helper class that holds a single child transform and acts
- equivalently to it.
- This is useful if a node of the transform tree must be replaced at
- run time with a transform of a different type. This class allows
- that replacement to correctly trigger invalidation.
- Note that :class:`TransformWrapper` instances must have the same
- input and output dimensions during their entire lifetime, so the
- child transform may only be replaced with another child transform
- of the same dimensions.
- """
- pass_through = True
- def __init__(self, child):
- """
- *child*: A class:`Transform` instance. This child may later
- be replaced with :meth:`set`.
- """
- cbook._check_isinstance(Transform, child=child)
- self._init(child)
- self.set_children(child)
- def _init(self, child):
- Transform.__init__(self)
- self.input_dims = child.input_dims
- self.output_dims = child.output_dims
- self._set(child)
- self._invalid = 0
- def __eq__(self, other):
- return self._child.__eq__(other)
- def __str__(self):
- return ("{}(\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._child)))
- def frozen(self):
- # docstring inherited
- return self._child.frozen()
- def _set(self, child):
- self._child = child
- self.transform = child.transform
- self.transform_affine = child.transform_affine
- self.transform_non_affine = child.transform_non_affine
- self.transform_path = child.transform_path
- self.transform_path_affine = child.transform_path_affine
- self.transform_path_non_affine = child.transform_path_non_affine
- self.get_affine = child.get_affine
- self.inverted = child.inverted
- self.get_matrix = child.get_matrix
- # note we do not wrap other properties here since the transform's
- # child can be changed with WrappedTransform.set and so checking
- # is_affine and other such properties may be dangerous.
- def set(self, child):
- """
- Replace the current child of this transform with another one.
- The new child must have the same number of input and output
- dimensions as the current child.
- """
- if (child.input_dims != self.input_dims or
- child.output_dims != self.output_dims):
- raise ValueError(
- "The new child must have the same number of input and output "
- "dimensions as the current child")
- self.set_children(child)
- self._set(child)
- self._invalid = 0
- self.invalidate()
- self._invalid = 0
- is_affine = property(lambda self: self._child.is_affine)
- is_separable = property(lambda self: self._child.is_separable)
- has_inverse = property(lambda self: self._child.has_inverse)
- class AffineBase(Transform):
- """
- The base class of all affine transformations of any number of
- dimensions.
- """
- is_affine = True
- def __init__(self, *args, **kwargs):
- Transform.__init__(self, *args, **kwargs)
- self._inverted = None
- def __array__(self, *args, **kwargs):
- # optimises the access of the transform matrix vs. the superclass
- return self.get_matrix()
- def __eq__(self, other):
- if getattr(other, "is_affine", False):
- return np.all(self.get_matrix() == other.get_matrix())
- return NotImplemented
- def transform(self, values):
- # docstring inherited
- return self.transform_affine(values)
- def transform_affine(self, values):
- # docstring inherited
- raise NotImplementedError('Affine subclasses should override this '
- 'method.')
- def transform_non_affine(self, points):
- # docstring inherited
- return points
- def transform_path(self, path):
- # docstring inherited
- return self.transform_path_affine(path)
- def transform_path_affine(self, path):
- # docstring inherited
- return Path(self.transform_affine(path.vertices),
- path.codes, path._interpolation_steps)
- def transform_path_non_affine(self, path):
- # docstring inherited
- return path
- def get_affine(self):
- # docstring inherited
- return self
- class Affine2DBase(AffineBase):
- """
- The base class of all 2D affine transformations.
- 2D affine transformations are performed using a 3x3 numpy array::
- a c e
- b d f
- 0 0 1
- This class provides the read-only interface. For a mutable 2D
- affine transformation, use :class:`Affine2D`.
- Subclasses of this class will generally only need to override a
- constructor and :meth:`get_matrix` that generates a custom 3x3 matrix.
- """
- input_dims = 2
- output_dims = 2
- def frozen(self):
- # docstring inherited
- return Affine2D(self.get_matrix().copy())
- @property
- def is_separable(self):
- mtx = self.get_matrix()
- return mtx[0, 1] == mtx[1, 0] == 0.0
- def to_values(self):
- """
- Return the values of the matrix as an ``(a, b, c, d, e, f)`` tuple.
- """
- mtx = self.get_matrix()
- return tuple(mtx[:2].swapaxes(0, 1).flat)
- @staticmethod
- @cbook.deprecated(
- "3.2", alternative="Affine2D.from_values(...).get_matrix()")
- def matrix_from_values(a, b, c, d, e, f):
- """
- Create a new transformation matrix as a 3x3 numpy array of the form::
- a c e
- b d f
- 0 0 1
- """
- return np.array([[a, c, e], [b, d, f], [0.0, 0.0, 1.0]], float)
- def transform_affine(self, points):
- mtx = self.get_matrix()
- if isinstance(points, np.ma.MaskedArray):
- tpoints = affine_transform(points.data, mtx)
- return np.ma.MaskedArray(tpoints, mask=np.ma.getmask(points))
- return affine_transform(points, mtx)
- if DEBUG:
- _transform_affine = transform_affine
- def transform_affine(self, points):
- # docstring inherited
- # The major speed trap here is just converting to the
- # points to an array in the first place. If we can use
- # more arrays upstream, that should help here.
- if not isinstance(points, (np.ma.MaskedArray, np.ndarray)):
- cbook._warn_external(
- f'A non-numpy array of type {type(points)} was passed in '
- f'for transformation, which results in poor performance.')
- return self._transform_affine(points)
- def inverted(self):
- # docstring inherited
- if self._inverted is None or self._invalid:
- mtx = self.get_matrix()
- shorthand_name = None
- if self._shorthand_name:
- shorthand_name = '(%s)-1' % self._shorthand_name
- self._inverted = Affine2D(inv(mtx), shorthand_name=shorthand_name)
- self._invalid = 0
- return self._inverted
- class Affine2D(Affine2DBase):
- """
- A mutable 2D affine transformation.
- """
- def __init__(self, matrix=None, **kwargs):
- """
- Initialize an Affine transform from a 3x3 numpy float array::
- a c e
- b d f
- 0 0 1
- If *matrix* is None, initialize with the identity transform.
- """
- Affine2DBase.__init__(self, **kwargs)
- if matrix is None:
- # A bit faster than np.identity(3).
- matrix = IdentityTransform._mtx.copy()
- self._mtx = matrix
- self._invalid = 0
- def __str__(self):
- return ("{}(\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._mtx)))
- @staticmethod
- def from_values(a, b, c, d, e, f):
- """
- Create a new Affine2D instance from the given values::
- a c e
- b d f
- 0 0 1
- .
- """
- return Affine2D(
- np.array([a, c, e, b, d, f, 0.0, 0.0, 1.0], float).reshape((3, 3)))
- def get_matrix(self):
- """
- Get the underlying transformation matrix as a 3x3 numpy array::
- a c e
- b d f
- 0 0 1
- .
- """
- if self._invalid:
- self._inverted = None
- self._invalid = 0
- return self._mtx
- def set_matrix(self, mtx):
- """
- Set the underlying transformation matrix from a 3x3 numpy array::
- a c e
- b d f
- 0 0 1
- .
- """
- self._mtx = mtx
- self.invalidate()
- def set(self, other):
- """
- Set this transformation from the frozen copy of another
- :class:`Affine2DBase` object.
- """
- cbook._check_isinstance(Affine2DBase, other=other)
- self._mtx = other.get_matrix()
- self.invalidate()
- @staticmethod
- def identity():
- """
- Return a new `Affine2D` object that is the identity transform.
- Unless this transform will be mutated later on, consider using
- the faster :class:`IdentityTransform` class instead.
- """
- return Affine2D()
- def clear(self):
- """
- Reset the underlying matrix to the identity transform.
- """
- # A bit faster than np.identity(3).
- self._mtx = IdentityTransform._mtx.copy()
- self.invalidate()
- return self
- def rotate(self, theta):
- """
- Add a rotation (in radians) to this transform in place.
- Returns *self*, so this method can easily be chained with more
- calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
- and :meth:`scale`.
- """
- a = np.cos(theta)
- b = np.sin(theta)
- rotate_mtx = np.array([[a, -b, 0.0], [b, a, 0.0], [0.0, 0.0, 1.0]],
- float)
- self._mtx = np.dot(rotate_mtx, self._mtx)
- self.invalidate()
- return self
- def rotate_deg(self, degrees):
- """
- Add a rotation (in degrees) to this transform in place.
- Returns *self*, so this method can easily be chained with more
- calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
- and :meth:`scale`.
- """
- return self.rotate(np.deg2rad(degrees))
- def rotate_around(self, x, y, theta):
- """
- Add a rotation (in radians) around the point (x, y) in place.
- Returns *self*, so this method can easily be chained with more
- calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
- and :meth:`scale`.
- """
- return self.translate(-x, -y).rotate(theta).translate(x, y)
- def rotate_deg_around(self, x, y, degrees):
- """
- Add a rotation (in degrees) around the point (x, y) in place.
- Returns *self*, so this method can easily be chained with more
- calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
- and :meth:`scale`.
- """
- # Cast to float to avoid wraparound issues with uint8's
- x, y = float(x), float(y)
- return self.translate(-x, -y).rotate_deg(degrees).translate(x, y)
- def translate(self, tx, ty):
- """
- Adds a translation in place.
- Returns *self*, so this method can easily be chained with more
- calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
- and :meth:`scale`.
- """
- translate_mtx = np.array(
- [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]], float)
- self._mtx = np.dot(translate_mtx, self._mtx)
- self.invalidate()
- return self
- def scale(self, sx, sy=None):
- """
- Adds a scale in place.
- If *sy* is None, the same scale is applied in both the *x*- and
- *y*-directions.
- Returns *self*, so this method can easily be chained with more
- calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
- and :meth:`scale`.
- """
- if sy is None:
- sy = sx
- scale_mtx = np.array(
- [[sx, 0.0, 0.0], [0.0, sy, 0.0], [0.0, 0.0, 1.0]], float)
- self._mtx = np.dot(scale_mtx, self._mtx)
- self.invalidate()
- return self
- def skew(self, xShear, yShear):
- """
- Adds a skew in place.
- *xShear* and *yShear* are the shear angles along the *x*- and
- *y*-axes, respectively, in radians.
- Returns *self*, so this method can easily be chained with more
- calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
- and :meth:`scale`.
- """
- rotX = np.tan(xShear)
- rotY = np.tan(yShear)
- skew_mtx = np.array(
- [[1.0, rotX, 0.0], [rotY, 1.0, 0.0], [0.0, 0.0, 1.0]], float)
- self._mtx = np.dot(skew_mtx, self._mtx)
- self.invalidate()
- return self
- def skew_deg(self, xShear, yShear):
- """
- Adds a skew in place.
- *xShear* and *yShear* are the shear angles along the *x*- and
- *y*-axes, respectively, in degrees.
- Returns *self*, so this method can easily be chained with more
- calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
- and :meth:`scale`.
- """
- return self.skew(np.deg2rad(xShear), np.deg2rad(yShear))
- class IdentityTransform(Affine2DBase):
- """
- A special class that does one thing, the identity transform, in a
- fast way.
- """
- _mtx = np.identity(3)
- def frozen(self):
- # docstring inherited
- return self
- def __str__(self):
- return ("{}()"
- .format(type(self).__name__))
- def get_matrix(self):
- # docstring inherited
- return self._mtx
- def transform(self, points):
- # docstring inherited
- return np.asanyarray(points)
- def transform_affine(self, points):
- # docstring inherited
- return np.asanyarray(points)
- def transform_non_affine(self, points):
- # docstring inherited
- return np.asanyarray(points)
- def transform_path(self, path):
- # docstring inherited
- return path
- def transform_path_affine(self, path):
- # docstring inherited
- return path
- def transform_path_non_affine(self, path):
- # docstring inherited
- return path
- def get_affine(self):
- # docstring inherited
- return self
- def inverted(self):
- # docstring inherited
- return self
- class _BlendedMixin:
- """Common methods for `BlendedGenericTransform` and `BlendedAffine2D`."""
- def __eq__(self, other):
- if isinstance(other, (BlendedAffine2D, BlendedGenericTransform)):
- return (self._x == other._x) and (self._y == other._y)
- elif self._x == self._y:
- return self._x == other
- else:
- return NotImplemented
- def contains_branch_seperately(self, transform):
- return (self._x.contains_branch(transform),
- self._y.contains_branch(transform))
- def __str__(self):
- return ("{}(\n"
- "{},\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._x),
- _indent_str(self._y)))
- class BlendedGenericTransform(_BlendedMixin, Transform):
- """
- A "blended" transform uses one transform for the *x*-direction, and
- another transform for the *y*-direction.
- This "generic" version can handle any given child transform in the
- *x*- and *y*-directions.
- """
- input_dims = 2
- output_dims = 2
- is_separable = True
- pass_through = True
- def __init__(self, x_transform, y_transform, **kwargs):
- """
- Create a new "blended" transform using *x_transform* to
- transform the *x*-axis and *y_transform* to transform the
- *y*-axis.
- You will generally not call this constructor directly but use the
- `blended_transform_factory` function instead, which can determine
- automatically which kind of blended transform to create.
- """
- # Here we ask: "Does it blend?"
- Transform.__init__(self, **kwargs)
- self._x = x_transform
- self._y = y_transform
- self.set_children(x_transform, y_transform)
- self._affine = None
- @property
- def depth(self):
- return max(self._x.depth, self._y.depth)
- def contains_branch(self, other):
- # A blended transform cannot possibly contain a branch from two
- # different transforms.
- return False
- is_affine = property(lambda self: self._x.is_affine and self._y.is_affine)
- has_inverse = property(
- lambda self: self._x.has_inverse and self._y.has_inverse)
- def frozen(self):
- # docstring inherited
- return blended_transform_factory(self._x.frozen(), self._y.frozen())
- def transform_non_affine(self, points):
- # docstring inherited
- if self._x.is_affine and self._y.is_affine:
- return points
- x = self._x
- y = self._y
- if x == y and x.input_dims == 2:
- return x.transform_non_affine(points)
- if x.input_dims == 2:
- x_points = x.transform_non_affine(points)[:, 0:1]
- else:
- x_points = x.transform_non_affine(points[:, 0])
- x_points = x_points.reshape((len(x_points), 1))
- if y.input_dims == 2:
- y_points = y.transform_non_affine(points)[:, 1:]
- else:
- y_points = y.transform_non_affine(points[:, 1])
- y_points = y_points.reshape((len(y_points), 1))
- if (isinstance(x_points, np.ma.MaskedArray) or
- isinstance(y_points, np.ma.MaskedArray)):
- return np.ma.concatenate((x_points, y_points), 1)
- else:
- return np.concatenate((x_points, y_points), 1)
- def inverted(self):
- # docstring inherited
- return BlendedGenericTransform(self._x.inverted(), self._y.inverted())
- def get_affine(self):
- # docstring inherited
- if self._invalid or self._affine is None:
- if self._x == self._y:
- self._affine = self._x.get_affine()
- else:
- x_mtx = self._x.get_affine().get_matrix()
- y_mtx = self._y.get_affine().get_matrix()
- # This works because we already know the transforms are
- # separable, though normally one would want to set b and
- # c to zero.
- mtx = np.vstack((x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0]))
- self._affine = Affine2D(mtx)
- self._invalid = 0
- return self._affine
- class BlendedAffine2D(_BlendedMixin, Affine2DBase):
- """
- A "blended" transform uses one transform for the *x*-direction, and
- another transform for the *y*-direction.
- This version is an optimization for the case where both child
- transforms are of type :class:`Affine2DBase`.
- """
- is_separable = True
- def __init__(self, x_transform, y_transform, **kwargs):
- """
- Create a new "blended" transform using *x_transform* to
- transform the *x*-axis and *y_transform* to transform the
- *y*-axis.
- Both *x_transform* and *y_transform* must be 2D affine
- transforms.
- You will generally not call this constructor directly but use the
- `blended_transform_factory` function instead, which can determine
- automatically which kind of blended transform to create.
- """
- is_affine = x_transform.is_affine and y_transform.is_affine
- is_separable = x_transform.is_separable and y_transform.is_separable
- is_correct = is_affine and is_separable
- if not is_correct:
- raise ValueError("Both *x_transform* and *y_transform* must be 2D "
- "affine transforms")
- Transform.__init__(self, **kwargs)
- self._x = x_transform
- self._y = y_transform
- self.set_children(x_transform, y_transform)
- Affine2DBase.__init__(self)
- self._mtx = None
- def get_matrix(self):
- # docstring inherited
- if self._invalid:
- if self._x == self._y:
- self._mtx = self._x.get_matrix()
- else:
- x_mtx = self._x.get_matrix()
- y_mtx = self._y.get_matrix()
- # This works because we already know the transforms are
- # separable, though normally one would want to set b and
- # c to zero.
- self._mtx = np.vstack((x_mtx[0], y_mtx[1], [0.0, 0.0, 1.0]))
- self._inverted = None
- self._invalid = 0
- return self._mtx
- def blended_transform_factory(x_transform, y_transform):
- """
- Create a new "blended" transform using *x_transform* to transform
- the *x*-axis and *y_transform* to transform the *y*-axis.
- A faster version of the blended transform is returned for the case
- where both child transforms are affine.
- """
- if (isinstance(x_transform, Affine2DBase)
- and isinstance(y_transform, Affine2DBase)):
- return BlendedAffine2D(x_transform, y_transform)
- return BlendedGenericTransform(x_transform, y_transform)
- class CompositeGenericTransform(Transform):
- """
- A composite transform formed by applying transform *a* then
- transform *b*.
- This "generic" version can handle any two arbitrary
- transformations.
- """
- pass_through = True
- def __init__(self, a, b, **kwargs):
- """
- Create a new composite transform that is the result of
- applying transform *a* then transform *b*.
- You will generally not call this constructor directly but use the
- `composite_transform_factory` function instead, which can automatically
- choose the best kind of composite transform instance to create.
- """
- if a.output_dims != b.input_dims:
- raise ValueError("The output dimension of 'a' must be equal to "
- "the input dimensions of 'b'")
- self.input_dims = a.input_dims
- self.output_dims = b.output_dims
- Transform.__init__(self, **kwargs)
- self._a = a
- self._b = b
- self.set_children(a, b)
- def frozen(self):
- # docstring inherited
- self._invalid = 0
- frozen = composite_transform_factory(
- self._a.frozen(), self._b.frozen())
- if not isinstance(frozen, CompositeGenericTransform):
- return frozen.frozen()
- return frozen
- def _invalidate_internal(self, value, invalidating_node):
- # In some cases for a composite transform, an invalidating call to
- # AFFINE_ONLY needs to be extended to invalidate the NON_AFFINE part
- # too. These cases are when the right hand transform is non-affine and
- # either:
- # (a) the left hand transform is non affine
- # (b) it is the left hand node which has triggered the invalidation
- if value == Transform.INVALID_AFFINE \
- and not self._b.is_affine \
- and (not self._a.is_affine or invalidating_node is self._a):
- value = Transform.INVALID
- Transform._invalidate_internal(self, value=value,
- invalidating_node=invalidating_node)
- def __eq__(self, other):
- if isinstance(other, (CompositeGenericTransform, CompositeAffine2D)):
- return self is other or (self._a == other._a
- and self._b == other._b)
- else:
- return False
- def _iter_break_from_left_to_right(self):
- for left, right in self._a._iter_break_from_left_to_right():
- yield left, right + self._b
- for left, right in self._b._iter_break_from_left_to_right():
- yield self._a + left, right
- depth = property(lambda self: self._a.depth + self._b.depth)
- is_affine = property(lambda self: self._a.is_affine and self._b.is_affine)
- is_separable = property(
- lambda self: self._a.is_separable and self._b.is_separable)
- has_inverse = property(
- lambda self: self._a.has_inverse and self._b.has_inverse)
- def __str__(self):
- return ("{}(\n"
- "{},\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._a),
- _indent_str(self._b)))
- def transform_affine(self, points):
- # docstring inherited
- return self.get_affine().transform(points)
- def transform_non_affine(self, points):
- # docstring inherited
- if self._a.is_affine and self._b.is_affine:
- return points
- elif not self._a.is_affine and self._b.is_affine:
- return self._a.transform_non_affine(points)
- else:
- return self._b.transform_non_affine(
- self._a.transform(points))
- def transform_path_non_affine(self, path):
- # docstring inherited
- if self._a.is_affine and self._b.is_affine:
- return path
- elif not self._a.is_affine and self._b.is_affine:
- return self._a.transform_path_non_affine(path)
- else:
- return self._b.transform_path_non_affine(
- self._a.transform_path(path))
- def get_affine(self):
- # docstring inherited
- if not self._b.is_affine:
- return self._b.get_affine()
- else:
- return Affine2D(np.dot(self._b.get_affine().get_matrix(),
- self._a.get_affine().get_matrix()))
- def inverted(self):
- # docstring inherited
- return CompositeGenericTransform(
- self._b.inverted(), self._a.inverted())
- class CompositeAffine2D(Affine2DBase):
- """
- A composite transform formed by applying transform *a* then transform *b*.
- This version is an optimization that handles the case where both *a*
- and *b* are 2D affines.
- """
- def __init__(self, a, b, **kwargs):
- """
- Create a new composite transform that is the result of
- applying transform *a* then transform *b*.
- Both *a* and *b* must be instances of :class:`Affine2DBase`.
- You will generally not call this constructor directly but use the
- `composite_transform_factory` function instead, which can automatically
- choose the best kind of composite transform instance to create.
- """
- if not a.is_affine or not b.is_affine:
- raise ValueError("'a' and 'b' must be affine transforms")
- if a.output_dims != b.input_dims:
- raise ValueError("The output dimension of 'a' must be equal to "
- "the input dimensions of 'b'")
- self.input_dims = a.input_dims
- self.output_dims = b.output_dims
- Affine2DBase.__init__(self, **kwargs)
- self._a = a
- self._b = b
- self.set_children(a, b)
- self._mtx = None
- @property
- def depth(self):
- return self._a.depth + self._b.depth
- def _iter_break_from_left_to_right(self):
- for left, right in self._a._iter_break_from_left_to_right():
- yield left, right + self._b
- for left, right in self._b._iter_break_from_left_to_right():
- yield self._a + left, right
- def __str__(self):
- return ("{}(\n"
- "{},\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._a),
- _indent_str(self._b)))
- def get_matrix(self):
- # docstring inherited
- if self._invalid:
- self._mtx = np.dot(
- self._b.get_matrix(),
- self._a.get_matrix())
- self._inverted = None
- self._invalid = 0
- return self._mtx
- def composite_transform_factory(a, b):
- """
- Create a new composite transform that is the result of applying
- transform a then transform b.
- Shortcut versions of the blended transform are provided for the
- case where both child transforms are affine, or one or the other
- is the identity transform.
- Composite transforms may also be created using the '+' operator,
- e.g.::
- c = a + b
- """
- # check to see if any of a or b are IdentityTransforms. We use
- # isinstance here to guarantee that the transforms will *always*
- # be IdentityTransforms. Since TransformWrappers are mutable,
- # use of equality here would be wrong.
- if isinstance(a, IdentityTransform):
- return b
- elif isinstance(b, IdentityTransform):
- return a
- elif isinstance(a, Affine2D) and isinstance(b, Affine2D):
- return CompositeAffine2D(a, b)
- return CompositeGenericTransform(a, b)
- class BboxTransform(Affine2DBase):
- """
- `BboxTransform` linearly transforms points from one `Bbox` to another.
- """
- is_separable = True
- def __init__(self, boxin, boxout, **kwargs):
- """
- Create a new :class:`BboxTransform` that linearly transforms
- points from *boxin* to *boxout*.
- """
- if not boxin.is_bbox or not boxout.is_bbox:
- raise ValueError("'boxin' and 'boxout' must be bbox")
- Affine2DBase.__init__(self, **kwargs)
- self._boxin = boxin
- self._boxout = boxout
- self.set_children(boxin, boxout)
- self._mtx = None
- self._inverted = None
- def __str__(self):
- return ("{}(\n"
- "{},\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._boxin),
- _indent_str(self._boxout)))
- def get_matrix(self):
- # docstring inherited
- if self._invalid:
- inl, inb, inw, inh = self._boxin.bounds
- outl, outb, outw, outh = self._boxout.bounds
- x_scale = outw / inw
- y_scale = outh / inh
- if DEBUG and (x_scale == 0 or y_scale == 0):
- raise ValueError(
- "Transforming from or to a singular bounding box")
- self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale+outl)],
- [0.0 , y_scale, (-inb*y_scale+outb)],
- [0.0 , 0.0 , 1.0 ]],
- float)
- self._inverted = None
- self._invalid = 0
- return self._mtx
- class BboxTransformTo(Affine2DBase):
- """
- `BboxTransformTo` is a transformation that linearly transforms points from
- the unit bounding box to a given `Bbox`.
- """
- is_separable = True
- def __init__(self, boxout, **kwargs):
- """
- Create a new :class:`BboxTransformTo` that linearly transforms
- points from the unit bounding box to *boxout*.
- """
- if not boxout.is_bbox:
- raise ValueError("'boxout' must be bbox")
- Affine2DBase.__init__(self, **kwargs)
- self._boxout = boxout
- self.set_children(boxout)
- self._mtx = None
- self._inverted = None
- def __str__(self):
- return ("{}(\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._boxout)))
- def get_matrix(self):
- # docstring inherited
- if self._invalid:
- outl, outb, outw, outh = self._boxout.bounds
- if DEBUG and (outw == 0 or outh == 0):
- raise ValueError("Transforming to a singular bounding box.")
- self._mtx = np.array([[outw, 0.0, outl],
- [ 0.0, outh, outb],
- [ 0.0, 0.0, 1.0]],
- float)
- self._inverted = None
- self._invalid = 0
- return self._mtx
- class BboxTransformToMaxOnly(BboxTransformTo):
- """
- `BboxTransformTo` is a transformation that linearly transforms points from
- the unit bounding box to a given `Bbox` with a fixed upper left of (0, 0).
- """
- def get_matrix(self):
- # docstring inherited
- if self._invalid:
- xmax, ymax = self._boxout.max
- if DEBUG and (xmax == 0 or ymax == 0):
- raise ValueError("Transforming to a singular bounding box.")
- self._mtx = np.array([[xmax, 0.0, 0.0],
- [ 0.0, ymax, 0.0],
- [ 0.0, 0.0, 1.0]],
- float)
- self._inverted = None
- self._invalid = 0
- return self._mtx
- class BboxTransformFrom(Affine2DBase):
- """
- `BboxTransformFrom` linearly transforms points from a given `Bbox` to the
- unit bounding box.
- """
- is_separable = True
- def __init__(self, boxin, **kwargs):
- if not boxin.is_bbox:
- raise ValueError("'boxin' must be bbox")
- Affine2DBase.__init__(self, **kwargs)
- self._boxin = boxin
- self.set_children(boxin)
- self._mtx = None
- self._inverted = None
- def __str__(self):
- return ("{}(\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._boxin)))
- def get_matrix(self):
- # docstring inherited
- if self._invalid:
- inl, inb, inw, inh = self._boxin.bounds
- if DEBUG and (inw == 0 or inh == 0):
- raise ValueError("Transforming from a singular bounding box.")
- x_scale = 1.0 / inw
- y_scale = 1.0 / inh
- self._mtx = np.array([[x_scale, 0.0 , (-inl*x_scale)],
- [0.0 , y_scale, (-inb*y_scale)],
- [0.0 , 0.0 , 1.0 ]],
- float)
- self._inverted = None
- self._invalid = 0
- return self._mtx
- class ScaledTranslation(Affine2DBase):
- """
- A transformation that translates by *xt* and *yt*, after *xt* and *yt*
- have been transformed by *scale_trans*.
- """
- def __init__(self, xt, yt, scale_trans, **kwargs):
- Affine2DBase.__init__(self, **kwargs)
- self._t = (xt, yt)
- self._scale_trans = scale_trans
- self.set_children(scale_trans)
- self._mtx = None
- self._inverted = None
- def __str__(self):
- return ("{}(\n"
- "{})"
- .format(type(self).__name__,
- _indent_str(self._t)))
- def get_matrix(self):
- # docstring inherited
- if self._invalid:
- # A bit faster than np.identity(3).
- self._mtx = IdentityTransform._mtx.copy()
- self._mtx[:2, 2] = self._scale_trans.transform(self._t)
- self._invalid = 0
- self._inverted = None
- return self._mtx
- class TransformedPath(TransformNode):
- """
- A `TransformedPath` caches a non-affine transformed copy of the
- `~.path.Path`. This cached copy is automatically updated when the
- non-affine part of the transform changes.
- .. note::
- Paths are considered immutable by this class. Any update to the
- path's vertices/codes will not trigger a transform recomputation.
- """
- def __init__(self, path, transform):
- """
- Parameters
- ----------
- path : `~.path.Path`
- transform : `Transform`
- """
- cbook._check_isinstance(Transform, transform=transform)
- TransformNode.__init__(self)
- self._path = path
- self._transform = transform
- self.set_children(transform)
- self._transformed_path = None
- self._transformed_points = None
- def _revalidate(self):
- # only recompute if the invalidation includes the non_affine part of
- # the transform
- if (self._invalid & self.INVALID_NON_AFFINE == self.INVALID_NON_AFFINE
- or self._transformed_path is None):
- self._transformed_path = \
- self._transform.transform_path_non_affine(self._path)
- self._transformed_points = \
- Path._fast_from_codes_and_verts(
- self._transform.transform_non_affine(self._path.vertices),
- None, self._path)
- self._invalid = 0
- def get_transformed_points_and_affine(self):
- """
- Return a copy of the child path, with the non-affine part of
- the transform already applied, along with the affine part of
- the path necessary to complete the transformation. Unlike
- :meth:`get_transformed_path_and_affine`, no interpolation will
- be performed.
- """
- self._revalidate()
- return self._transformed_points, self.get_affine()
- def get_transformed_path_and_affine(self):
- """
- Return a copy of the child path, with the non-affine part of
- the transform already applied, along with the affine part of
- the path necessary to complete the transformation.
- """
- self._revalidate()
- return self._transformed_path, self.get_affine()
- def get_fully_transformed_path(self):
- """
- Return a fully-transformed copy of the child path.
- """
- self._revalidate()
- return self._transform.transform_path_affine(self._transformed_path)
- def get_affine(self):
- return self._transform.get_affine()
- class TransformedPatchPath(TransformedPath):
- """
- A `TransformedPatchPath` caches a non-affine transformed copy of the
- `~.patch.Patch`. This cached copy is automatically updated when the
- non-affine part of the transform or the patch changes.
- """
- def __init__(self, patch):
- """
- Parameters
- ----------
- patch : `~.patches.Patch`
- """
- TransformNode.__init__(self)
- transform = patch.get_transform()
- self._patch = patch
- self._transform = transform
- self.set_children(transform)
- self._path = patch.get_path()
- self._transformed_path = None
- self._transformed_points = None
- def _revalidate(self):
- patch_path = self._patch.get_path()
- # Only recompute if the invalidation includes the non_affine part of
- # the transform, or the Patch's Path has changed.
- if (self._transformed_path is None or self._path != patch_path or
- (self._invalid & self.INVALID_NON_AFFINE ==
- self.INVALID_NON_AFFINE)):
- self._path = patch_path
- self._transformed_path = \
- self._transform.transform_path_non_affine(patch_path)
- self._transformed_points = \
- Path._fast_from_codes_and_verts(
- self._transform.transform_non_affine(patch_path.vertices),
- None, patch_path)
- self._invalid = 0
- def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True):
- """
- Modify the endpoints of a range as needed to avoid singularities.
- Parameters
- ----------
- vmin, vmax : float
- The initial endpoints.
- expander : float, optional, default: 0.001
- Fractional amount by which *vmin* and *vmax* are expanded if
- the original interval is too small, based on *tiny*.
- tiny : float, optional, default: 1e-15
- Threshold for the ratio of the interval to the maximum absolute
- value of its endpoints. If the interval is smaller than
- this, it will be expanded. This value should be around
- 1e-15 or larger; otherwise the interval will be approaching
- the double precision resolution limit.
- increasing : bool, optional, default: True
- If True, swap *vmin*, *vmax* if *vmin* > *vmax*.
- Returns
- -------
- vmin, vmax : float
- Endpoints, expanded and/or swapped if necessary.
- If either input is inf or NaN, or if both inputs are 0 or very
- close to zero, it returns -*expander*, *expander*.
- """
- if (not np.isfinite(vmin)) or (not np.isfinite(vmax)):
- return -expander, expander
- swapped = False
- if vmax < vmin:
- vmin, vmax = vmax, vmin
- swapped = True
- # Expand vmin, vmax to float: if they were integer types, they can wrap
- # around in abs (abs(np.int8(-128)) == -128) and vmax - vmin can overflow.
- vmin, vmax = map(float, [vmin, vmax])
- maxabsvalue = max(abs(vmin), abs(vmax))
- if maxabsvalue < (1e6 / tiny) * np.finfo(float).tiny:
- vmin = -expander
- vmax = expander
- elif vmax - vmin <= maxabsvalue * tiny:
- if vmax == 0 and vmin == 0:
- vmin = -expander
- vmax = expander
- else:
- vmin -= expander*abs(vmin)
- vmax += expander*abs(vmax)
- if swapped and not increasing:
- vmin, vmax = vmax, vmin
- return vmin, vmax
- def interval_contains(interval, val):
- """
- Check, inclusively, whether an interval includes a given value.
- Parameters
- ----------
- interval : sequence of scalar
- A 2-length sequence, endpoints that define the interval.
- val : scalar
- Value to check is within interval.
- Returns
- -------
- bool
- Returns *True* if given *val* is within the *interval*.
- """
- a, b = interval
- if a > b:
- a, b = b, a
- return a <= val <= b
- def _interval_contains_close(interval, val, rtol=1e-10):
- """
- Check, inclusively, whether an interval includes a given value, with the
- interval expanded by a small tolerance to admit floating point errors.
- Parameters
- ----------
- interval : sequence of scalar
- A 2-length sequence, endpoints that define the interval.
- val : scalar
- Value to check is within interval.
- rtol : scalar
- Tolerance slippage allowed outside of this interval. Default
- 1e-10 * (b - a).
- Returns
- -------
- bool
- Returns *True* if given *val* is within the *interval* (with tolerance)
- """
- a, b = interval
- if a > b:
- a, b = b, a
- rtol = (b - a) * rtol
- return a - rtol <= val <= b + rtol
- def interval_contains_open(interval, val):
- """
- Check, excluding endpoints, whether an interval includes a given value.
- Parameters
- ----------
- interval : sequence of scalar
- A 2-length sequence, endpoints that define the interval.
- val : scalar
- Value to check is within interval.
- Returns
- -------
- bool
- Returns true if given val is within the interval.
- """
- a, b = interval
- return a < val < b or a > val > b
- def offset_copy(trans, fig=None, x=0.0, y=0.0, units='inches'):
- """
- Return a new transform with an added offset.
- Parameters
- ----------
- trans : :class:`Transform` instance
- Any transform, to which offset will be applied.
- fig : :class:`~matplotlib.figure.Figure`, optional, default: None
- Current figure. It can be None if *units* are 'dots'.
- x, y : float, optional, default: 0.0
- Specifies the offset to apply.
- units : {'inches', 'points', 'dots'}, optional
- Units of the offset.
- Returns
- -------
- trans : :class:`Transform` instance
- Transform with applied offset.
- """
- if units == 'dots':
- return trans + Affine2D().translate(x, y)
- if fig is None:
- raise ValueError('For units of inches or points a fig kwarg is needed')
- if units == 'points':
- x /= 72.0
- y /= 72.0
- elif units == 'inches':
- pass
- else:
- cbook._check_in_list(['dots', 'points', 'inches'], units=units)
- return trans + ScaledTranslation(x, y, fig.dpi_scale_trans)
|