-
-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Description
Bug summary
I'm trying to draw a curved FancyArrowPatch to annotate an angle in a 3D plot. When I try transform FancyArrowPatch to 3D and use the arc3 connectionstyle, it seems that the transform isn't applied correctly.
When I do this without a connectionstyle, I get the expected result.
Code for reproduction
import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d.art3d as art3d
import numpy as np
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import proj3d
class myArrow3D(FancyArrowPatch):
def __init__(self, xs, ys, zs, *args, **kwargs):
FancyArrowPatch.__init__(self, (0, 0), (0, 0), *args, **kwargs)
self._verts3d = xs, ys, zs
def do_3d_projection(self, renderer=None):
xs3d, ys3d, zs3d = self._verts3d
xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
return np.min(zs)
class otherArrow3D(FancyArrowPatch):
# https://stackoverflow.com/a/74122407/31611660
# https://stackoverflow.com/a/22867877/31611660
def __init__(self, xdir, zdir, *args, **kwargs):
super().__init__(*args, **kwargs)
transform_2d_to_3d(self, veca=xdir, vecb=zdir)
def rotation_matrix(x, z):
x = np.array(x) / np.linalg.norm(x)
y = np.cross(np.array(z), x)
z = np.cross(x, y)
return np.array([x, y / np.linalg.norm(y), z / np.linalg.norm(z)]).T
def transform_2d_to_3d(pathpatch, veca=[-1, 0, 0], vecb=[0, 1, 0], zs=0):
"""
Transforms a 2D Patch to a 3D patch using the given normal vector.
The patch is projected into they XY plane, rotated about the origin
and finally translated by z.
https://stackoverflow.com/a/18228967/31611660
"""
path = pathpatch.get_path() # Get the path and the associated transform
trans = pathpatch.get_patch_transform()
path = trans.transform_path(path) # Apply the transform
pathpatch.__class__ = art3d.PathPatch3D # Change the class
pathpatch._code3d = path.codes # Copy the codes
pathpatch._facecolor3d = pathpatch.get_facecolor # Get the face color
verts = path.vertices # Get the vertices in 2D
pathpatch._segment3d = np.array(
[np.dot(rotation_matrix(veca, vecb), (x, y, 0)) + (0, 0, zs) for x, y in verts]
)
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.set_xlim([0, 4])
ax.set_ylim([4, 0])
ax.set_zlim([0, 4])
ax.plot([0, 0], [0, 0], [0, 4], "r-")
ax.plot([0, 4], [0, 4], [0, 4], "g-")
ax.add_artist(
myArrow3D(
[0, 1],
[0, 1],
[1, 1],
mutation_scale=25,
lw=0.5,
arrowstyle="simple",
color="b",
)
)
ax.add_artist(
myArrow3D(
[0, 2],
[0, 2],
[2, 2],
mutation_scale=25,
lw=1,
arrowstyle="simple",
connectionstyle="arc3, rad=-0.5",
color="r",
)
)
ax.add_artist(
otherArrow3D(
[0, 0, 1],
np.cross([0, 0, 1], [1, 1, 1]),
(3, 0),
(3, 3),
lw=1,
mutation_scale=1,
arrowstyle="simple",
connectionstyle="arc3, rad=0.75",
color="g",
)
)
ax.set(xlabel="X", ylabel="Y", zlabel="Z")
plt.show()Actual outcome
The blue arrow (bottommost) is just for reference. The red arrow (middle) curves but is not in the same plane as the two lines. The green arrow (topmost) is in the plane and curves, but doesn't touch either line.
Expected outcome
I expected the topmost and green arrow to begin at the vertical (red) line and extend to the diagonal (green) line. The bottommost and blue arrow doesn't have a connectionstyle and it does begin at the vertical (red) line and extend to the diagonal (green) line.
Additional information
I also posted about this at https://stackoverflow.com/questions/79782209/fancyarrowpatch-in-3d-with-curve-connectionstyle/79787179 but, as of 16 October 2025, no one has provided additional insight.
Operating system
Red Hat Enterprise Linux 8
Matplotlib Version
3.9.2
Matplotlib Backend
qtagg
Python version
3.11.4
Jupyter version
No response
Installation
conda