Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Plot limit with transform #731

Merged
merged 11 commits into from

3 participants

Phil Elson John Hunter Michael Droettboom
Phil Elson
Collaborator

This pull request represents a significant chunk of work to address a simple bug:

import matplotlib.pyplot as plt
import matplotlib.transforms as mtrans

ax = plt.axes()
off_trans = mtrans.Affine2D().translate(10, 10)

plt.plot(range(11), transform=off_trans + ax.transData)

print(ax.dataLim)

The result should be [10, 10, 20, 20], but the offset transform has not been taken into account.

Since a path transformation can be costly, it made sense to use the created Line's cached transform concept.
This threw up another, quite confusing bug:

import matplotlib.projections.polar as polar
import matplotlib.transforms as mtrans
import matplotlib.path as mpath
import numpy as np

full = mtrans.Affine2D().translate(1, 0) + polar.PolarAxes.PolarTransform()

verts = np.array([[0, 0], [5, 5], [2, 0]])
p = mpath.Path(verts)

tpath = mtrans.TransformedPath(p, full)
partial_p, aff = tpath.get_transformed_path_and_affine()

print full.transform_path_affine(full.transform_path_non_affine(p))
print full.get_affine().transform_path_affine(full.transform_path_non_affine(p))

The numbers themselves aren't important, suffice to say that the former is correct.

Additionally, the need for non-affine Transform subclasses to implement transform_non_affine and also copy this definition into transform too is confusing/obfuscating e.g.:

class PolarTransform(Transform):
        def transform(self, tr):
            # ...
            # do some stuff
            # ...
            return xy
        transform.__doc__ = Transform.transform.__doc__

        transform_non_affine = transform
        transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__

This latter complaint is the result of an optimisation that will see little benefit (transform stacks are typically mostly Affine, and the non-affine part is easily cached).

Therefore this pull request represents a simplification (at the cost of a couple more function calls) of the current Transform framework. Whilst it is my opinion that the Transform class heirachy remains non-optimally representative of the problem space, I have tried to be pragmatic in my changes for both backwards compatibility and size of review considerations.

This pull request is independent of the invalidation mechanism upgrade being discussed in #723, and a merge between the two should be straight forward.

The tests run exactly the same as they did before commencing this work (they weren't passing on my machine in the first place, but the RMS values have not changed at all). The run time has gone up 5 seconds up to 458 seconds (~1% slower), but this includes the new tests as a result of this pull.

Note this change subtly affects the way one should implement a Transform. If you are implementing a non affine transformation, then you should
override the transform_non_affine, rather than overriding the transform & copying the transform into transform_non_affine too. e.g.:

class PolarTransform(Transform):
        def transform_non_affine(self, tr):
            # ...
            # do some stuff
            # ...
            return xy
        transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__

The documentation is still representative of this change, hence there are few documentation changes included in this pull request.

Michael Droettboom mdboom was assigned
John Hunter
Owner

Hey Phil,

I just did a quick read through of this PR and it will take me much more time for me to have something intelligent to say. While I wrote the original transformations infrastructure, @mdboom did a thorough revamp of it for mpl 0.98 and he is the resident expert. So hopefully he will chime in here shortly. From my read, it is obvious that this is very careful, well thought out and well tested work, so congrats on successfully diving into one of the hairiest parts of the codebase.

I don't use any exotic non-affine transformations in my own work, just the plain vanilla logarithmic scales, so I am not intimately familiar with many of the issues arising there. I do use the blended transformations quite a bit, and this leads me to my question. In the original bug you discuss:

off_trans = mtrans.Affine2D().translate(10, 10)
plt.plot(range(11), transform=off_trans + ax.transData)
print(ax.dataLim)

which produces incorrect datalim, you write "The result should be [10, 10, 20, 20]". It is not obvious to me that mpl is producing the incorrect result here. I'm not really disagreeing with you here, mainly looking to be educated.

In the simple case of a "blended transformation" produced by axvline:

In [46]: ax = plt.gca()

In [47]: line, = ax.plot([0, 1], [.5, .7])

In [48]: ax.dataLim
Out[48]:
Bbox(array([[ 0. ,  0.5],
       [ 1. ,  0.7]]))

In [49]: linev = ax.axvline(0.5)

In [50]: ax.dataLim
Out[50]:
Bbox(array([[ 0. ,  0.5],
       [ 1. ,  0.7]]))

In [51]: linev.get_ydata()
Out[51]: [0, 1]

In this case, even though the axvline has y coords in [0, 1], this does not affect the axes datalim y limits, because using the blended transform (x is data, y is axes) we do not consider the y coordinate to be in the data system at all. The line will span the y space regardless of the x data limits or view limits. Obviously when presented with a generic transformation, there is no way for mpl to infer this, so we give it a hint with the x_isdata and y_isdata which is then respected by update_from_path. I see you support this behavior in your comment about backwards compatibility in _update_line_limits

Now this breaks down if I create the blended transformation on my own and do not set these x_isdata/y_isdata hints:

In [53]: ax = plt.gca()

In [54]: trans = transforms.blended_transform_factory(
    ax.transData, ax.transAxes)

In [55]: line, = ax.plot([1,2,3], transform=trans)

In [56]: ax.dataLim
Out[56]:
Bbox(array([[ 0.,  1.],
       [ 2.,  3.]]))

In the original (pre-Michael refactor) transformation scheme, the dataLim gave the bounding box of the untransformed data, the viewLim gave the bounding box of the view port, and the transformation took you from data limits to figure pixels. The reason the y-data in the blended axes lines like axvline were not considered was because these were already considered "pre-transformed" if you will, and hence not part of the data limits. Ie, only the raw, untransformed data were included in the datalim.

This early scheme obviously wasn't robust enough to handle all the wonderful transformations we can now support following Michael's refactor, but he left those attributes (dataLim, viewLim) in for backwards compatibility, and I think some of the weirdness we are now seeing is a result of a more thorough treatment of transformations trying to maintain as much backwards compatibility as possible.

I mention all of this just for historical context so you might understand why some of the weird things you are seeing are in there. I am not advocating slavish backwards compatibility, because I'd rather have a correct and robust system going forward, and you've done an admirable job in your patch supporting these idiosyncracies already. What I'm trying to understand is why the dataLim in the case of the initial bug you proposed should utilize the transform at all, when the dataLim in the other cases we have considered (log scales, polar transforms, funky blended transforms from axvline etc), all ignore the transformation.

I haven't yet gotten to your second "quite confusing bug" :-)

Michael Droettboom
Owner

I have to agree with John that a lot of work has clearly been put into this pull request on some of the trickiest parts of the code, without much guidance from the original author (I've been busy on many non-matplotlib things lately). I hope we can (over time) simplify rather than further complicate the transforms system -- in the long term even by breaking backward compatibility if there's good benefits to doing so.

Let me address each bug independently.

1) I'm not sure it's actually a bug. I don't think it's ever been the case that the auto view limit algorithm takes into account the transformation of the data. The transform member of the artist is intended to be a way to get from data coordinates to the page, not to be a way to pre-transform the data coordinates into effectively other data coordinates. To put this another way, the "actual" data coordinates should have an impact on the ticking the view box, "transformed" data coordinates should not, and are not intended to. That's what the scale and projection infrastructure is for. The transform member is used, for example, by the "Shadow" patch to slightly offset the drawing of the data without affecting the data itself. (And I think it's the fault of documentation that that isn't very clear -- the first commit here says it's fixing it to work as documented, but I'm not sure what documentation you're referring to -- we should clarify anything that is misleading). Consider the case of a log transform -- if that was done by prepending a log transform to the line's transform member, there would be no way of communicating to the axes ticker that ticks should be handled differently. I think what is more appropriate, given the current imperfect framework, is to define a new scale or projection along these lines:

http://matplotlib.sourceforge.net/devel/add_new_projection.html?highlight=new%20projection

And, as defining a scale is somewhat heavyweight, providing a new interface to handle the simple case where one just wants to provide a 1D transformation function would be a nice new features, IMHO. It's is also possible to transform the data before passing it to matplotlib. But of course, understanding your use case here better may help us arrive at an even better solution.

As for 2): I agree it's a bug. Is there anyway you could pull out independently the solution just for that part? Also note you say the first result is correct -- I tend to agree, but with this pull request the former returns the same erroneous results as the latter.

3) Changing how transforms are defined: The current approach leaves some flexibility in that one can define three implementations, affine, non-affine and combined. "Combined" in most cases will be equivalent to affine + non-affine, but there may be cases where it is more efficient (particularly in code not written using numpy) to do both in a single swoop, and I wouldn't want to lose that flexibility. As it stands now, if a Transform subclass defines transform and not transform_non_affine, transform_non_affine implicitly delegates transform. (See transforms.py:1098). So the micro-optimization where transforms add transform_non_affine = transform is not strictly necessary, but it doesn't change the behavior. So, unless I'm missing something, I don't think your change is necessary (even if only for reducing keystrokes). One might argue that we should have transform delegate to transform_non_affine instead of the reverse, but that seems like change for change's sake. The reason it is the way it is now is that I assumed most people writing custom transformations would be writing non-affine ones, and this allows them to be ignorant of the whole affine/non-affine distinction and just write a method called transform.

Thanks for all your work on this -- it's great to have someone picking it apart and this level of detail. I hope I don't come across as discouraging pull requests -- in fact I'd love to pass this baton along and I think there's a lot of food for thought here. As for next steps, I think it might be most helpful to have an independent pull request just for #2, and continue to discuss ways of supporting the use case suggested by #1 (perhaps on a new issue).

Phil Elson
Collaborator

Mike, John,

Firstly, thank you very much for all of your time looking at this non trivial pull request, your feedback is massively valuable and I really appreciate it. I should add that normally when I say the word bug, it tends to mean "it is not behaving as I would expect"; I guess that is not its strict definition in the dictionary :-) .

I hope I don't come across as discouraging pull requests

Not at all. The beauty of github is that we can have in-depth discussions about how code should look and behave and I only see benefit from your input.

John, your example is a good one. In my opinion the current behaviour is undesirable:

>>> import matplotlib.pyplot as plt
>>> ax = plt.gca()
>>> line, = ax.plot([0.2, 0.3], [0.6, 0.7]) 
>>> ax.dataLim
Bbox(array([[ 0.2,  0.6],
       [ 0.3,  0.7]]))
>>> line_ax, = ax.plot([0, 1], [0.1, 0.9], transform=ax.transAxes) 
>>> ax.dataLim
Bbox(array([[ 0. ,  0.1],
       [ 1. ,  0.9]]))

It is my opinion that the dataLim should be unaffected by anything which does not involve the transData. If a transform only involves a part (be it x or y components) then only that part should affect the dataLim. This information is derivable (trans.contains_branch(self.transData) in this pull request) and doesn't need to be limited to the easily controlled cases such as axvline. I haven't yet overriden the behaviour of contains_branch for blended transforms, partially as it seems the result needs to be a 2-tuple rather than a single boolean, but certainly the capability should be fairly straight forward.

The transform member of the artist is intended to be a way to get from data coordinates to the page, not to be a way to pre-transform the data coordinates into effectively other data coordinates.

I guess this is the fundamental shift that this pull request is trying to achieve. The way the transform framework has been implemented means that there is great flexibility when it comes to handling data in different coordinate systems on the same plot (or projection in your terms) without having to "re-project" in advance (i.e. I can work in each datum's "native" coordinate system). Thanks to this I am able to plot polar data with a theta origin at pi/4 from north on the same plot as data with a different theta origin. Similarly, I am able to work with geospatial data from multiple map projections and put them all on to an arbitrary map projection - without having to re-project the data first:

ax = plt.axes(projection='map_projection1')
plt.plot(xdata_in_proj2_coords, ydata_in_proj2_coords, 
           transform=map_projection2_to_1_transform + ax.transData)

This code pretty much just works with v1.1, except for the data limit calculation which currently assumes that xdata_in_proj2_coords is in projection1's coordinate system.

I don't want to make this post to long, so I will leave it there for now with the hope that this has been sufficient to explain why I made this pull request and that it will help inform our discussion further.

All the best,

Phil Elson
Collaborator

Woops, I added (and have subsequently removed) a commit which I didn't intend to include.

Phil Elson
Collaborator

@mdboom: I am still keen to get this functionality in before the 20th. It will need a little work to address some of your concerns, and I hope to avoid the need for x_isdata and y_isdata.

Phil Elson
Collaborator

@mdboom: I have rebased and removed the use of x_isdata in favour of deriving this information from the transforms. This was surprisingly easy with the changes being proposed here, and for me is a good sign that the changes are valuable.

The things which I think need discussion (please add more if you have anything) are:

  • a usecase for "pre-transforming" ones data and allowing the dataLim and viewLim to reflect those values
  • undoing the "Changing how transforms are defined" (3)
  • extracting the fix to the transform non-affine + transform affine to a separate request

I suggest we have these discussions in this PR, but try to keep the posts short-ish. If it gets a bit noisy, we can always use the devel mailing list.

Phil Elson
Collaborator
A usecase for "pre-transforming" ones data

The example I gave previously is my primary usecase for making it possible to plot data which is not necessarily in the ax.transData coordinate system. I would like to be able to define matplotlib axes subclasses which represent map projections (e.g. Hammer) and be able to add lines, polygons and images from other cartographic projections (e.g. PlateCarree). I do not want to limit the user as to which cartographic projection they use for their data. To take a tangible case, suppose a user as an image in Mercator that they want to put next to a line in PlateCarree, onto a Robinson map. The syntax that I would like to achieve is:

plate_carree = MyProjectionLibrary.PlateCarree()
robinson = MyProjectionLibrary.Robinson()
mercator = MyProjectionLibrary.Mercator()

ax = plt.axes(projection=robinson)
plt.plot(lons, lats, transform=plate_carree, zorder=100)
ax.imshow(img, transform=mercator)

The beauty of this interface is that it is so familiar to a mpl user that they can just pick it up and run. Apart from the need for me to expose a _as_mpl_transform api which would provide parity with the _as_mpl_projection interface, this all just works (my axes subclass does the image transformations), except from the fact that the dataLim has been incorrectly set to be in plate_carree coordinates rather than robinson ones.

Phil Elson
Collaborator
Michael Droettboom
Owner

Ok -- @pelson: does that mean you're working on a fix?

I think, if we can, it would be nice to include this before the freeze to get it lots of random user testing before the release. This is one of the more "open heart surgery" PR's in the pipe.

Phil Elson
Collaborator

Does that mean you're working on a fix?

No, but I will do in the next 3 hours or so.

This is one of the more "open heart surgery" PR's in the pipe.

Ha. I see what you mean. I agree that, because the unit tests don't have full coverage, the only way we can have confidence with code is to put it out in the wild.

lib/matplotlib/patches.py
((7 lines not shown))
return artist.Artist.get_transform(self)
def get_patch_transform(self):
+ """
+ Return the :class:`~matplotlib.transforms.Transform` ... I'm not sure
Phil Elson Collaborator
pelson added a note

@mdboom: Would you know what these (get_patch_transform and get_data_transform) are for? I would like to get a one liner for their purpose.

Phil Elson Collaborator
pelson added a note

Needs resolving before merging.

Michael Droettboom Owner
mdboom added a note

data_transform maps the data coordinates to physical coordinates.

patch_transform maps the native coordinates of the patch to physical coordinates. For example, to draw a circle with a radius of 2, the original circle goes from (-1, -1) to (1, 1) (i.e. radius == 1), and the patch transform would scale it up to 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Phil Elson
Collaborator

Ok. I think this is in a good state now (changelog and tests polished a little). travis-ci seems to be having a little bit of a problem atm, which I don't think is related, so I haven't actually been able to test this on python3 just yet.

doc/api/api_changes.rst
((33 lines not shown))
+ >>> print(ax.viewLim)
+ Bbox('array([[ 0., 0.],\n [ 90., 90.]])')
+
+* One can now easily get a transform which goes from one transform's coordinate system
+ to another, in an optimized way, using the new subtract method on a transform. For instance,
+ to go from data coordinates to axes coordinates::
+
+ >>> import matplotlib.pyplot as plt
+ >>> ax = plt.axes()
+ >>> data2ax = ax.transData - ax.transAxes
+ >>> print(ax.transData.depth, ax.transAxes.depth)
+ 3, 1
+ >>> print(data2ax.depth)
+ 2
+
+ for versions before 2.0 this could only be achieved in a sub-optimal way, using
Phil Elson Collaborator
pelson added a note

version number is wrong. Should be 1.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/transforms.py
((19 lines not shown))
has_inverse = False
+ """True if this transform as a corresponding inverse transform."""
Phil Elson Collaborator
pelson added a note

as -> has

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/transforms.py
((6 lines not shown))
self._a = a
self._b = b
self.set_children(a, b)
self._mtx = None
+ if DEBUG:
+ def __str__(self):
+ return '(%s, %s)' % (self._a, self._b)
+
+ @property
+ def depth(self):
+ return self._a.depth + self._b.depth
+
+ def _iter_break_from_left_to_right(self):
+ for lh_compliment, rh_compliment in self._a._iter_break_from_left_to_right():
+ yield lh_compliment, rh_compliment + self._b
+ for lh_compliment, rh_compliment in self._b._iter_break_from_left_to_right():
+ yield self._a + lh_compliment, rh_compliment
+
+
Phil Elson Collaborator
pelson added a note

One too many newlines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Phil Elson
Collaborator

@mdboom: I don't know when you last read this, so I am holding back from rebasing as the only way you can have confidence in the rebase is to re-read the whole lot.

Merging by hand at this point is probably a better bet. Are you happy to do this, or would you like me to do it?

Michael Droettboom
Owner

Why don't you address the small documentation changes and do a rebase -- it's probably easiest to comment on the rebase here as a pull request than in a manual merge.

Phil Elson
Collaborator

Ok. Will do shortly. Just working on pickle PR.

Phil Elson
Collaborator

Ok. The conflicts were to do with the test_transform.py and api_changes.rst and were pretty straight forward. This is now good to go as far as I can see.

Michael Droettboom
Owner

Thanks for the rebase -- I'll have another look at this today, but it may not be until later in the day.

lib/matplotlib/lines.py
@@ -446,6 +446,10 @@ def recache(self, always=False):
self._invalidy = False
def _transform_path(self, subslice=None):
+ """
+ Puts a TransformedPath instance at self._transformed_path,
+ all invalidation of the transform is then handled by the TransformedPath instance.
Michael Droettboom Owner
mdboom added a note

Too long line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Michael Droettboom
Owner

Ok -- I think other than my nit about line length and filling in the docstrings for get_data_transform and get_patch_transform, I think this is good to go.

Phil Elson
Collaborator

Thanks Mike. All done.

Michael Droettboom mdboom merged commit 98f6eb2 into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 20, 2012
  1. Phil Elson

    Substantial change to transform to make it work as documented. The up…

    Phil Elson authored pelson committed
    …shot is that the dataLim determination is now working.
  2. Phil Elson

    Several bugs fixed, particularly with Polar & Geo.

    Phil Elson authored pelson committed
  3. Phil Elson

    All tests work as expected.

    Phil Elson authored pelson committed
  4. Phil Elson

    Made small, pre-pull changes.

    Phil Elson authored pelson committed
  5. Phil Elson

    Finall improvements to plot extent calculation.

    Phil Elson authored pelson committed
  6. Phil Elson

    Changes as a result of reading diffs prior to pull request.

    Phil Elson authored pelson committed
  7. Phil Elson
  8. Phil Elson
  9. Phil Elson

    Added appropriate change logs for new transform mechanims, fixed a bu…

    pelson authored
    …g (and updated appropriate test).
  10. Phil Elson
Commits on Aug 21, 2012
  1. Phil Elson
This page is out of date. Refresh to see the latest.
48 doc/api/api_changes.rst
View
@@ -72,6 +72,54 @@ Changes in 1.2.x
original keyword arguments will override any value provided by
*capthick*.
+* Transform subclassing behaviour is now subtly changed. If your transform
+ implements a non-affine transformation, then it should override the
+ ``transform_non_affine`` method, rather than the generic ``transform`` method.
+ Previously transforms would define ``transform`` and then copy the
+ method into ``transform_non_affine``:
+
+ class MyTransform(mtrans.Transform):
+ def transform(self, xy):
+ ...
+ transform_non_affine = transform
+
+ This approach will no longer function correctly and should be changed to:
+
+ class MyTransform(mtrans.Transform):
+ def transform_non_affine(self, xy):
+ ...
+
+* Artists no longer have ``x_isdata`` or ``y_isdata`` attributes; instead
+ any artist's transform can be interrogated with
+ ``artist_instance.get_transform().contains_branch(ax.transData)``
+
+* Lines added to an axes now take into account their transform when updating the
+ data and view limits. This means transforms can now be used as a pre-transform.
+ For instance:
+
+ >>> import matplotlib.pyplot as plt
+ >>> import matplotlib.transforms as mtrans
+ >>> ax = plt.axes()
+ >>> ax.plot(range(10), transform=mtrans.Affine2D().scale(10) + ax.transData)
+ >>> print(ax.viewLim)
+ Bbox('array([[ 0., 0.],\n [ 90., 90.]])')
+
+* One can now easily get a transform which goes from one transform's coordinate system
+ to another, in an optimized way, using the new subtract method on a transform. For instance,
+ to go from data coordinates to axes coordinates::
+
+ >>> import matplotlib.pyplot as plt
+ >>> ax = plt.axes()
+ >>> data2ax = ax.transData - ax.transAxes
+ >>> print(ax.transData.depth, ax.transAxes.depth)
+ 3, 1
+ >>> print(data2ax.depth)
+ 2
+
+ for versions before 1.2 this could only be achieved in a sub-optimal way, using
+ ``ax.transData + ax.transAxes.inverted()`` (depth is a new concept, but had it existed
+ it would return 4 for this example).
+
Changes in 1.1.x
================
2  lib/matplotlib/artist.py
View
@@ -101,8 +101,6 @@ def __init__(self):
self._remove_method = None
self._url = None
self._gid = None
- self.x_isdata = True # False to avoid updating Axes.dataLim with x
- self.y_isdata = True # with y
self._snap = None
def remove(self):
69 lib/matplotlib/axes.py
View
@@ -1461,17 +1461,52 @@ def add_line(self, line):
self._update_line_limits(line)
if not line.get_label():
- line.set_label('_line%d'%len(self.lines))
+ line.set_label('_line%d' % len(self.lines))
self.lines.append(line)
line._remove_method = lambda h: self.lines.remove(h)
return line
def _update_line_limits(self, line):
- p = line.get_path()
- if p.vertices.size > 0:
- self.dataLim.update_from_path(p, self.ignore_existing_data_limits,
- updatex=line.x_isdata,
- updatey=line.y_isdata)
+ """Figures out the data limit of the given line, updating self.dataLim."""
+ path = line.get_path()
+ if path.vertices.size == 0:
+ return
+
+ line_trans = line.get_transform()
+
+ if line_trans == self.transData:
+ data_path = path
+
+ elif any(line_trans.contains_branch_seperately(self.transData)):
+ # identify the transform to go from line's coordinates
+ # to data coordinates
+ trans_to_data = line_trans - self.transData
+
+ # if transData is affine we can use the cached non-affine component
+ # of line's path. (since the non-affine part of line_trans is
+ # entirely encapsulated in trans_to_data).
+ if self.transData.is_affine:
+ line_trans_path = line._get_transformed_path()
+ na_path, _ = line_trans_path.get_transformed_path_and_affine()
+ data_path = trans_to_data.transform_path_affine(na_path)
+ else:
+ data_path = trans_to_data.transform_path(path)
+ else:
+ # for backwards compatibility we update the dataLim with the
+ # coordinate range of the given path, even though the coordinate
+ # systems are completely different. This may occur in situations
+ # such as when ax.transAxes is passed through for absolute
+ # positioning.
+ data_path = path
+
+ if data_path.vertices.size > 0:
+ updatex, updatey = line_trans.contains_branch_seperately(
+ self.transData
+ )
+ self.dataLim.update_from_path(data_path,
+ self.ignore_existing_data_limits,
+ updatex=updatex,
+ updatey=updatey)
self.ignore_existing_data_limits = False
def add_patch(self, p):
@@ -1507,11 +1542,14 @@ def _update_patch_limits(self, patch):
if vertices.size > 0:
xys = patch.get_patch_transform().transform(vertices)
if patch.get_data_transform() != self.transData:
- transform = (patch.get_data_transform() +
- self.transData.inverted())
- xys = transform.transform(xys)
- self.update_datalim(xys, updatex=patch.x_isdata,
- updatey=patch.y_isdata)
+ patch_to_data = (patch.get_data_transform() -
+ self.transData)
+ xys = patch_to_data.transform(xys)
+
+ updatex, updatey = patch.get_transform().\
+ contains_branch_seperately(self.transData)
+ self.update_datalim(xys, updatex=updatex,
+ updatey=updatey)
def add_table(self, tab):
@@ -1599,13 +1637,13 @@ def _process_unit_info(self, xdata=None, ydata=None, kwargs=None):
if xdata is not None:
# we only need to update if there is nothing set yet.
if not self.xaxis.have_units():
- self.xaxis.update_units(xdata)
+ self.xaxis.update_units(xdata)
#print '\tset from xdata', self.xaxis.units
if ydata is not None:
# we only need to update if there is nothing set yet.
if not self.yaxis.have_units():
- self.yaxis.update_units(ydata)
+ self.yaxis.update_units(ydata)
#print '\tset from ydata', self.yaxis.units
# process kwargs 2nd since these will override default units
@@ -3424,7 +3462,6 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs):
trans = mtransforms.blended_transform_factory(
self.transAxes, self.transData)
l = mlines.Line2D([xmin,xmax], [y,y], transform=trans, **kwargs)
- l.x_isdata = False
self.add_line(l)
self.autoscale_view(scalex=False, scaley=scaley)
return l
@@ -3489,7 +3526,6 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs):
trans = mtransforms.blended_transform_factory(
self.transData, self.transAxes)
l = mlines.Line2D([x,x], [ymin,ymax] , transform=trans, **kwargs)
- l.y_isdata = False
self.add_line(l)
self.autoscale_view(scalex=scalex, scaley=False)
return l
@@ -3546,7 +3582,6 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs):
verts = (xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)
p = mpatches.Polygon(verts, **kwargs)
p.set_transform(trans)
- p.x_isdata = False
self.add_patch(p)
self.autoscale_view(scalex=False)
return p
@@ -3603,7 +3638,6 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs):
verts = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]
p = mpatches.Polygon(verts, **kwargs)
p.set_transform(trans)
- p.y_isdata = False
self.add_patch(p)
self.autoscale_view(scaley=False)
return p
@@ -3909,7 +3943,6 @@ def plot(self, *args, **kwargs):
self.add_line(line)
lines.append(line)
-
self.autoscale_view(scalex=scalex, scaley=scaley)
return lines
31 lib/matplotlib/lines.py
View
@@ -6,6 +6,8 @@
# TODO: expose cap and join style attrs
from __future__ import division, print_function
+import warnings
+
import numpy as np
from numpy import ma
from matplotlib import verbose
@@ -249,17 +251,15 @@ def contains(self, mouseevent):
if len(self._xy)==0: return False,{}
# Convert points to pixels
- if self._transformed_path is None:
- self._transform_path()
- path, affine = self._transformed_path.get_transformed_path_and_affine()
+ path, affine = self._get_transformed_path().get_transformed_path_and_affine()
path = affine.transform_path(path)
xy = path.vertices
xt = xy[:, 0]
yt = xy[:, 1]
# Convert pick radius from points to pixels
- if self.figure == None:
- warning.warn('no figure set when check if mouse is on line')
+ if self.figure is None:
+ warnings.warn('no figure set when check if mouse is on line')
pixels = self.pickradius
else:
pixels = self.figure.dpi/72. * self.pickradius
@@ -446,6 +446,11 @@ def recache(self, always=False):
self._invalidy = False
def _transform_path(self, subslice=None):
+ """
+ Puts a TransformedPath instance at self._transformed_path,
+ all invalidation of the transform is then handled by the
+ TransformedPath instance.
+ """
# Masked arrays are now handled by the Path class itself
if subslice is not None:
_path = Path(self._xy[subslice,:])
@@ -453,6 +458,14 @@ def _transform_path(self, subslice=None):
_path = self._path
self._transformed_path = TransformedPath(_path, self.get_transform())
+ def _get_transformed_path(self):
+ """
+ Return the :class:`~matplotlib.transforms.TransformedPath` instance
+ of this line.
+ """
+ if self._transformed_path is None:
+ self._transform_path()
+ return self._transformed_path
def set_transform(self, t):
"""
@@ -482,8 +495,8 @@ def draw(self, renderer):
subslice = slice(max(i0-1, 0), i1+1)
self.ind_offset = subslice.start
self._transform_path(subslice)
- if self._transformed_path is None:
- self._transform_path()
+
+ transformed_path = self._get_transformed_path()
if not self.get_visible(): return
@@ -507,7 +520,7 @@ def draw(self, renderer):
funcname = self._lineStyles.get(self._linestyle, '_draw_nothing')
if funcname != '_draw_nothing':
- tpath, affine = self._transformed_path.get_transformed_path_and_affine()
+ tpath, affine = transformed_path.get_transformed_path_and_affine()
if len(tpath.vertices):
self._lineFunc = getattr(self, funcname)
funcname = self.drawStyles.get(self._drawstyle, '_draw_lines')
@@ -528,7 +541,7 @@ def draw(self, renderer):
gc.set_linewidth(self._markeredgewidth)
gc.set_alpha(self._alpha)
marker = self._marker
- tpath, affine = self._transformed_path.get_transformed_points_and_affine()
+ tpath, affine = transformed_path.get_transformed_points_and_affine()
if len(tpath.vertices):
# subsample the markers if markevery is not None
markevery = self.get_markevery()
12 lib/matplotlib/patches.py
View
@@ -167,9 +167,21 @@ def get_transform(self):
return self.get_patch_transform() + artist.Artist.get_transform(self)
def get_data_transform(self):
+ """
+ Return the :class:`~matplotlib.transforms.Transform` instance which
+ maps data coordinates to physical coordinates.
+ """
return artist.Artist.get_transform(self)
def get_patch_transform(self):
+ """
+ Return the :class:`~matplotlib.transforms.Transform` instance which
+ takes patch coordinates to data coordinates.
+
+ For example, one may define a patch of a circle which represents a
+ radius of 5 by providing coordinates for a unit circle, and a
+ transform which scales the coordinates (the patch coordinate) by 5.
+ """
return transforms.IdentityTransform()
def get_antialiased(self):
56 lib/matplotlib/projections/geo.py
View
@@ -263,7 +263,7 @@ def __init__(self, resolution):
Transform.__init__(self)
self._resolution = resolution
- def transform(self, ll):
+ def transform_non_affine(self, ll):
longitude = ll[:, 0:1]
latitude = ll[:, 1:2]
@@ -282,18 +282,12 @@ def transform(self, ll):
x = (cos_latitude * ma.sin(half_long)) / sinc_alpha
y = (ma.sin(latitude) / sinc_alpha)
return np.concatenate((x.filled(0), y.filled(0)), 1)
- transform.__doc__ = Transform.transform.__doc__
-
- transform_non_affine = transform
transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
- def transform_path(self, path):
+ def transform_path_non_affine(self, path):
vertices = path.vertices
ipath = path.interpolated(self._resolution)
return Path(self.transform(ipath.vertices), ipath.codes)
- transform_path.__doc__ = Transform.transform_path.__doc__
-
- transform_path_non_affine = transform_path
transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self):
@@ -309,10 +303,10 @@ def __init__(self, resolution):
Transform.__init__(self)
self._resolution = resolution
- def transform(self, xy):
+ def transform_non_affine(self, xy):
# MGDTODO: Math is hard ;(
return xy
- transform.__doc__ = Transform.transform.__doc__
+ transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def inverted(self):
return AitoffAxes.AitoffTransform(self._resolution)
@@ -348,7 +342,7 @@ def __init__(self, resolution):
Transform.__init__(self)
self._resolution = resolution
- def transform(self, ll):
+ def transform_non_affine(self, ll):
longitude = ll[:, 0:1]
latitude = ll[:, 1:2]
@@ -361,18 +355,12 @@ def transform(self, ll):
x = (2.0 * sqrt2) * (cos_latitude * np.sin(half_long)) / alpha
y = (sqrt2 * np.sin(latitude)) / alpha
return np.concatenate((x, y), 1)
- transform.__doc__ = Transform.transform.__doc__
-
- transform_non_affine = transform
transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
- def transform_path(self, path):
+ def transform_path_non_affine(self, path):
vertices = path.vertices
ipath = path.interpolated(self._resolution)
return Path(self.transform(ipath.vertices), ipath.codes)
- transform_path.__doc__ = Transform.transform_path.__doc__
-
- transform_path_non_affine = transform_path
transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self):
@@ -388,7 +376,7 @@ def __init__(self, resolution):
Transform.__init__(self)
self._resolution = resolution
- def transform(self, xy):
+ def transform_non_affine(self, xy):
x = xy[:, 0:1]
y = xy[:, 1:2]
@@ -398,7 +386,7 @@ def transform(self, xy):
longitude = 2 * np.arctan((z*x) / (2.0 * (2.0*z*z - 1.0)))
latitude = np.arcsin(y*z)
return np.concatenate((longitude, latitude), 1)
- transform.__doc__ = Transform.transform.__doc__
+ transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def inverted(self):
return HammerAxes.HammerTransform(self._resolution)
@@ -434,7 +422,7 @@ def __init__(self, resolution):
Transform.__init__(self)
self._resolution = resolution
- def transform(self, ll):
+ def transform_non_affine(self, ll):
def d(theta):
delta = -(theta + np.sin(theta) - pi_sin_l) / (1 + np.cos(theta))
return delta, np.abs(delta) > 0.001
@@ -466,18 +454,12 @@ def d(theta):
xy[:,1] = np.sqrt(2.0) * np.sin(aux)
return xy
- transform.__doc__ = Transform.transform.__doc__
-
- transform_non_affine = transform
transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
- def transform_path(self, path):
+ def transform_path_non_affine(self, path):
vertices = path.vertices
ipath = path.interpolated(self._resolution)
return Path(self.transform(ipath.vertices), ipath.codes)
- transform_path.__doc__ = Transform.transform_path.__doc__
-
- transform_path_non_affine = transform_path
transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self):
@@ -493,10 +475,10 @@ def __init__(self, resolution):
Transform.__init__(self)
self._resolution = resolution
- def transform(self, xy):
+ def transform_non_affine(self, xy):
# MGDTODO: Math is hard ;(
return xy
- transform.__doc__ = Transform.transform.__doc__
+ transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def inverted(self):
return MollweideAxes.MollweideTransform(self._resolution)
@@ -534,7 +516,7 @@ def __init__(self, center_longitude, center_latitude, resolution):
self._center_longitude = center_longitude
self._center_latitude = center_latitude
- def transform(self, ll):
+ def transform_non_affine(self, ll):
longitude = ll[:, 0:1]
latitude = ll[:, 1:2]
clong = self._center_longitude
@@ -555,18 +537,12 @@ def transform(self, ll):
np.sin(clat)*cos_lat*cos_diff_long)
return np.concatenate((x, y), 1)
- transform.__doc__ = Transform.transform.__doc__
-
- transform_non_affine = transform
transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
- def transform_path(self, path):
+ def transform_path_non_affine(self, path):
vertices = path.vertices
ipath = path.interpolated(self._resolution)
return Path(self.transform(ipath.vertices), ipath.codes)
- transform_path.__doc__ = Transform.transform_path.__doc__
-
- transform_path_non_affine = transform_path
transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self):
@@ -587,7 +563,7 @@ def __init__(self, center_longitude, center_latitude, resolution):
self._center_longitude = center_longitude
self._center_latitude = center_latitude
- def transform(self, xy):
+ def transform_non_affine(self, xy):
x = xy[:, 0:1]
y = xy[:, 1:2]
clong = self._center_longitude
@@ -604,7 +580,7 @@ def transform(self, xy):
(x*sin_c) / (p*np.cos(clat)*cos_c - y*np.sin(clat)*sin_c))
return np.concatenate((long, lat), 1)
- transform.__doc__ = Transform.transform.__doc__
+ transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def inverted(self):
return LambertAxes.LambertTransform(
14 lib/matplotlib/projections/polar.py
View
@@ -42,7 +42,7 @@ def __init__(self, axis=None, use_rmin=True):
self._axis = axis
self._use_rmin = use_rmin
- def transform(self, tr):
+ def transform_non_affine(self, tr):
xy = np.empty(tr.shape, np.float_)
if self._axis is not None:
if self._use_rmin:
@@ -74,20 +74,14 @@ def transform(self, tr):
y[:] = r * np.sin(t)
return xy
- transform.__doc__ = Transform.transform.__doc__
-
- transform_non_affine = transform
transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
- def transform_path(self, path):
+ def transform_path_non_affine(self, path):
vertices = path.vertices
if len(vertices) == 2 and vertices[0, 0] == vertices[1, 0]:
return Path(self.transform(vertices), path.codes)
ipath = path.interpolated(path._interpolation_steps)
return Path(self.transform(ipath.vertices), ipath.codes)
- transform_path.__doc__ = Transform.transform_path.__doc__
-
- transform_path_non_affine = transform_path
transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def inverted(self):
@@ -138,7 +132,7 @@ def __init__(self, axis=None, use_rmin=True):
self._axis = axis
self._use_rmin = use_rmin
- def transform(self, xy):
+ def transform_non_affine(self, xy):
if self._axis is not None:
if self._use_rmin:
rmin = self._axis.viewLim.ymin
@@ -163,7 +157,7 @@ def transform(self, xy):
r += rmin
return np.concatenate((theta, r), 1)
- transform.__doc__ = Transform.transform.__doc__
+ transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def inverted(self):
return PolarAxes.PolarTransform(self._axis, self._use_rmin)
265 lib/matplotlib/tests/test_transforms.py
View
@@ -1,5 +1,8 @@
from __future__ import print_function
-from nose.tools import assert_equal
+import unittest
+
+from nose.tools import assert_equal, assert_raises
+import numpy.testing as np_test
from numpy.testing import assert_almost_equal
from matplotlib.transforms import Affine2D, BlendedGenericTransform
from matplotlib.path import Path
@@ -9,6 +12,8 @@
import matplotlib.transforms as mtrans
import matplotlib.pyplot as plt
+import matplotlib.path as mpath
+import matplotlib.patches as mpatches
@@ -106,37 +111,37 @@ def test_pre_transform_plotting():
def test_Affine2D_from_values():
- points = [ [0,0],
+ points = np.array([ [0,0],
[10,20],
[-1,0],
- ]
+ ])
- t = Affine2D.from_values(1,0,0,0,0,0)
+ t = mtrans.Affine2D.from_values(1,0,0,0,0,0)
actual = t.transform(points)
expected = np.array( [[0,0],[10,0],[-1,0]] )
assert_almost_equal(actual,expected)
- t = Affine2D.from_values(0,2,0,0,0,0)
+ t = mtrans.Affine2D.from_values(0,2,0,0,0,0)
actual = t.transform(points)
expected = np.array( [[0,0],[0,20],[0,-2]] )
assert_almost_equal(actual,expected)
- t = Affine2D.from_values(0,0,3,0,0,0)
+ t = mtrans.Affine2D.from_values(0,0,3,0,0,0)
actual = t.transform(points)
expected = np.array( [[0,0],[60,0],[0,0]] )
assert_almost_equal(actual,expected)
- t = Affine2D.from_values(0,0,0,4,0,0)
+ t = mtrans.Affine2D.from_values(0,0,0,4,0,0)
actual = t.transform(points)
expected = np.array( [[0,0],[0,80],[0,0]] )
assert_almost_equal(actual,expected)
- t = Affine2D.from_values(0,0,0,0,5,0)
+ t = mtrans.Affine2D.from_values(0,0,0,0,5,0)
actual = t.transform(points)
expected = np.array( [[5,0],[5,0],[5,0]] )
assert_almost_equal(actual,expected)
- t = Affine2D.from_values(0,0,0,0,0,6)
+ t = mtrans.Affine2D.from_values(0,0,0,0,0,6)
actual = t.transform(points)
expected = np.array( [[0,6],[0,6],[0,6]] )
assert_almost_equal(actual,expected)
@@ -165,6 +170,246 @@ def test_clipping_of_log():
assert np.allclose(tpoints[-1], tpoints[0])
+class NonAffineForTest(mtrans.Transform):
+ """
+ A class which looks like a non affine transform, but does whatever
+ the given transform does (even if it is affine). This is very useful
+ for testing NonAffine behaviour with a simple Affine transform.
+
+ """
+ is_affine = False
+ output_dims = 2
+ input_dims = 2
+
+ def __init__(self, real_trans, *args, **kwargs):
+ self.real_trans = real_trans
+ r = mtrans.Transform.__init__(self, *args, **kwargs)
+
+ def transform_non_affine(self, values):
+ return self.real_trans.transform(values)
+
+ def transform_path_non_affine(self, path):
+ return self.real_trans.transform_path(path)
+
+
+class BasicTransformTests(unittest.TestCase):
+ def setUp(self):
+
+ self.ta1 = mtrans.Affine2D(shorthand_name='ta1').rotate(np.pi / 2)
+ self.ta2 = mtrans.Affine2D(shorthand_name='ta2').translate(10, 0)
+ self.ta3 = mtrans.Affine2D(shorthand_name='ta3').scale(1, 2)
+
+ self.tn1 = NonAffineForTest(mtrans.Affine2D().translate(1, 2), shorthand_name='tn1')
+ self.tn2 = NonAffineForTest(mtrans.Affine2D().translate(1, 2), shorthand_name='tn2')
+ self.tn3 = NonAffineForTest(mtrans.Affine2D().translate(1, 2), shorthand_name='tn3')
+
+ # creates a transform stack which looks like ((A, (N, A)), A)
+ self.stack1 = (self.ta1 + (self.tn1 + self.ta2)) + self.ta3
+ # creates a transform stack which looks like (((A, N), A), A)
+ self.stack2 = self.ta1 + self.tn1 + self.ta2 + self.ta3
+ # creates a transform stack which is a subset of stack2
+ self.stack2_subset = self.tn1 + self.ta2 + self.ta3
+
+ # when in debug, the transform stacks can produce dot images:
+# self.stack1.write_graphviz(file('stack1.dot', 'w'))
+# self.stack2.write_graphviz(file('stack2.dot', 'w'))
+# self.stack2_subset.write_graphviz(file('stack2_subset.dot', 'w'))
+
+ def test_transform_depth(self):
+ assert_equal(self.stack1.depth, 4)
+ assert_equal(self.stack2.depth, 4)
+ assert_equal(self.stack2_subset.depth, 3)
+
+ def test_left_to_right_iteration(self):
+ stack3 = (self.ta1 + (self.tn1 + (self.ta2 + self.tn2))) + self.ta3
+# stack3.write_graphviz(file('stack3.dot', 'w'))
+
+ target_transforms = [stack3,
+ (self.tn1 + (self.ta2 + self.tn2)) + self.ta3,
+ (self.ta2 + self.tn2) + self.ta3,
+ self.tn2 + self.ta3,
+ self.ta3,
+ ]
+ r = [rh for _, rh in stack3._iter_break_from_left_to_right()]
+ self.assertEqual(len(r), len(target_transforms))
+
+ for target_stack, stack in zip(target_transforms, r):
+ self.assertEqual(target_stack, stack)
+
+ def test_transform_shortcuts(self):
+ self.assertEqual(self.stack1 - self.stack2_subset, self.ta1)
+ self.assertEqual(self.stack2 - self.stack2_subset, self.ta1)
+
+ assert_equal((self.stack2_subset - self.stack2),
+ self.ta1.inverted(),
+ )
+ assert_equal((self.stack2_subset - self.stack2).depth, 1)
+
+ assert_raises(ValueError, self.stack1.__sub__, self.stack2)
+
+ aff1 = self.ta1 + (self.ta2 + self.ta3)
+ aff2 = self.ta2 + self.ta3
+
+ self.assertEqual(aff1 - aff2, self.ta1)
+ self.assertEqual(aff1 - self.ta2, aff1 + self.ta2.inverted())
+
+ self.assertEqual(self.stack1 - self.ta3, self.ta1 + (self.tn1 + self.ta2))
+ self.assertEqual(self.stack2 - self.ta3, self.ta1 + self.tn1 + self.ta2)
+
+ self.assertEqual((self.ta2 + self.ta3) - self.ta3 + self.ta3, self.ta2 + self.ta3)
+
+ def test_contains_branch(self):
+ r1 = (self.ta2 + self.ta1)
+ r2 = (self.ta2 + self.ta1)
+ self.assertEqual(r1, r2)
+ self.assertNotEqual(r1, self.ta1)
+ self.assertTrue(r1.contains_branch(r2))
+ self.assertTrue(r1.contains_branch(self.ta1))
+ self.assertFalse(r1.contains_branch(self.ta2))
+ self.assertFalse(r1.contains_branch((self.ta2 + self.ta2)))
+
+ self.assertEqual(r1, r2)
+
+ self.assertTrue(self.stack1.contains_branch(self.ta3))
+ self.assertTrue(self.stack2.contains_branch(self.ta3))
+
+ self.assertTrue(self.stack1.contains_branch(self.stack2_subset))
+ self.assertTrue(self.stack2.contains_branch(self.stack2_subset))
+
+ self.assertFalse(self.stack2_subset.contains_branch(self.stack1))
+ self.assertFalse(self.stack2_subset.contains_branch(self.stack2))
+
+ self.assertTrue(self.stack1.contains_branch((self.ta2 + self.ta3)))
+ self.assertTrue(self.stack2.contains_branch((self.ta2 + self.ta3)))
+
+ self.assertFalse(self.stack1.contains_branch((self.tn1 + self.ta2)))
+
+ def test_affine_simplification(self):
+ # tests that a transform stack only calls as much is absolutely necessary
+ # "non-affine" allowing the best possible optimization with complex
+ # transformation stacks.
+ points = np.array([[0, 0], [10, 20], [np.nan, 1], [-1, 0]], dtype=np.float64)
+ na_pts = self.stack1.transform_non_affine(points)
+ all_pts = self.stack1.transform(points)
+
+ na_expected = np.array([[1., 2.], [-19., 12.],
+ [np.nan, np.nan], [1., 1.]], dtype=np.float64)
+ all_expected = np.array([[11., 4.], [-9., 24.],
+ [np.nan, np.nan], [11., 2.]], dtype=np.float64)
+
+ # check we have the expected results from doing the affine part only
+ np_test.assert_array_almost_equal(na_pts, na_expected)
+ # check we have the expected results from a full transformation
+ np_test.assert_array_almost_equal(all_pts, all_expected)
+ # check we have the expected results from doing the transformation in two steps
+ np_test.assert_array_almost_equal(self.stack1.transform_affine(na_pts), all_expected)
+ # check that getting the affine transformation first, then fully transforming using that
+ # yields the same result as before.
+ np_test.assert_array_almost_equal(self.stack1.get_affine().transform(na_pts), all_expected)
+
+ # check that the affine part of stack1 & stack2 are equivalent (i.e. the optimization
+ # is working)
+ expected_result = (self.ta2 + self.ta3).get_matrix()
+ result = self.stack1.get_affine().get_matrix()
+ np_test.assert_array_equal(expected_result, result)
+
+ result = self.stack2.get_affine().get_matrix()
+ np_test.assert_array_equal(expected_result, result)
+
+
+class TestTransformPlotInterface(unittest.TestCase):
+ def tearDown(self):
+ plt.close()
+
+ def test_line_extent_axes_coords(self):
+ # a simple line in axes coordinates
+ ax = plt.axes()
+ ax.plot([0.1, 1.2, 0.8], [0.9, 0.5, 0.8], transform=ax.transAxes)
+ np.testing.assert_array_equal(ax.dataLim.get_points(), np.array([[0, 0], [1, 1]]))
+
+ def test_line_extent_data_coords(self):
+ # a simple line in data coordinates
+ ax = plt.axes()
+ ax.plot([0.1, 1.2, 0.8], [0.9, 0.5, 0.8], transform=ax.transData)
+ np.testing.assert_array_equal(ax.dataLim.get_points(), np.array([[ 0.1, 0.5], [ 1.2, 0.9]]))
+
+ def test_line_extent_compound_coords1(self):
+ # a simple line in data coordinates in the y component, and in axes coordinates in the x
+ ax = plt.axes()
+ trans = mtrans.blended_transform_factory(ax.transAxes, ax.transData)
+ ax.plot([0.1, 1.2, 0.8], [35, -5, 18], transform=trans)
+ np.testing.assert_array_equal(ax.dataLim.get_points(), np.array([[ 0., -5.], [ 1., 35.]]))
+ plt.close()
+
+ def test_line_extent_predata_transform_coords(self):
+ # a simple line in (offset + data) coordinates
+ ax = plt.axes()
+ trans = mtrans.Affine2D().scale(10) + ax.transData
+ ax.plot([0.1, 1.2, 0.8], [35, -5, 18], transform=trans)
+ np.testing.assert_array_equal(ax.dataLim.get_points(), np.array([[1., -50.], [12., 350.]]))
+ plt.close()
+
+ def test_line_extent_compound_coords2(self):
+ # a simple line in (offset + data) coordinates in the y component, and in axes coordinates in the x
+ ax = plt.axes()
+ trans = mtrans.blended_transform_factory(ax.transAxes, mtrans.Affine2D().scale(10) + ax.transData)
+ ax.plot([0.1, 1.2, 0.8], [35, -5, 18], transform=trans)
+ np.testing.assert_array_equal(ax.dataLim.get_points(), np.array([[ 0., -50.], [ 1., 350.]]))
+ plt.close()
+
+ def test_line_extents_affine(self):
+ ax = plt.axes()
+ offset = mtrans.Affine2D().translate(10, 10)
+ plt.plot(range(10), transform=offset + ax.transData)
+ expeted_data_lim = np.array([[0., 0.], [9., 9.]]) + 10
+ np.testing.assert_array_almost_equal(ax.dataLim.get_points(),
+ expeted_data_lim)
+
+ def test_line_extents_non_affine(self):
+ ax = plt.axes()
+ offset = mtrans.Affine2D().translate(10, 10)
+ na_offset = NonAffineForTest(mtrans.Affine2D().translate(10, 10))
+ plt.plot(range(10), transform=offset + na_offset + ax.transData)
+ expeted_data_lim = np.array([[0., 0.], [9., 9.]]) + 20
+ np.testing.assert_array_almost_equal(ax.dataLim.get_points(),
+ expeted_data_lim)
+
+ def test_pathc_extents_non_affine(self):
+ ax = plt.axes()
+ offset = mtrans.Affine2D().translate(10, 10)
+ na_offset = NonAffineForTest(mtrans.Affine2D().translate(10, 10))
+ pth = mpath.Path(np.array([[0, 0], [0, 10], [10, 10], [10, 0]]))
+ patch = mpatches.PathPatch(pth, transform=offset + na_offset + ax.transData)
+ ax.add_patch(patch)
+ expeted_data_lim = np.array([[0., 0.], [10., 10.]]) + 20
+ np.testing.assert_array_almost_equal(ax.dataLim.get_points(),
+ expeted_data_lim)
+
+ def test_pathc_extents_affine(self):
+ ax = plt.axes()
+ offset = mtrans.Affine2D().translate(10, 10)
+ pth = mpath.Path(np.array([[0, 0], [0, 10], [10, 10], [10, 0]]))
+ patch = mpatches.PathPatch(pth, transform=offset + ax.transData)
+ ax.add_patch(patch)
+ expeted_data_lim = np.array([[0., 0.], [10., 10.]]) + 10
+ np.testing.assert_array_almost_equal(ax.dataLim.get_points(),
+ expeted_data_lim)
+
+
+ def test_line_extents_for_non_affine_transData(self):
+ ax = plt.axes(projection='polar')
+ # add 10 to the radius of the data
+ offset = mtrans.Affine2D().translate(0, 10)
+
+ plt.plot(range(10), transform=offset + ax.transData)
+ # the data lim of a polar plot is stored in coordinates
+ # before a transData transformation, hence the data limits
+ # are not what is being shown on the actual plot.
+ expeted_data_lim = np.array([[0., 0.], [9., 9.]]) + [0, 10]
+ np.testing.assert_array_almost_equal(ax.dataLim.get_points(),
+ expeted_data_lim)
+
+
if __name__=='__main__':
import nose
- nose.runmodule(argv=['-s','--with-doctest'], exit=False)
+ nose.runmodule(argv=['-s','--with-doctest'], exit=False)
487 lib/matplotlib/transforms.py
View
@@ -36,19 +36,16 @@
update_path_extents)
from numpy.linalg import inv
-from weakref import WeakKeyDictionary
+from weakref import WeakValueDictionary
import warnings
try:
set
except NameError:
from sets import Set as set
-import cbook
from path import Path
DEBUG = False
-if DEBUG:
- import warnings
MaskedArray = ma.MaskedArray
@@ -74,22 +71,35 @@ class TransformNode(object):
is_affine = False
is_bbox = False
- # If pass_through is True, all ancestors will always be
- # invalidated, even if 'self' is already invalid.
pass_through = False
+ """
+ If pass_through is True, all ancestors will always be
+ invalidated, even if 'self' is already invalid.
+ """
- def __init__(self):
+ def __init__(self, shorthand_name=None):
"""
Creates a new :class:`TransformNode`.
+
+ **shorthand_name** - a string representing the "name" of this
+ transform. The name carries no significance
+ other than to improve the readability of
+ ``str(transform)`` when DEBUG=True.
"""
# Parents are stored in a WeakKeyDictionary, so that if the
# parents are deleted, references from the children won't keep
# them alive.
- self._parents = WeakKeyDictionary()
+ self._parents = WeakValueDictionary()
# 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 it's repr
+ return self._shorthand_name or repr(self)
def __copy__(self, *args):
raise NotImplementedError(
@@ -128,7 +138,7 @@ def _invalidate_internal(self, value, invalidating_node):
if self.pass_through or status_changed:
self._invalid = value
- for parent in self._parents.iterkeys():
+ for parent in self._parents.itervalues():
parent._invalidate_internal(value=value, invalidating_node=self)
def set_children(self, *children):
@@ -139,7 +149,7 @@ def set_children(self, *children):
depend on other transforms.
"""
for child in children:
- child._parents[self] = None
+ child._parents[id(self)] = self
if DEBUG:
_set_children = set_children
@@ -170,6 +180,12 @@ def write_graphviz(self, fobj, highlight=[]):
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()
@@ -197,7 +213,7 @@ def recurse(root):
if val is child:
name = key
break
- fobj.write('%s -> %s [label="%s", fontsize=10];\n' % (
+ fobj.write('"%s" -> "%s" [label="%s", fontsize=10];\n' % (
hash(root),
hash(child),
name))
@@ -206,9 +222,6 @@ def recurse(root):
fobj.write("digraph G {\n")
recurse(self)
fobj.write("}\n")
- else:
- def write_graphviz(self, fobj, highlight=[]):
- return
class BboxBase(TransformNode):
@@ -707,7 +720,7 @@ class Bbox(BboxBase):
A mutable bounding box.
"""
- def __init__(self, points):
+ def __init__(self, points, **kwargs):
"""
*points*: a 2x2 numpy array of the form [[x0, y0], [x1, y1]]
@@ -715,7 +728,7 @@ def __init__(self, points):
of data, consider the static methods :meth:`unit`,
:meth:`from_bounds` and :meth:`from_extents`.
"""
- BboxBase.__init__(self)
+ BboxBase.__init__(self, **kwargs)
self._points = np.asarray(points, np.float_)
self._minpos = np.array([0.0000001, 0.0000001])
self._ignore = True
@@ -725,9 +738,9 @@ def __init__(self, points):
self._points_orig = self._points.copy()
if DEBUG:
___init__ = __init__
- def __init__(self, points):
+ def __init__(self, points, **kwargs):
self._check(points)
- self.___init__(points)
+ self.___init__(points, **kwargs)
def invalidate(self):
self._check(self._points)
@@ -764,8 +777,7 @@ def from_extents(*args):
return Bbox(points)
def __repr__(self):
- return 'Bbox(%s)' % repr(self._points)
- __str__ = __repr__
+ return 'Bbox(%r)' % repr(self._points)
def ignore(self, value):
"""
@@ -819,6 +831,7 @@ def update_from_path(self, path, ignore=None, updatex=True, updatey=True):
*updatex*: when True, update the x values
*updatey*: when True, update the y values
+
"""
if ignore is None:
ignore = self._ignore
@@ -964,15 +977,13 @@ def mutatedy(self):
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):
+ def __init__(self, bbox, transform, **kwargs):
"""
*bbox*: a child :class:`Bbox`
@@ -983,15 +994,14 @@ def __init__(self, bbox, transform):
assert transform.input_dims == 2
assert transform.output_dims == 2
- BboxBase.__init__(self)
+ BboxBase.__init__(self, **kwargs)
self._bbox = bbox
self._transform = transform
self.set_children(bbox, transform)
self._points = None
def __repr__(self):
- return "TransformedBbox(%s, %s)" % (self._bbox, self._transform)
- __str__ = __repr__
+ return "TransformedBbox(%r, %r)" % (self._bbox, self._transform)
def get_points(self):
if self._invalid:
@@ -1009,6 +1019,7 @@ def get_points(self):
self._check(points)
return points
+
class Transform(TransformNode):
"""
The base class of all :class:`TransformNode` instances that
@@ -1026,7 +1037,7 @@ class Transform(TransformNode):
- :meth:`transform`
- :attr:`is_separable`
- :attr:`has_inverse`
- - :meth:`inverted` (if :meth:`has_inverse` can return True)
+ - :meth:`inverted` (if :attr:`has_inverse` is True)
If the transform needs to do something non-standard with
:class:`matplotlib.path.Path` objects, such as adding curves
@@ -1034,21 +1045,23 @@ class Transform(TransformNode):
- :meth:`transform_path`
"""
- # The number of input and output dimensions for this transform.
- # These must be overridden (with integers) in the subclass.
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.
+ """
- # True if this transform as a corresponding inverse transform.
has_inverse = False
+ """True if this transform has a corresponding inverse transform."""
- # True if this transform is separable in the x- and y- dimensions.
is_separable = False
-
- #* Redundant: Removed for performance
- #
- # def __init__(self):
- # TransformNode.__init__(self)
+ """True if this transform is separable in the x- and y- dimensions."""
def __add__(self, other):
"""
@@ -1070,15 +1083,124 @@ def __radd__(self, other):
raise TypeError(
"Can not add Transform to object of type '%s'" % type(other))
- def __array__(self, *args, **kwargs):
+ def __eq__(self, other):
+ # equality is based on transform object id. Hence:
+ # Transform() != Transform().
+ # Some classes, such as TransformWrapper & AffineBase, will override.
+ return self is other
+
+ def _iter_break_from_left_to_right(self):
"""
- Array interface to get at this Transform's matrix.
+ 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).
+
"""
- # note, this method is also used by C/C++ -based backends
- if self.is_affine:
- return self.get_matrix()
+ 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 seperate 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 seperate 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 decending 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's transform includes a non-invertable 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('Cannot convert this transform to an array.')
+ 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):
"""
@@ -1087,7 +1209,7 @@ def transform(self, values):
Accepts a numpy array of shape (N x :attr:`input_dims`) and
returns a numpy array of shape (N x :attr:`output_dims`).
"""
- raise NotImplementedError()
+ return self.transform_affine(self.transform_non_affine(values))
def transform_affine(self, values):
"""
@@ -1104,7 +1226,7 @@ def transform_affine(self, values):
Accepts a numpy array of shape (N x :attr:`input_dims`) and
returns a numpy array of shape (N x :attr:`output_dims`).
"""
- return values
+ return self.get_affine().transform(values)
def transform_non_affine(self, values):
"""
@@ -1120,7 +1242,7 @@ def transform_non_affine(self, values):
Accepts a numpy array of shape (N x :attr:`input_dims`) and
returns a numpy array of shape (N x :attr:`output_dims`).
"""
- return self.transform(values)
+ return values
def get_affine(self):
"""
@@ -1130,7 +1252,9 @@ def get_affine(self):
def get_matrix(self):
"""
- Get the transformation matrix for the affine part of this transform.
+ Get the Affine transformation array for the affine part
+ of this transform.
+
"""
return self.get_affine().get_matrix()
@@ -1148,19 +1272,18 @@ def transform_point(self, point):
def transform_path(self, path):
"""
- Returns a transformed copy of 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 Path(self.transform(path.vertices), path.codes,
- path._interpolation_steps)
+ return self.transform_path_affine(self.transform_path_non_affine(path))
def transform_path_affine(self, path):
"""
- Returns a copy of path, transformed only by the affine part of
+ Returns a path, transformed only by the affine part of
this transform.
*path*: a :class:`~matplotlib.path.Path` instance.
@@ -1168,11 +1291,11 @@ def transform_path_affine(self, path):
``transform_path(path)`` is equivalent to
``transform_path_affine(transform_path_non_affine(values))``.
"""
- return path
+ return self.get_affine().transform_path_affine(path)
def transform_path_non_affine(self, path):
"""
- Returns a copy of path, transformed only by the non-affine
+ Returns a path, transformed only by the non-affine
part of this transform.
*path*: a :class:`~matplotlib.path.Path` instance.
@@ -1282,9 +1405,15 @@ def __init__(self, child):
self._set(child)
self._invalid = 0
+ def __eq__(self, other):
+ return self._child.__eq__(other)
+
+ if DEBUG:
+ def __str__(self):
+ return str(self._child)
+
def __repr__(self):
return "TransformWrapper(%r)" % self._child
- __str__ = __repr__
def frozen(self):
return self._child.frozen()
@@ -1344,8 +1473,8 @@ class AffineBase(Transform):
"""
is_affine = True
- def __init__(self):
- Transform.__init__(self)
+ def __init__(self, *args, **kwargs):
+ Transform.__init__(self, *args, **kwargs)
self._inverted = None
def __array__(self, *args, **kwargs):
@@ -1360,18 +1489,30 @@ def _concat(a, b):
"""
return np.dot(b, a)
- def get_matrix(self):
- """
- Get the underlying transformation matrix as a numpy array.
- """
- raise NotImplementedError()
+ def __eq__(self, other):
+ if other.is_affine:
+ return np.all(self.get_matrix() == other.get_matrix())
+ return NotImplemented
+
+ def transform(self, values):
+ return self.transform_affine(values)
+ transform.__doc__ = Transform.transform.__doc__
+
+ def transform_affine(self, values):
+ raise NotImplementedError('Affine subclasses should override this method.')
+ transform_affine.__doc__ = Transform.transform_affine.__doc__
def transform_non_affine(self, points):
return points
transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
+ def transform_path(self, path):
+ return self.transform_path_affine(path)
+ transform_path.__doc__ = Transform.transform_path.__doc__
+
def transform_path_affine(self, path):
- return self.transform_path(path)
+ return Path(self.transform_affine(path.vertices),
+ path.codes, path._interpolation_steps)
transform_path_affine.__doc__ = Transform.transform_path_affine.__doc__
def transform_path_non_affine(self, path):
@@ -1399,6 +1540,7 @@ class Affine2DBase(AffineBase):
Subclasses of this class will generally only need to override a
constructor and :meth:`get_matrix` that generates a custom 3x3 matrix.
"""
+ has_inverse = True
input_dims = 2
output_dims = 2
@@ -1431,7 +1573,7 @@ def matrix_from_values(a, b, c, d, e, f):
"""
return np.array([[a, c, e], [b, d, f], [0.0, 0.0, 1.0]], np.float_)
- def transform(self, points):
+ def transform_affine(self, points):
mtx = self.get_matrix()
if isinstance(points, MaskedArray):
tpoints = affine_transform(points.data, mtx)
@@ -1444,8 +1586,8 @@ def transform_point(self, point):
transform_point.__doc__ = AffineBase.transform_point.__doc__
if DEBUG:
- _transform = transform
- def transform(self, points):
+ _transform_affine = transform_affine
+ def transform_affine(self, points):
# 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.
@@ -1455,16 +1597,16 @@ def transform(self, points):
('A non-numpy array of type %s was passed in for ' +
'transformation. Please correct this.')
% type(points))
- return self._transform(points)
- transform.__doc__ = AffineBase.transform.__doc__
-
- transform_affine = transform
+ return self._transform_affine(points)
transform_affine.__doc__ = AffineBase.transform_affine.__doc__
def inverted(self):
if self._inverted is None or self._invalid:
mtx = self.get_matrix()
- self._inverted = Affine2D(inv(mtx))
+ 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
inverted.__doc__ = AffineBase.inverted.__doc__
@@ -1475,7 +1617,7 @@ class Affine2D(Affine2DBase):
A mutable 2D affine transformation.
"""
- def __init__(self, matrix = None):
+ def __init__(self, matrix=None, **kwargs):
"""
Initialize an Affine transform from a 3x3 numpy float array::
@@ -1485,7 +1627,7 @@ def __init__(self, matrix = None):
If *matrix* is None, initialize with the identity transform.
"""
- Affine2DBase.__init__(self)
+ Affine2DBase.__init__(self, **kwargs)
if matrix is None:
matrix = np.identity(3)
elif DEBUG:
@@ -1496,13 +1638,13 @@ def __init__(self, matrix = None):
def __repr__(self):
return "Affine2D(%s)" % repr(self._mtx)
- __str__ = __repr__
- def __cmp__(self, other):
- if (isinstance(other, Affine2D) and
- (self.get_matrix() == other.get_matrix()).all()):
- return 0
- return -1
+# def __cmp__(self, other):
+# # XXX redundant. this only tells us eq.
+# if (isinstance(other, Affine2D) and
+# (self.get_matrix() == other.get_matrix()).all()):
+# return 0
+# return -1
@staticmethod
def from_values(a, b, c, d, e, f):
@@ -1675,7 +1817,6 @@ def frozen(self):
def __repr__(self):
return "IdentityTransform()"
- __str__ = __repr__
def get_matrix(self):
return self._mtx
@@ -1722,7 +1863,7 @@ class BlendedGenericTransform(Transform):
is_separable = True
pass_through = True
- def __init__(self, x_transform, y_transform):
+ 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
@@ -1735,29 +1876,55 @@ def __init__(self, x_transform, y_transform):
"""
# Here we ask: "Does it blend?"
- Transform.__init__(self)
+ Transform.__init__(self, **kwargs)
self._x = x_transform
self._y = y_transform
self.set_children(x_transform, y_transform)
self._affine = None
+ def __eq__(self, other):
+ # Note, this is an exact copy of BlendedAffine2D.__eq__
+ 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):
+ # Note, this is an exact copy of BlendedAffine2D.contains_branch_seperately
+ return self._x.contains_branch(transform), self._y.contains_branch(transform)
+
+ @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
+
def _get_is_affine(self):
return self._x.is_affine and self._y.is_affine
is_affine = property(_get_is_affine)
+ def _get_has_inverse(self):
+ return self._x.has_inverse and self._y.has_inverse
+ has_inverse = property(_get_has_inverse)
+
def frozen(self):
return blended_transform_factory(self._x.frozen(), self._y.frozen())
frozen.__doc__ = Transform.frozen.__doc__
def __repr__(self):
return "BlendedGenericTransform(%s,%s)" % (self._x, self._y)
- __str__ = __repr__
- def transform(self, points):
+ def transform_non_affine(self, points):
+ if self._x.is_affine and self._y.is_affine:
+ return points
x = self._x
y = self._y
- if x is y and x.input_dims == 2:
+ if x == y and x.input_dims == 2:
return x.transform(points)
if x.input_dims == 2:
@@ -1776,16 +1943,6 @@ def transform(self, points):
return ma.concatenate((x_points, y_points), 1)
else:
return np.concatenate((x_points, y_points), 1)
- transform.__doc__ = Transform.transform.__doc__
-
- def transform_affine(self, points):
- return self.get_affine().transform(points)
- transform_affine.__doc__ = Transform.transform_affine.__doc__
-
- def transform_non_affine(self, points):
- if self._x.is_affine and self._y.is_affine:
- return points
- return self.transform(points)
transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
def inverted(self):
@@ -1822,7 +1979,7 @@ class BlendedAffine2D(Affine2DBase):
"""
is_separable = True
- def __init__(self, x_transform, y_transform):
+ 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
@@ -1841,7 +1998,7 @@ def __init__(self, x_transform, y_transform):
assert x_transform.is_separable
assert y_transform.is_separable
- Transform.__init__(self)
+ Transform.__init__(self, **kwargs)
self._x = x_transform
self._y = y_transform
self.set_children(x_transform, y_transform)
@@ -1849,9 +2006,21 @@ def __init__(self, x_transform, y_transform):
Affine2DBase.__init__(self)
self._mtx = None
+ def __eq__(self, other):
+ # Note, this is an exact copy of BlendedGenericTransform.__eq__
+ 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):
+ # Note, this is an exact copy of BlendedTransform.contains_branch_seperately
+ return self._x.contains_branch(transform), self._y.contains_branch(transform)
+
def __repr__(self):
return "BlendedAffine2D(%s,%s)" % (self._x, self._y)
- __str__ = __repr__
def get_matrix(self):
if self._invalid:
@@ -1894,7 +2063,7 @@ class CompositeGenericTransform(Transform):
"""
pass_through = True
- def __init__(self, a, b):
+ def __init__(self, a, b, **kwargs):
"""
Create a new composite transform that is the result of
applying transform *a* then transform *b*.
@@ -1908,7 +2077,7 @@ def __init__(self, a, b):
self.input_dims = a.input_dims
self.output_dims = b.output_dims
- Transform.__init__(self)
+ Transform.__init__(self, **kwargs)
self._a = a
self._b = b
self.set_children(a, b)
@@ -1938,6 +2107,22 @@ def _invalidate_internal(self, value, invalidating_node):
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 lh_compliment, rh_compliment in self._a._iter_break_from_left_to_right():
+ yield lh_compliment, rh_compliment + self._b
+ for lh_compliment, rh_compliment in self._b._iter_break_from_left_to_right():
+ yield self._a + lh_compliment, rh_compliment
+
+ @property
+ def depth(self):
+ return self._a.depth + self._b.depth
+
def _get_is_affine(self):
return self._a.is_affine and self._b.is_affine
is_affine = property(_get_is_affine)
@@ -1946,14 +2131,12 @@ def _get_is_separable(self):
return self._a.is_separable and self._b.is_separable
is_separable = property(_get_is_separable)
- def __repr__(self):
- return "CompositeGenericTransform(%s, %s)" % (self._a, self._b)
- __str__ = __repr__
+ if DEBUG:
+ def __str__(self):
+ return '(%s, %s)' % (self._a, self._b)
- def transform(self, points):
- return self._b.transform(
- self._a.transform(points))
- transform.__doc__ = Transform.transform.__doc__
+ def __repr__(self):
+ return "CompositeGenericTransform(%r, %r)" % (self._a, self._b)
def transform_affine(self, points):
return self.get_affine().transform(points)
@@ -1962,39 +2145,39 @@ def transform_affine(self, points):
def transform_non_affine(self, points):
if self._a.is_affine and self._b.is_affine:
return points
- return self._b.transform_non_affine(
- self._a.transform(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))
transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
- def transform_path(self, path):
- return self._b.transform_path(
- self._a.transform_path(path))
- transform_path.__doc__ = Transform.transform_path.__doc__
-
- def transform_path_affine(self, path):
- return self._b.transform_path_affine(
- self._a.transform_path(path))
- transform_path_affine.__doc__ = Transform.transform_path_affine.__doc__
-
def transform_path_non_affine(self, path):
if self._a.is_affine and self._b.is_affine:
return path
- return self._b.transform_path_non_affine(
- self._a.transform_path(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))
transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
def get_affine(self):
- if self._a.is_affine and self._b.is_affine:
- return Affine2D(np.dot(self._b.get_affine().get_matrix(),
- self._a.get_affine().get_matrix()))
- else:
+ 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()))
get_affine.__doc__ = Transform.get_affine.__doc__
def inverted(self):
return CompositeGenericTransform(self._b.inverted(), self._a.inverted())
inverted.__doc__ = Transform.inverted.__doc__
+ def _get_has_inverse(self):
+ return self._a.has_inverse and self._b.has_inverse
+ has_inverse = property(_get_has_inverse)
+
class CompositeAffine2D(Affine2DBase):
"""
@@ -2003,7 +2186,7 @@ class CompositeAffine2D(Affine2DBase):
This version is an optimization that handles the case where both *a*
and *b* are 2D affines.
"""
- def __init__(self, a, b):
+ def __init__(self, a, b, **kwargs):
"""
Create a new composite transform that is the result of
applying transform *a* then transform *b*.
@@ -2021,15 +2204,28 @@ def __init__(self, a, b):
assert a.is_affine
assert b.is_affine
- Affine2DBase.__init__(self)
+ Affine2DBase.__init__(self, **kwargs)
self._a = a
self._b = b
self.set_children(a, b)
self._mtx = None
+ if DEBUG:
+ def __str__(self):
+ return '(%s, %s)' % (self._a, self._b)
+
+ @property
+ def depth(self):
+ return self._a.depth + self._b.depth
+
+ def _iter_break_from_left_to_right(self):
+ for lh_compliment, rh_compliment in self._a._iter_break_from_left_to_right():
+ yield lh_compliment, rh_compliment + self._b
+ for lh_compliment, rh_compliment in self._b._iter_break_from_left_to_right():
+ yield self._a + lh_compliment, rh_compliment
+
def __repr__(self):
- return "CompositeAffine2D(%s, %s)" % (self._a, self._b)
- __str__ = __repr__
+ return "CompositeAffine2D(%r, %r)" % (self._a, self._b)
def get_matrix(self):
if self._invalid:
@@ -2076,7 +2272,7 @@ class BboxTransform(Affine2DBase):
"""
is_separable = True
- def __init__(self, boxin, boxout):
+ def __init__(self, boxin, boxout, **kwargs):
"""
Create a new :class:`BboxTransform` that linearly transforms
points from *boxin* to *boxout*.
@@ -2084,7 +2280,7 @@ def __init__(self, boxin, boxout):
assert boxin.is_bbox
assert boxout.is_bbox
- Affine2DBase.__init__(self)
+ Affine2DBase.__init__(self, **kwargs)
self._boxin = boxin
self._boxout = boxout
self.set_children(boxin, boxout)
@@ -2092,8 +2288,7 @@ def __init__(self, boxin, boxout):
self._inverted = None
def __repr__(self):
- return "BboxTransform(%s, %s)" % (self._boxin, self._boxout)
- __str__ = __repr__
+ return "BboxTransform(%r, %r)" % (self._boxin, self._boxout)
def get_matrix(self):
if self._invalid:
@@ -2121,22 +2316,21 @@ class BboxTransformTo(Affine2DBase):
"""
is_separable = True
- def __init__(self, boxout):
+ def __init__(self, boxout, **kwargs):
"""
Create a new :class:`BboxTransformTo` that linearly transforms
points from the unit bounding box to *boxout*.
"""
assert boxout.is_bbox
- Affine2DBase.__init__(self)
+ Affine2DBase.__init__(self, **kwargs)
self._boxout = boxout
self.set_children(boxout)
self._mtx = None
self._inverted = None
def __repr__(self):
- return "BboxTransformTo(%s)" % (self._boxout)
- __str__ = __repr__
+ return "BboxTransformTo(%r)" % (self._boxout)
def get_matrix(self):
if self._invalid:
@@ -2160,8 +2354,7 @@ class BboxTransformToMaxOnly(BboxTransformTo):
:class:`Bbox` with a fixed upper left of (0, 0).
"""
def __repr__(self):
- return "BboxTransformToMaxOnly(%s)" % (self._boxout)
- __str__ = __repr__
+ return "BboxTransformToMaxOnly(%r)" % (self._boxout)
def get_matrix(self):
if self._invalid:
@@ -2185,18 +2378,17 @@ class BboxTransformFrom(Affine2DBase):
"""
is_separable = True
- def __init__(self, boxin):
+ def __init__(self, boxin, **kwargs):
assert boxin.is_bbox
- Affine2DBase.__init__(self)
+ Affine2DBase.__init__(self, **kwargs)
self._boxin = boxin
self.set_children(boxin)
self._mtx = None
self._inverted = None
def __repr__(self):
- return "BboxTransformFrom(%s)" % (self._boxin)
- __str__ = __repr__
+ return "BboxTransformFrom(%r)" % (self._boxin)
def get_matrix(self):
if self._invalid:
@@ -2220,8 +2412,8 @@ class ScaledTranslation(Affine2DBase):
A transformation that translates by *xt* and *yt*, after *xt* and *yt*
have been transformad by the given transform *scale_trans*.
"""
- def __init__(self, xt, yt, scale_trans):
- Affine2DBase.__init__(self)
+ 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)
@@ -2229,8 +2421,7 @@ def __init__(self, xt, yt, scale_trans):
self._inverted = None
def __repr__(self):
- return "ScaledTranslation(%s)" % (self._t,)
- __str__ = __repr__
+ return "ScaledTranslation(%r)" % (self._t,)
def get_matrix(self):
if self._invalid:
@@ -2307,11 +2498,7 @@ def get_fully_transformed_path(self):
"""
Return a fully-transformed copy of the child path.
"""
- 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._invalid = 0
+ self._revalidate()
return self._transform.transform_path_affine(self._transformed_path)
def get_affine(self):