如何在常规绘图与曲面绘图间映射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数据坐标到屏幕坐标的完整转换。
具体解决步骤
- 转换3D目标点到屏幕归一化坐标
先通过3D轴的proj3d.transform_point()将3D数据坐标(x,y,z)转换为2D投影坐标,再用ax.transData.transform()转成屏幕像素坐标,最后通过fig.transFigure.inverted().transform()将像素坐标归一化到0-1的画布空间。 - 转换2D目标点到屏幕归一化坐标
对2D图的目标点执行类似的转换,统一到画布坐标空间。 - 重构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
相关产品推荐
相关产品推荐

