diff --git a/doc/api/api_changes.rst b/doc/api/api_changes.rst index 062601a1f78d..3f3cd30f240a 100644 --- a/doc/api/api_changes.rst +++ b/doc/api/api_changes.rst @@ -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 ================ diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 0332b9cf4677..686535420ac9 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -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): diff --git a/lib/matplotlib/axes.py b/lib/matplotlib/axes.py index e6705b182548..7afea83e0c0f 100644 --- a/lib/matplotlib/axes.py +++ b/lib/matplotlib/axes.py @@ -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 diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index da852d0f641e..1b64165d4bc2 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -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() diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index b03cddcbfbf8..793052567fb8 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -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): diff --git a/lib/matplotlib/projections/geo.py b/lib/matplotlib/projections/geo.py index 7adee7da7146..692e6c60d10e 100644 --- a/lib/matplotlib/projections/geo.py +++ b/lib/matplotlib/projections/geo.py @@ -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( diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 37c9494a1329..126c59289a42 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -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) diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index 1099ab8d5ccc..86c5c73d8673 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -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) \ No newline at end of file + nose.runmodule(argv=['-s','--with-doctest'], exit=False) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index bac5d963b3ca..f884881708bb 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -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):