You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在常规绘图与曲面绘图间映射Connection Patch

问题:ConnectionPatch连接2D图与3D曲面图的坐标映射异常

我尝试用ConnectionPatch连接常规2D绘图与3D曲面绘图,目前只得到箭头错位的效果(箭头未正确指向3D图目标位置)。我判断这是坐标转换问题,但未找到ConnectionPatch进行2D与3D坐标映射的相关细节,恳请提供解决思路。

以下是可运行的完整代码:

import matplotlib.patches as patches
from matplotlib import lines, pyplot as plt
from matplotlib.patches import ConnectionPatch
import numpy as np
import math

def fastKernelCalc(f, x, dx):
    nx = len(x) 
    kappa = np.zeros((nx, nx))
    f2 = f.transpose()
    for i in range(nx):
        for j, xj in enumerate(x):
            kernel = 0
            if i-j>0 and j!=0:
                kernel -= sum(np.diagonal(f, offset=j-i)[0:j])*dx
                for k in range(0, j):
                    kernel += sum(f2[k][k:k+i-j]*kappa[i-j+k][k:k+i-j])*dx*dx
            kappa[i][j] = kernel
    return kappa

def solveBetaFunction(x, gamma, amp):
    beta = np.zeros(len(x))
    for idx, val in enumerate(x):
        beta[idx] = amp*math.cos(gamma*math.acos(val))
    return beta

def buildF(x, gamma1, amp1, gamma2, amp2):
    b1 = solveBetaFunction(x, gamma1, amp1)
    b2 = solveBetaFunction(x, gamma2, amp2)
    nx = len(x)
    f = np.zeros((nx, nx))
    for idx, val in enumerate(x):
        for idx2, val2 in enumerate(x):
            if idx > idx2:
                f[idx][idx2] = b1[idx]*b2[idx2]
    return f

def zeroToNan(x):
    for i in range(len(x)):
        for j in range(len(x[0])):
            if j >= i:
                x[i][j] = float('nan')
    return x

X = 1
dx = 0.01
nx = int(round(X/dx))+1
spatial = np.linspace(0, X, nx)
f = buildF(spatial, 6,4, 6, 4)
res = 1
kernel = fastKernelCalc(f, spatial, dx)

fig = plt.figure(figsize=(8,6))
subfigs = fig.subfigures(nrows=1, ncols=2, hspace=0)
meshx, mesht = np.meshgrid(spatial, spatial)
subfig = subfigs[1]
subfig.subplots_adjust(left=0.09, bottom=-.1, right=1, top=1.2, wspace=0.5, hspace=0)

ax = subfig.subplots(nrows=1, ncols=1, subplot_kw={"projection": "3d", "computed_zorder": False})
ax.plot_surface(meshx, mesht, zeroToNan(kernel), edgecolor="black",lw=0.2, rstride=2, cstride=2, 
                        alpha=1, color="white", shade=False, rasterized=True)
for axis in [ax.xaxis, ax.yaxis, ax.zaxis]:
    axis._axinfo['axisline']['linewidth'] = 1
    axis._axinfo['axisline']['color'] = "b"
    axis._axinfo['grid']['linewidth'] = 0
    axis._axinfo['grid']['linestyle'] = "--"
    axis._axinfo['grid']['color'] = "#d1d1d1"
    axis.set_pane_color((0, 0, 0))
    
ax.w_zaxis.line.set_lw(0.)
ax.set_zticks([])    
ax.zaxis._axinfo['axisline']['linewidth'] = 0
plt.gca().invert_yaxis()
plt.gca().invert_xaxis()
ax.set_xlabel("x")
ax.text(1, 1, -17, s="y")

tmp_planes = ax.yaxis._PLANES 
ax.yaxis._PLANES = ( tmp_planes[2], tmp_planes[3], 
                     tmp_planes[0], tmp_planes[1], 
                     tmp_planes[4], tmp_planes[5])
xticks = [0, 1]
ax.set_xticks(xticks)
ax.set_xticklabels(xticks[::-1])
ax.set_yticks([], [])
test = np.zeros(nx)
vals = np.ones((len(spatial), len(spatial)))[-1]*-30
plt.plot(test[0:], spatial[0:], vals[0:], linestyle="dotted", color="black", antialiased=False, rasterized=False)
plt.plot(spatial[0:], spatial[0:], vals[0:], linestyle="dotted", color="black", antialiased=False, rasterized=False)
ax.text(0.5, 0.5, 7, s=r'$k(x, y)$', fontdict={"fontsize": 16})

ax.view_init(25,120)

subfig = subfigs[0]
subfig.subplots_adjust(left=0.05, bottom=0.15, right=0.95, top=0.8, wspace=0.5, hspace=0)

ax2 = subfig.subplots(nrows=1, ncols=1)
b = solveBetaFunction(spatial, 6, 1)
ax2.plot(spatial, solveBetaFunction(spatial, 5, 1))
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax2.set_yticks([], [])
ax2.set_xticks([0, 1])
ax2.set_xlabel("x")
space = np.linspace(0, b[0], 10)
ax2.plot(np.zeros(10), space,color="black", linestyle="dotted", markersize=1)
space = np.linspace(-1, b[-1], 20)
ax2.text(0.51, 0.7, s=r'$\lambda(x)$', fontdict={"fontsize": 16})
ax2.plot(np.ones(20), space,color="black", linestyle="dotted", markersize=1)

con = ConnectionPatch(
    xyB=(.5, .5), coordsB=ax2.transData, axesB=ax2,
    xyA=(.5, .5), coordsA=ax.transData, axesA=ax,
    color='red',
)
ax2.add_artist(con)

plt.show()

解决思路

核心原因

ConnectionPatch使用ax.transData处理3D坐标时,仅会将x、y轴的数值映射到屏幕空间,完全忽略z轴坐标,导致箭头端点在3D图上的投影位置错误。必须手动完成3D数据坐标到屏幕坐标的完整转换。

具体解决步骤

  1. 转换3D目标点到屏幕归一化坐标
    先通过3D轴的proj3d.transform_point()将3D数据坐标(x,y,z)转换为2D投影坐标,再用ax.transData.transform()转成屏幕像素坐标,最后通过fig.transFigure.inverted().transform()将像素坐标归一化到0-1的画布空间。
  2. 转换2D目标点到屏幕归一化坐标
    对2D图的目标点执行类似的转换,统一到画布坐标空间。
  3. 重构ConnectionPatch
    将coordsA和coordsB设置为fig.transFigure,传入归一化后的坐标,确保两端点在同一空间下对齐。

修改后的关键代码片段

在原代码的ax.view_init(25,120)之后,替换原有ConnectionPatch的代码:

# 定义要连接的3D和2D目标点
target_3d = (0.5, 0.5, 0)  # 3D图中的(x,y,z)点
target_2d = (0.5, 0.5)     # 2D图中的(x,y)点

# 转换3D点到画布归一化坐标
proj_3d = ax.proj3d.transform_point(target_3d)
screen_3d = ax.transData.transform(proj_3d)
norm_3d = fig.transFigure.inverted().transform(screen_3d)

# 转换2D点到画布归一化坐标
screen_2d = ax2.transData.transform(target_2d)
norm_2d = fig.transFigure.inverted().transform(screen_2d)

# 创建正确的ConnectionPatch
con = ConnectionPatch(
    xyB=norm_2d, coordsB=fig.transFigure,
    xyA=norm_3d, coordsA=fig.transFigure,
    color='red', arrowstyle='->', linewidth=2
)
fig.add_artist(con)

注意事项

  • 坐标转换必须在设置完3D图的view_init(视角)之后执行,否则投影坐标会因视角变化而错位。
  • 如果需要动态调整视角,需要重新计算坐标并更新ConnectionPatch的xyA和xyB参数。

内容的提问来源于stack exchange,提问作者Luke Bhan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 06:44:54