matplotlib

Adding a Rectangle Patch and Text Patch to 3D Collection in Matplotlib


Problem Statement

I'm attempting to add two patches -- a rectangle patch and a text patch -- to the same space within a 3D plot. The ultimate goal is to annotate the rectangle patch with a corresponding value (about 20 rectangles across 4 planes -- see Figure 3). The following code does not get all the way there, but does demonstrate a rendering issue where sometimes the text patch is completely visible and sometimes it isn't -- interestingly, if the string doesn't extend outside the rectangle patch, it never seems to become visible at all. The only difference between Figures 1 and 2 is the rotation of the plot viewer image. I've left the cmap code in the example below because it's a requirement of the project (and just in case it affects the outcome).

Things I've Tried

Conclusion

I suspect that setting zorder to each patch before adding them to a 3D collection may be the solution, but I can't seem to find a way to get to that outcome. Similar questions suggest this, but I haven't been able to apply their answers to this problem specifically.

Environment

macOS: Big Sur 11.2.3
Python 3.8
Matplotlib 3.3.4

Figure 1

Text patch partially visible

Figure 2

Text patch fully visible

Figure 3

Actual patches to annotate

The Code

Generates Figures 1 and 2 (not 3).

#! /usr/bin/env python3
# -*- coding: utf-8 -*-

from matplotlib.patches import Rectangle, PathPatch
from matplotlib.text import TextPath
from matplotlib.transforms import Affine2D
import mpl_toolkits.mplot3d.art3d as art3d
import matplotlib.pyplot as plt
from matplotlib.colors import Normalize


plt.style.use('dark_background')
fig = plt.figure()
ax = fig.gca(projection='3d')
cmap = plt.cm.bwr
norm = Normalize(vmin=50, vmax=80)
base_color = cmap(norm(50))

# Draw box
box = Rectangle((25, 25), width=50, height=50, color=cmap(norm(62)), ec='black', alpha=1)
ax.add_patch(box)
art3d.pathpatch_2d_to_3d(box, z=1, zdir="z")

# Draw text
text_path = TextPath((60, 50), "xxxx", size=10)
trans = Affine2D().rotate(0).translate(0, 1)
p1 = PathPatch(trans.transform_path(text_path))
ax.add_patch(p1)
art3d.pathpatch_2d_to_3d(p1, z=1, zdir="z")

ax.set_xlabel('x')
ax.set_xlim(0, 100)
ax.set_xticklabels([])
ax.xaxis.set_pane_color(base_color)

ax.set_ylabel('y')
ax.set_ylim(0, 100)
ax.set_yticklabels([])
ax.yaxis.set_pane_color(base_color)

ax.set_zlabel('z')
ax.set_zlim(1, 4)
ax.set_zticks([1, 2, 3, 4])
ax.zaxis.set_pane_color(base_color)
ax.set_zticklabels([])

plt.show()


Solution

  • This is a well-known problem with matplotlib 3D plotting: objects are drawn in a particular order, and those plotted last appear on "top" of the others, regardless of which should be in front in a "true" 3D plot.

    See the FAQ here:

    https://matplotlib.org/stable/api/toolkits/mplot3d/faq.html#my-3d-plot-doesn-t-look-right-at-certain-viewing-angles

    My 3D plot doesn’t look right at certain viewing angles

    This is probably the most commonly reported issue with mplot3d. The problem is that – from some viewing angles – a 3D object would appear in front of another object, even though it is physically behind it. This can result in plots that do not look “physically correct.”

    Unfortunately, while some work is being done to reduce the occurrence of this artifact, it is currently an intractable problem, and can not be fully solved until matplotlib supports 3D graphics rendering at its core.

    The problem occurs due to the reduction of 3D data down to 2D + z-order scalar. A single value represents the 3rd dimension for all parts of 3D objects in a collection. Therefore, when the bounding boxes of two collections intersect, it becomes possible for this artifact to occur. Furthermore, the intersection of two 3D objects (such as polygons or patches) can not be rendered properly in matplotlib’s 2D rendering engine.

    This problem will likely not be solved until OpenGL support is added to all of the backends (patches are greatly welcomed). Until then, if you need complex 3D scenes, we recommend using MayaVi.