I want to draw a patch in Matplotlib constructed by hollowing it out with another patch, in a way such that the hollowed out part is completely transparent.
For example, lets say I wanted to draw an ellipse hollowed out by another. I could do the following:
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
ellipse_1 = Ellipse((0,0), 4, 3, color='blue')
ellipse_2 = Ellipse((0.5,0.25), 2, 1, angle=30, color='white')
ax = plt.axes()
ax.add_artist(ellipse_1)
ax.add_artist(ellipse_2)
plt.axis('equal')
plt.axis((-3,3,-3,3))
plt.show()
However, if now I wanted to draw something behind, the part behind the hollowed out part would not be visible, for example:
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Rectangle
ellipse_1 = Ellipse((0,0), 4, 3, color='blue')
ellipse_2 = Ellipse((0.5,0.25), 2, 1, angle=30, color='white')
rectangle = Rectangle((-2.5,-2), 5, 2, color='red')
ax = plt.axes()
ax.add_artist(rectangle)
ax.add_artist(ellipse_1)
ax.add_artist(ellipse_2)
plt.axis('equal')
plt.axis((-3,3,-3,3))
plt.show()
where the part of the red rectangle inside the blue shape cannot be seen. Is there an easy way to do this?
Another way to solve this would be with a function to do the opposite of set_clip_path
, lets say set_anticlip_path
, where the line
ellipse_1.set_anticlip_path(ellipse_2)
would do the trick, but I have not been able to find anything like this.
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Rectangle, PathPatch
from matplotlib.path import Path
from matplotlib.transforms import Affine2D
ellipse_1 = Ellipse((0, 0), 4, 3, color='blue')
ellipse_2 = Ellipse((0.5, 0.25), 2, 1, angle=30, color='white')
rectangle = Rectangle((-2.5, -2), 5, 2, color='red')
# Provide a flipping transform to reverse one of the paths
flip = Affine2D().scale(-1, 1).transform
transform_1 = ellipse_1.get_patch_transform().transform
transform_2 = ellipse_2.get_patch_transform().transform
vertices_1 = ellipse_1.get_path().vertices.copy()
vertices_2 = ellipse_2.get_path().vertices.copy()
# Combine the paths, create a PathPatch from the combined path
combined_path = Path(
transform_1(vertices_1).tolist() + transform_2(flip(vertices_2)).tolist(),
ellipse_1.get_path().codes.tolist() + ellipse_2.get_path().codes.tolist(),
)
combined_ellipse = PathPatch(combined_path, color='blue')
ax = plt.axes()
ax.add_artist(rectangle)
ax.add_artist(combined_ellipse)
plt.axis('equal')
plt.axis((-3, 3, -3, 3))
plt.show()
Key ideas:
vertices
and codes
of the two ellipses and concatenating them within a new matplotlib.path.Path
instance.matplotlib.transforms.Affine2D
.get_patch_transform()
result before concatenating the vertices.Alternative approach and caveat: Reversing the direction of one of the paths really is important here. It is not straightforward, however: First, I tried simply reversing the order of vertices of the second ellipse (i.e. I tried writing vertices_2[::-1]
rather than flip(vertices_2)
). This, however, messed up the ellipse shape quite a bit. Second, I had a look at the values of the vertices that result (a) from flipping and (b) from reversing, and I realized that the latter had an offset compared to the former. In particular, it seems that everything except the last vertex needs to be reversed. Thus, the following code results in the same image as the image above, at least for a reversed ellipse:
import numpy as np
...
# Writing `vertices[-2::-1]` essentially means `vertices[:-1][::-1]`
reverse = lambda vertices: np.r_[vertices[-2::-1], vertices[-1:]]
...
combined_path = Path(
transform_1(vertices_1).tolist() + transform_2(reverse(vertices_2)).tolist(),
ellipse_1.get_path().codes.tolist() + ellipse_2.get_path().codes.tolist(),
)
To summarize: