Skip to content

Commit 8bbe2e5

Browse files
Phil Elsonpelson
Phil Elson
authored andcommitted
Substantial change to transform to make it work as documented. The upshot is that the dataLim determination is now working.
1 parent 4cf846d commit 8bbe2e5

File tree

4 files changed

+356
-136
lines changed

4 files changed

+356
-136
lines changed

lib/matplotlib/axes.py

+33-7
Original file line numberDiff line numberDiff line change
@@ -1461,17 +1461,44 @@ def add_line(self, line):
14611461

14621462
self._update_line_limits(line)
14631463
if not line.get_label():
1464-
line.set_label('_line%d'%len(self.lines))
1464+
line.set_label('_line%d' % len(self.lines))
14651465
self.lines.append(line)
14661466
line._remove_method = lambda h: self.lines.remove(h)
14671467
return line
14681468

14691469
def _update_line_limits(self, line):
1470-
p = line.get_path()
1471-
if p.vertices.size > 0:
1472-
self.dataLim.update_from_path(p, self.ignore_existing_data_limits,
1473-
updatex=line.x_isdata,
1474-
updatey=line.y_isdata)
1470+
"""Figures out the data limit of the given line, updating self.dataLim."""
1471+
path = line.get_path()
1472+
if path.vertices.size == 0:
1473+
return
1474+
1475+
line_trans = line.get_transform()
1476+
1477+
if line.get_transform() == self.transData:
1478+
data_path = path
1479+
1480+
elif line_trans.contains_branch(self.transData):
1481+
# transform the path all the way down (to device coordinates)
1482+
# then come back up (by inverting transData) to data coordinates.
1483+
# it would be possible to do this by identifying the transform
1484+
# needed to go from line_trans directly to transData, doing it
1485+
# this way means that the line instance has an opportunity to
1486+
# cache the transformed path.
1487+
device2data = self.transData.inverted()
1488+
data_path = device2data.transform_path(line.get_transformed_path())
1489+
else:
1490+
# for backwards compatibility we update the dataLim with the
1491+
# coordinate range of the given path, even though the coordinate
1492+
# systems are completely different. This may occur in situations
1493+
# such as when ax.transAxes is passed through for absolute
1494+
# positioning.
1495+
data_path = path
1496+
1497+
if data_path.vertices.size > 0:
1498+
self.dataLim.update_from_path(data_path,
1499+
self.ignore_existing_data_limits,
1500+
updatex=line.x_isdata,
1501+
updatey=line.y_isdata)
14751502
self.ignore_existing_data_limits = False
14761503

14771504
def add_patch(self, p):
@@ -3909,7 +3936,6 @@ def plot(self, *args, **kwargs):
39093936
self.add_line(line)
39103937
lines.append(line)
39113938

3912-
39133939
self.autoscale_view(scalex=scalex, scaley=scaley)
39143940
return lines
39153941

lib/matplotlib/lines.py

+9
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,22 @@ def recache(self, always=False):
446446
self._invalidy = False
447447

448448
def _transform_path(self, subslice=None):
449+
"""
450+
Puts a TransformedPath instance at self._transformed_path,
451+
all invalidation of the transform is then handled by the TransformedPath instance.
452+
"""
449453
# Masked arrays are now handled by the Path class itself
450454
if subslice is not None:
451455
_path = Path(self._xy[subslice,:])
452456
else:
453457
_path = self._path
454458
self._transformed_path = TransformedPath(_path, self.get_transform())
455459

460+
def get_transformed_path(self):
461+
"""Return the path of this line, (fully) transformed using the line's transform."""
462+
if self._transformed_path is None:
463+
self._transform_path()
464+
return self._transformed_path.get_fully_transformed_path()
456465

457466
def set_transform(self, t):
458467
"""

lib/matplotlib/tests/test_transforms.py

+121-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import print_function
2+
import itertools
3+
import unittest
4+
25
from nose.tools import assert_equal
3-
from numpy.testing import assert_almost_equal
6+
import numpy.testing as np_test, assert_almost_equal
47
from matplotlib.transforms import Affine2D, BlendedGenericTransform
58
from matplotlib.path import Path
69
from matplotlib.scale import LogScale
@@ -11,7 +14,6 @@
1114
import matplotlib.pyplot as plt
1215

1316

14-
1517
@cleanup
1618
def test_non_affine_caching():
1719
class AssertingNonAffineTransform(mtrans.Transform):
@@ -106,37 +108,37 @@ def test_pre_transform_plotting():
106108

107109

108110
def test_Affine2D_from_values():
109-
points = [ [0,0],
111+
points = np.array([ [0,0],
110112
[10,20],
111113
[-1,0],
112-
]
114+
])
113115

114-
t = Affine2D.from_values(1,0,0,0,0,0)
116+
t = mtrans.Affine2D.from_values(1,0,0,0,0,0)
115117
actual = t.transform(points)
116118
expected = np.array( [[0,0],[10,0],[-1,0]] )
117119
assert_almost_equal(actual,expected)
118120

119-
t = Affine2D.from_values(0,2,0,0,0,0)
121+
t = mtrans.Affine2D.from_values(0,2,0,0,0,0)
120122
actual = t.transform(points)
121123
expected = np.array( [[0,0],[0,20],[0,-2]] )
122124
assert_almost_equal(actual,expected)
123125

124-
t = Affine2D.from_values(0,0,3,0,0,0)
126+
t = mtrans.Affine2D.from_values(0,0,3,0,0,0)
125127
actual = t.transform(points)
126128
expected = np.array( [[0,0],[60,0],[0,0]] )
127129
assert_almost_equal(actual,expected)
128130

129-
t = Affine2D.from_values(0,0,0,4,0,0)
131+
t = mtrans.Affine2D.from_values(0,0,0,4,0,0)
130132
actual = t.transform(points)
131133
expected = np.array( [[0,0],[0,80],[0,0]] )
132134
assert_almost_equal(actual,expected)
133135

134-
t = Affine2D.from_values(0,0,0,0,5,0)
136+
t = mtrans.Affine2D.from_values(0,0,0,0,5,0)
135137
actual = t.transform(points)
136138
expected = np.array( [[5,0],[5,0],[5,0]] )
137139
assert_almost_equal(actual,expected)
138140

139-
t = Affine2D.from_values(0,0,0,0,0,6)
141+
t = mtrans.Affine2D.from_values(0,0,0,0,0,6)
140142
actual = t.transform(points)
141143
expected = np.array( [[0,6],[0,6],[0,6]] )
142144
assert_almost_equal(actual,expected)
@@ -165,6 +167,115 @@ def test_clipping_of_log():
165167
assert np.allclose(tpoints[-1], tpoints[0])
166168

167169

170+
class BasicTransformTests(unittest.TestCase):
171+
def setUp(self):
172+
class NonAffineForTest(mtrans.Transform):
173+
is_affine = False
174+
output_dims = 2
175+
input_dims = 2
176+
177+
def __init__(self, real_trans, *args, **kwargs):
178+
self.real_trans = real_trans
179+
r = mtrans.Transform.__init__(self, *args, **kwargs)
180+
181+
def transform_non_affine(self, values):
182+
return self.real_trans.transform(values)
183+
184+
def transform_path_non_affine(self, path):
185+
return self.real_trans.transform_path(path)
186+
187+
self.ta1 = mtrans.Affine2D(shorthand_name='ta1').rotate(np.pi / 2)
188+
self.ta2 = mtrans.Affine2D(shorthand_name='ta2').translate(10, 0)
189+
self.ta3 = mtrans.Affine2D(shorthand_name='ta3').scale(1, 2)
190+
191+
self.tn1 = NonAffineForTest(mtrans.Affine2D().translate(1, 2), shorthand_name='tn1')
192+
self.tn2 = NonAffineForTest(mtrans.Affine2D().translate(1, 2), shorthand_name='tn2')
193+
self.tn3 = NonAffineForTest(mtrans.Affine2D().translate(1, 2), shorthand_name='tn3')
194+
195+
# creates a transform stack which looks like ((A, (N, A)), A)
196+
self.stack1 = (self.ta1 + (self.tn1 + self.ta2)) + self.ta3
197+
# creates a transform stack which looks like (((A, N), A), A)
198+
self.stack2 = self.ta1 + self.tn1 + self.ta2 + self.ta3
199+
# creates a transform stack which is a subset of stack2
200+
self.stack2_subset = self.tn1 + self.ta2 + self.ta3
201+
202+
# when in debug, the transform stacks can produce dot images:
203+
# self.stack1.write_graphviz(file('stack1.dot', 'w'))
204+
# self.stack2.write_graphviz(file('stack2.dot', 'w'))
205+
# self.stack2_subset.write_graphviz(file('stack2_subset.dot', 'w'))
206+
207+
def test_left_to_right_iteration(self):
208+
stack3 = (self.ta1 + (self.tn1 + (self.ta2 + self.tn2))) + self.ta3
209+
# stack3.write_graphviz(file('stack3.dot', 'w'))
210+
211+
target_transforms = [stack3,
212+
(self.tn1 + (self.ta2 + self.tn2)) + self.ta3,
213+
(self.ta2 + self.tn2) + self.ta3,
214+
self.tn2 + self.ta3,
215+
self.ta3,
216+
]
217+
r = list(self.stack3._iter_break_from_left_to_right())
218+
self.assertEqual(len(r), len(target_transforms))
219+
220+
for target_stack, stack in itertools.izip(target_transforms, r):
221+
self.assertEqual(target_stack, stack)
222+
223+
def test_contains_branch(self):
224+
r1 = (self.ta2 + self.ta1)
225+
r2 = (self.ta2 + self.ta1)
226+
self.assertEqual(r1, r2)
227+
self.assertNotEqual(r1, self.ta1)
228+
self.assertTrue(r1.contains_branch(r2))
229+
self.assertTrue(r1.contains_branch(self.ta1))
230+
self.assertFalse(r1.contains_branch(self.ta2))
231+
self.assertFalse(r1.contains_branch((self.ta2 + self.ta2)))
232+
233+
self.assertEqual(r1, r2)
234+
235+
self.assertTrue(self.stack1.contains_branch(self.ta3))
236+
self.assertTrue(self.stack2.contains_branch(self.ta3))
237+
238+
self.assertTrue(self.stack1.contains_branch(self.stack2_subset))
239+
self.assertTrue(self.stack2.contains_branch(self.stack2_subset))
240+
241+
self.assertFalse(self.stack2_subset.contains_branch(self.stack1))
242+
self.assertFalse(self.stack2_subset.contains_branch(self.stack2))
243+
244+
self.assertTrue(self.stack1.contains_branch((self.ta2 + self.ta3)))
245+
self.assertTrue(self.stack2.contains_branch((self.ta2 + self.ta3)))
246+
247+
self.assertFalse(self.stack1.contains_branch((self.tn1 + self.ta2)))
248+
249+
def test_affine_simplification(self):
250+
points = np.array([[0, 0], [10, 20], [np.nan, 1], [-1, 0]], dtype=np.float64)
251+
na_pts = self.stack1.transform_non_affine(points)
252+
all_pts = self.stack1.transform(points)
253+
254+
na_expected = np.array([[1., 2.], [-19., 12.],
255+
[np.nan, np.nan], [1., 1.]], dtype=np.float64)
256+
all_expected = np.array([[11., 4.], [-9., 24.],
257+
[np.nan, np.nan], [11., 2.]], dtype=np.float64)
258+
259+
# check we have the expected results from doing the affine part only
260+
np_test.assert_array_almost_equal(na_pts, na_expected)
261+
# check we have the expected results from a full transformation
262+
np_test.assert_array_almost_equal(all_pts, all_expected)
263+
# check we have the expected results from doing the transformation in two steps
264+
np_test.assert_array_almost_equal(self.stack1.transform_affine(na_pts), all_expected)
265+
# check that getting the affine transformation first, then fully transforming using that
266+
# yields the same result as before.
267+
np_test.assert_array_almost_equal(self.stack1.get_affine().transform(na_pts), all_expected)
268+
269+
# check that the affine part of stack1 & stack2 are equivalent (i.e. the optimization
270+
# is working)
271+
expected_result = (self.ta2 + self.ta3).get_matrix()
272+
result = self.stack1.get_affine().get_matrix()
273+
np_test.assert_array_equal(expected_result, result)
274+
275+
result = self.stack2.get_affine().get_matrix()
276+
np_test.assert_array_equal(expected_result, result)
277+
278+
168279
if __name__=='__main__':
169280
import nose
170281
nose.runmodule(argv=['-s','--with-doctest'], exit=False)

0 commit comments

Comments
 (0)