diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 5341063f5357..59ba7b6ff198 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -657,9 +657,9 @@ def draw_gouraud_triangle(self, gc, points, colors, trans): writer.start(u'defs') for i in range(3): - x1, y1 = points[i] - x2, y2 = points[(i + 1) % 3] - x3, y3 = points[(i + 2) % 3] + x1, y1 = tpoints[i] + x2, y2 = tpoints[(i + 1) % 3] + x3, y3 = tpoints[(i + 2) % 3] c = colors[i][:] if x2 == x3: diff --git a/lib/matplotlib/tests/baseline_images/test_axes/scatter.pdf b/lib/matplotlib/tests/baseline_images/test_axes/scatter.pdf new file mode 100644 index 000000000000..6f17d724276d Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/scatter.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/scatter.png b/lib/matplotlib/tests/baseline_images/test_axes/scatter.png new file mode 100644 index 000000000000..876de2fa613e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/scatter.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/scatter.svg b/lib/matplotlib/tests/baseline_images/test_axes/scatter.svg new file mode 100644 index 000000000000..ceced12e0db1 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_axes/scatter.svg @@ -0,0 +1,646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 92385e1f1e02..15622bd467c8 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -697,6 +697,13 @@ def test_hist2d_transpose(): ax = fig.add_subplot(111) ax.hist2d(x,y,bins=10) + +@image_comparison(baseline_images=['scatter']) +def test_scatter_plot(): + ax = plt.axes() + ax.scatter([3, 4, 2, 6], [2, 5, 2, 3], c=['r', 'y', 'b', 'lime'], s=[24, 15, 19, 29]) + + if __name__=='__main__': import nose nose.runmodule(argv=['-s','--with-doctest'], exit=False) diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index e042ac73cac1..ed64d40f276a 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -10,17 +10,17 @@ def test_figure_label(): plt.close('all') plt.figure('today') plt.figure(3) - plt.figure('tomorow') + plt.figure('tomorrow') plt.figure() plt.figure(0) plt.figure(1) plt.figure(3) assert_equal(plt.get_fignums(), [0, 1, 3, 4, 5]) - assert_equal(plt.get_figlabels(), ['', 'today', '', 'tomorow', '']) + assert_equal(plt.get_figlabels(), ['', 'today', '', 'tomorrow', '']) plt.close(10) plt.close() plt.close(5) - plt.close('tomorow') + plt.close('tomorrow') assert_equal(plt.get_fignums(), [0, 1]) assert_equal(plt.get_figlabels(), ['', 'today']) @@ -33,7 +33,8 @@ def test_figure(): ax.set_title(fig.get_label()) ax.plot(range(5)) # plot red line in a different figure. - plt.figure('tomorow') + plt.figure('tomorrow') plt.plot([0, 1], [1,0], 'r') # Return to the original; make sure the red line is not there. plt.figure('today') + plt.close('tomorrow') diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index 04322ea58c95..25399d5c3258 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -6,6 +6,51 @@ from matplotlib.scale import LogScale import numpy as np +import matplotlib.transforms as mtrans +import matplotlib.pyplot as plt + + + + +def test_non_affine_caching(): + class AssertingNonAffineTransform(mtrans.Transform): + """ + This transform raises an assertion error when called when it + shouldn't be and self.raise_on_transform is True. + + """ + input_dims = output_dims = 2 + is_affine = False + def __init__(self, *args, **kwargs): + mtrans.Transform.__init__(self, *args, **kwargs) + self.raise_on_transform = False + self.underlying_transform = mtrans.Affine2D().scale(10, 10) + + def transform_path_non_affine(self, path): + if self.raise_on_transform: + assert False, ('Invalidated affine part of transform ' + 'unnecessarily.') + return self.underlying_transform.transform_path(path) + transform_path = transform_path_non_affine + + def transform_non_affine(self, path): + if self.raise_on_transform: + assert False, ('Invalidated affine part of transform ' + 'unnecessarily.') + return self.underlying_transform.transform(path) + transform = transform_non_affine + + my_trans = AssertingNonAffineTransform() + ax = plt.axes() + plt.plot(range(10), transform=my_trans + ax.transData) + plt.draw() + # enable the transform to raise an exception if it's non-affine transform + # method is triggered again. + my_trans.raise_on_transform = True + ax.transAxes.invalidate() + plt.draw() + + def test_Affine2D_from_values(): points = [ [0,0], [10,20], diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index a1f6cb9de489..5170cb13c8dc 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -99,30 +99,37 @@ def __copy__(self, *args): def invalidate(self): """ - Invalidate this :class:`TransformNode` and all of its - ancestors. Should be called any time the transform changes. + Invalidate this :class:`TransformNode` and triggers an + invalidation of its ancestors. Should be called any + time the transform changes. """ - # If we are an affine transform being changed, we can set the - # flag to INVALID_AFFINE_ONLY - value = (self.is_affine) and self.INVALID_AFFINE or self.INVALID + value = self.INVALID + if self.is_affine: + value = self.INVALID_AFFINE + return self._invalidate_internal(value, invalidating_node=self) - # Shortcut: If self is already invalid, that means its parents - # are as well, so we don't need to do anything. - if self._invalid == value: - return + def _invalidate_internal(self, value, invalidating_node): + """ + Called by :meth:`invalidate` and subsequently ascends the transform + stack calling each TransformNode's _invalidate_internal method. + """ + # determine if this call will be an extension to the invalidation + # status. If not, then a shortcut means that we needn't invoke an + # invalidation up the transform stack as it will already have been + # invalidated. - if not len(self._parents): + # N.B This makes the invalidation sticky, once a transform has been + # invalidated as NON_AFFINE, then it will always be invalidated as + # NON_AFFINE even when triggered with a AFFINE_ONLY invalidation. + # In most cases this is not a problem (i.e. for interactive panning and + # zooming) and the only side effect will be on performance. + status_changed = self._invalid < value + + if self.pass_through or status_changed: self._invalid = value - return - # Invalidate all ancestors of self using pseudo-recursion. - stack = [self] - while len(stack): - root = stack.pop() - # Stop at subtrees that have already been invalidated - if root._invalid != value or root.pass_through: - root._invalid = self.INVALID - stack.extend(root._parents.iterkeys()) + for parent in self._parents.iterkeys(): + parent._invalidate_internal(value=value, invalidating_node=self) def set_children(self, *children): """ @@ -1065,9 +1072,13 @@ def __radd__(self, other): def __array__(self, *args, **kwargs): """ - Used by C/C++ -based backends to get at the array matrix data. + Array interface to get at this Transform's matrix. """ - raise NotImplementedError + # note, this method is also used by C/C++ -based backends + if self.is_affine: + return self.get_matrix() + else: + raise ValueError('Cannot convert this transform to an array.') def transform(self, values): """ @@ -1117,6 +1128,12 @@ def get_affine(self): """ return IdentityTransform() + def get_matrix(self): + """ + Get the transformation matrix for the affine part of this transform. + """ + return self.get_affine().get_matrix() + def transform_point(self, point): """ A convenience function that returns the transformed copy of a @@ -1251,7 +1268,6 @@ class TransformWrapper(Transform): of the same dimensions. """ pass_through = True - is_affine = False def __init__(self, child): """ @@ -1286,6 +1302,11 @@ def _set(self, child): self.transform_path_non_affine = child.transform_path_non_affine self.get_affine = child.get_affine self.inverted = child.inverted + self.get_matrix = child.get_matrix + + # note we do not wrap other properties here since the transform's + # child can be changed with WrappedTransform.set and so checking + # is_affine and other such properties may be dangerous. def set(self, child): """ @@ -1303,6 +1324,10 @@ def set(self, child): self.invalidate() self._invalid = 0 + def _get_is_affine(self): + return self._child.is_affine + is_affine = property(_get_is_affine) + def _get_is_separable(self): return self._child.is_separable is_separable = property(_get_is_separable) @@ -1324,6 +1349,7 @@ def __init__(self): self._inverted = None def __array__(self, *args, **kwargs): + # optimises the access of the transform matrix vs the superclass return self.get_matrix() @staticmethod @@ -1386,9 +1412,6 @@ def _get_is_separable(self): return mtx[0, 1] == 0.0 and mtx[1, 0] == 0.0 is_separable = property(_get_is_separable) - def __array__(self, *args, **kwargs): - return self.get_matrix() - def to_values(self): """ Return the values of the matrix as a sequence (a,b,c,d,e,f) @@ -1431,7 +1454,7 @@ def transform(self, points): warnings.warn( ('A non-numpy array of type %s was passed in for ' + 'transformation. Please correct this.') - % type(values)) + % type(points)) return self._transform(points) transform.__doc__ = AffineBase.transform.__doc__ @@ -1890,6 +1913,8 @@ def __init__(self, a, b): self._b = b self.set_children(a, b) + is_affine = property(lambda self: self._a.is_affine and self._b.is_affine) + def frozen(self): self._invalid = 0 frozen = composite_transform_factory(self._a.frozen(), self._b.frozen()) @@ -1898,6 +1923,21 @@ def frozen(self): return frozen frozen.__doc__ = Transform.frozen.__doc__ + def _invalidate_internal(self, value, invalidating_node): + # In some cases for a composite transform, an invalidating call to AFFINE_ONLY needs + # to be extended to invalidate the NON_AFFINE part too. These cases are when the right + # hand transform is non-affine and either: + # (a) the left hand transform is non affine + # (b) it is the left hand node which has triggered the invalidation + if value == Transform.INVALID_AFFINE \ + and not self._b.is_affine \ + and (not self._a.is_affine or invalidating_node is self._a): + + value = Transform.INVALID + + Transform._invalidate_internal(self, value=value, + invalidating_node=invalidating_node) + def _get_is_affine(self): return self._a.is_affine and self._b.is_affine is_affine = property(_get_is_affine) @@ -2016,11 +2056,15 @@ def composite_transform_factory(a, b): c = a + b """ + # check to see if any of a or b are IdentityTransforms. We use + # isinstance here to guarantee that the transforms will *always* + # be IdentityTransforms. Since TransformWrappers are mutable, + # use of equality here would be wrong. if isinstance(a, IdentityTransform): return b elif isinstance(b, IdentityTransform): return a - elif isinstance(a, AffineBase) and isinstance(b, AffineBase): + elif isinstance(a, Affine2D) and isinstance(b, Affine2D): return CompositeAffine2D(a, b) return CompositeGenericTransform(a, b) @@ -2229,6 +2273,7 @@ def __init__(self, path, transform): self._transformed_points = None def _revalidate(self): + # only recompute if the invalidation includes the non_affine part of the transform if ((self._invalid & self.INVALID_NON_AFFINE == self.INVALID_NON_AFFINE) or self._transformed_path is None): self._transformed_path = \ diff --git a/src/agg_py_transforms.cpp b/src/agg_py_transforms.cpp index 457d5e7f602c..739512605b99 100644 --- a/src/agg_py_transforms.cpp +++ b/src/agg_py_transforms.cpp @@ -9,20 +9,45 @@ #include "agg_trans_affine.h" /** A helper function to convert from a Numpy affine transformation matrix - * to an agg::trans_affine. + * to an agg::trans_affine. If errors = false then an Identity transform is returned. */ agg::trans_affine py_to_agg_transformation_matrix(PyObject* obj, bool errors = true) { PyArrayObject* matrix = NULL; + /** If None either raise a TypeError or return an agg identity transform. */ + if (obj == Py_None) + { + if (errors) + { + throw Py::TypeError("Cannot convert None to an affine transform."); + } + + return agg::trans_affine(); + } + + /** Try turning the object into an affine transform matrix. */ try { - if (obj == Py_None) - throw std::exception(); matrix = (PyArrayObject*) PyArray_FromObject(obj, PyArray_DOUBLE, 2, 2); if (!matrix) throw std::exception(); + } + catch (...) + { + Py_XDECREF(matrix); + if (errors) + { + throw Py::TypeError("Unable to get an affine transform matrix from the given object."); + } + + return agg::trans_affine(); + } + + /** Try turning the matrix into an agg transform. */ + try + { if (PyArray_NDIM(matrix) == 2 || PyArray_DIM(matrix, 0) == 3 || PyArray_DIM(matrix, 1) == 3) { size_t stride0 = PyArray_STRIDE(matrix, 0); @@ -54,7 +79,7 @@ py_to_agg_transformation_matrix(PyObject* obj, bool errors = true) if (errors) { Py_XDECREF(matrix); - throw Py::TypeError("Invalid affine transformation matrix"); + throw Py::TypeError("Invalid affine transformation matrix."); } }