Skip to content

ENH: Add TransformedPatchPath for clipping. #4920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 30, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from matplotlib.cbook import mplDeprecation
from matplotlib import docstring, rcParams
from .transforms import (Bbox, IdentityTransform, TransformedBbox,
TransformedPath, Transform)
TransformedPatchPath, TransformedPath, Transform)
from .path import Path

# Note, matplotlib artists use the doc strings for set and get
Expand Down Expand Up @@ -685,9 +685,7 @@ def set_clip_path(self, path, transform=None):
self._clippath = None
success = True
elif isinstance(path, Patch):
self._clippath = TransformedPath(
path.get_path(),
path.get_transform())
self._clippath = TransformedPatchPath(path)
success = True
elif isinstance(path, tuple):
path, transform = path
Expand All @@ -698,6 +696,9 @@ def set_clip_path(self, path, transform=None):
elif isinstance(path, Path):
self._clippath = TransformedPath(path, transform)
success = True
elif isinstance(path, TransformedPatchPath):
self._clippath = path
success = True
elif isinstance(path, TransformedPath):
self._clippath = path
success = True
Expand Down
44 changes: 43 additions & 1 deletion lib/matplotlib/tests/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
import numpy.testing as np_test
from numpy.testing import assert_almost_equal, assert_array_equal
from numpy.testing import assert_array_almost_equal
from matplotlib.transforms import Affine2D, BlendedGenericTransform, Bbox
from matplotlib.transforms import (Affine2D, BlendedGenericTransform, Bbox,
TransformedPath, TransformedPatchPath)
from matplotlib.path import Path
from matplotlib.scale import LogScale
from matplotlib.testing.decorators import cleanup, image_comparison
Expand Down Expand Up @@ -576,6 +577,47 @@ def test_invalid_arguments():
assert_raises(RuntimeError, t.transform, [[1, 2, 3]])


def test_transformed_path():
points = [(0, 0), (1, 0), (1, 1), (0, 1)]
codes = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]
path = Path(points, codes)

trans = mtrans.Affine2D()
trans_path = TransformedPath(path, trans)
assert np.allclose(trans_path.get_fully_transformed_path().vertices,
points)

# Changing the transform should change the result.
r2 = 1 / np.sqrt(2)
trans.rotate(np.pi / 4)
assert np.allclose(trans_path.get_fully_transformed_path().vertices,
[(0, 0), (r2, r2), (0, 2 * r2), (-r2, r2)])

# Changing the path does not change the result (it's cached).
path.points = [(0, 0)] * 4
assert np.allclose(trans_path.get_fully_transformed_path().vertices,
[(0, 0), (r2, r2), (0, 2 * r2), (-r2, r2)])


def test_transformed_patch_path():
trans = mtrans.Affine2D()
patch = mpatches.Wedge((0, 0), 1, 45, 135, transform=trans)

tpatch = TransformedPatchPath(patch)
points = tpatch.get_fully_transformed_path().vertices

# Changing the transform should change the result.
trans.scale(2)
assert np.allclose(tpatch.get_fully_transformed_path().vertices,
points * 2)

# Changing the path should change the result (and cancel out the scaling
# from the transform).
patch.set_radius(0.5)
assert np.allclose(tpatch.get_fully_transformed_path().vertices,
points)


if __name__ == '__main__':
import nose
nose.runmodule(argv=['-s', '--with-doctest'], exit=False)
40 changes: 40 additions & 0 deletions lib/matplotlib/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2715,6 +2715,46 @@ def get_affine(self):
return self._transform.get_affine()


class TransformedPatchPath(TransformedPath):
"""
A :class:`TransformedPatchPath` caches a non-affine transformed copy of
the :class:`~matplotlib.path.Patch`. This cached copy is automatically
updated when the non-affine part of the transform or the patch changes.
"""
def __init__(self, patch):
"""
Create a new :class:`TransformedPatchPath` from the given
:class:`~matplotlib.path.Patch`.
"""
TransformNode.__init__(self)

transform = patch.get_transform()
self._patch = patch
self._transform = transform
self.set_children(transform)
self._path = patch.get_path()
self._transformed_path = None
self._transformed_points = None

def _revalidate(self):
patch_path = self._patch.get_path()
# Only recompute if the invalidation includes the non_affine part of
# the transform, or the Patch's Path has changed.
if (self._transformed_path is None or self._path != patch_path or
(self._invalid & self.INVALID_NON_AFFINE ==
self.INVALID_NON_AFFINE)):
self._path = patch_path
self._transformed_path = \
self._transform.transform_path_non_affine(patch_path)
self._transformed_points = \
Path._fast_from_codes_and_verts(
self._transform.transform_non_affine(patch_path.vertices),
None,
{'interpolation_steps': patch_path._interpolation_steps,
'should_simplify': patch_path.should_simplify})
self._invalid = 0


def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True):
'''
Modify the endpoints of a range as needed to avoid singularities.
Expand Down