16

I've just discovered a nice way to create a Matplotlib filled contour plot clipped to an arbitrary polygonal region. The method requires calling set_clip_path(patch) on each PathCollection instance in the QuadContourSet returned by Matplotlib's contourf() function. MWE:

import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patches as mpatches
import matplotlib.path as mpath

# some arbitrary data to plot
xx, yy = np.meshgrid(np.linspace(-5, 5, 20), np.linspace(-10, 10, 20), copy=False)
zz = np.sqrt(xx ** 2 + yy ** 2)

poly_verts = [
    (0, 0),
    (-4, 7),
    (-4, -7),
    (4, -7),
    (4, 7),
    (0, 0)
]
poly_codes = [mpath.Path.MOVETO] + (len(poly_verts) - 2) * [mpath.Path.LINETO] +
mpath.Path.CLOSEPOLY]

# create a Path from the polygon vertices
path = mpath.Path(poly_verts, poly_codes)

# create a Patch from the path
patch = mpatches.PathPatch(path, facecolor='none', edgecolor='k')

plt.figure()
ax = plt.gca()
cont = plt.contourf(xx, yy, zz, 50)

# add the patch to the axes
ax.add_patch(patch)  ## TRY COMMENTING THIS OUT
for col in cont.collections:
    col.set_clip_path(patch)

plt.show()

Resulting plot

I'm confused about one aspect: if I comment out the line that plots the patch, then none of the clipping works and I end up with a blank plot. I presume that when calling the set_clip_path method with a patch on the PathCollection, the patch must have been added to the axes, but I don't understand why. Setting edgecolor='none' for patch creation is a fine workaround, but where's the fun in that?

Any thoughts?

Gabriel
  • 1,870
  • 1
  • 19
  • 20
  • 1
    I suspect that this has to do with the clipping being done in display space and unless the patch is drawn it only has data-space coordinates. – tacaswell Sep 05 '14 at 23:48
  • It took me a while to figure out why I had a blank plot, and that the cuprit as you described was in fact that I needed to plot the imaginary, colorless patch before everything else shows up. Very unexpected behavior. – Brian Mar 18 '17 at 19:08
  • 1
    For imshow, you have to specify clipping not once but trice: https://matplotlib.org/examples/pylab_examples/image_clip_path.html – Yulia V Jan 13 '21 at 22:09

1 Answers1

8

If the patch is not added to the axes, is cannot know according to which transform it should be handled. By adding it to the axes, you implicitely set the transform to the data transformation of the axes it is added to.
I guess this necessity becomes clear if you imagine to have several axes on a figure. Then the mpatches.PathPatch could potentially be used for any of those axes.

You may indeed set the patch invisible by setting the face- and edgecolor to "none"

patch = mpatches.PathPatch(path, facecolor='none', edgecolor='none')

or turning it invisible alltogether,

patch = mpatches.PathPatch(path, visible=False)

In case you really want to get rid of adding the patch to the axes, you may set the required transform manually

patch = mpatches.PathPatch(path, transform=ax.transData)

for col in cont.collections:
    col.set_clip_path(patch)

In this case there would not be any need to add it to the axes.

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712