2

Consider that you have three artists in a matplotlib plot. What is the easiest way to not show the intermediate-level artist in the bbox of the top-level artist while retaining the low-level artist visible throughout the whole area?

Illustration of what I want to achieve:

Illustration of layered clipping

It there was no requirement of being able to see the lowest level a non-transparent facecolor of the toplevel plot would be enough. This does not work with the three levels as then the both of the lower levels would be concealed.

See this IPython notebook for a solution using shapely. Here is a not yet completely functional pure matplotlib example, but I'm hoping there is a simpler way of getting the same result that I have not thought of yet.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import patches, cm
from matplotlib.path import Path
fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)
ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto', cmap=cm.coolwarm)
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
               color='k', ha='center', va='center')
renderer = fig.canvas.get_renderer()
bbox = text.get_window_extent(renderer).transformed(ax.transData.inverted())
bboxrect = patches.Rectangle((bbox.x0, bbox.y0), bbox.width, bbox.height)
bbpath = bboxrect.get_path().transformed(bboxrect.get_patch_transform())
patch = patches.Rectangle((0.3, 0.3), 0.4, 0.4)
path = patch.get_path().transformed(patch.get_patch_transform())
path = Path.make_compound_path(path, bbpath)
patch = patches.PathPatch(path, facecolor='none', hatch=r'//')
ax.add_patch(patch)
Mangu Singh Rajpurohit
  • 10,806
  • 4
  • 68
  • 97
Andi
  • 1,233
  • 11
  • 17

2 Answers2

4

I came up with another answer that's a bit cleaner: it involves creating a clip mask for the hatched region that has a hole in it, so that you can see everything in the background behind it.

from matplotlib.path import Path
from matplotlib.patches import PathPatch

def DoubleRect(xy1, width1, height1,
               xy2, width2, height2, **kwargs):
    base = np.array([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
    verts = np.vstack([xy1 + (width1, height1) * base,
                       xy2 + (width2, height2) * base[::-1],
                       xy1])
    codes = 2 * ([Path.MOVETO] + 4 * [Path.LINETO]) + [Path.CLOSEPOLY]
    return PathPatch(Path(verts, codes), **kwargs)

fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)

# plot the image
im = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
               cmap='coolwarm', interpolation='nearest')

# plot the hatched rectangle
patch = plt.Rectangle((0.3, 0.3), 0.4, 0.4, facecolor='none',
                      hatch=r'//')
ax.add_patch(patch)

# add the text
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
               color='k', ha='center', va='center')

# create a mask for the hatched rectangle
mask = DoubleRect((0, 0), 1, 1, (0.4, 0.45), 0.2, 0.1,
                  facecolor='none', edgecolor='black')
ax.add_patch(mask)
patch.set_clip_path(mask)

enter image description here

jakevdp
  • 77,104
  • 11
  • 125
  • 160
  • Could you please explain why the shaded box's lower left corner does not start at (0, 0)? It would be great if you could explain a bit how `DoubleRect()` works. Thank you a lot! – Gilfoyle Aug 26 '21 at 14:42
1

It's a bit of a hack, but I would probably accomplish this by showing the image twice, once in the background, and once with a custom clip path in the foreground. Here's an example:

fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)

# plot the background image
im = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
               cmap=cm.coolwarm, zorder=1)

# plot the hatched rectangle
patch = patches.Rectangle((0.3, 0.3), 0.4, 0.4, facecolor='none',
                          hatch=r'//', zorder=2)
ax.add_patch(patch)

# plot the box around the text
minirect = patches.Rectangle((0.4, 0.45), 0.2, 0.1, facecolor='none',
                             edgecolor='black', zorder=4)
ax.add_patch(minirect)

# duplicate image and set a clip path
im2 = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
                cmap=cm.coolwarm, zorder=3)
im2.set_clip_path(minirect)

# add the text on top
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
               color='k', ha='center', va='center', zorder=5)

enter image description here

jakevdp
  • 77,104
  • 11
  • 125
  • 160