如何在Matplotlib 3D绘图中实现美观的极坐标网格
在3D绘图中实现带裁剪和标签的美观X-Y平面极坐标网格
我需要绘制带有X-Y平面极坐标网格的3D曲线,目前已实现的3D代码仅能生成简易的极坐标网格,希望替换为可在轴限处裁剪、带有距离和角度标签的美观版本。查阅Matplotlib文档后未找到3D自定义网格的相关方法,特此求助。
当前3D绘图代码
from math import cos, cosh, floor, pi, sin, sinh import matplotlib.pyplot as plt import numpy as np from scipy.spatial.transform import Rotation def set_axes_equal_3d(ax): lims = [ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()] ranges = [x[1] - x[0] for x in lims] mids = [np.mean(x) for x in lims] plot_radius = 0.5 * max([abs(x) for x in ranges]) for setl, r, m in zip([ax.set_xlim3d, ax.set_ylim3d, ax.set_zlim3d], ranges, mids): setl(m - np.sign(r) * plot_radius, m + np.sign(r) * plot_radius) rot = Rotation.from_euler("ZXZ", (25, 75, 75)) proj = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0]]) points = rot.apply([[1.0 * cosh(a), 2 * sinh(a), 0] for a in np.arange(-3, 3, 0.01)]) points += np.array([0, 0, -min(points[:, 2])]) circ = np.array([[cos(a), sin(a), 0] for a in np.arange(-pi, pi, 0.01)]) fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection="3d") # trajectory ax.plot(*zip(points.T), linewidth=3) # projection ax.plot(*zip(proj.dot(points.T)), "--", linewidth=1) # vertical guidelines for idx in range(0, len(points), floor(len(points) / 6)): ax.plot( [points[idx][0]] * 2, [points[idx][1]] * 2, [0, points[idx][2]], "--", linewidth=1, ) # polar grid of poverty for r in [10, 15, 20, 25]: ax.plot(*zip(*(r * circ)), ":", linewidth=0.5, color="gray") for ang in np.arange(0, 360, 30) * pi / 180: ax.plot( [0, 25 * cos(ang)], [0, 25 * sin(ang)], [0, 0], ":", linewidth=0.5, color="gray" ) # config ax.set_xlabel("X") ax.set_ylabel("Y") ax.set_zlabel("Z") ax.set_title("Simple 3D Hyperbola") set_axes_equal_3d(ax) ax.axis("off") plt.show()
参考2D美观极坐标网格代码
from math import cosh, sinh import matplotlib.pyplot as plt import numpy as np from matplotlib.projections import PolarAxes from matplotlib.transforms import Affine2D from mpl_toolkits.axisartist import HostAxes, angle_helper from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear from scipy.spatial.transform import Rotation # -- set up for Polar grid tr = Affine2D().scale(np.pi / 180, 1) + PolarAxes.PolarTransform() extreme_finder = angle_helper.ExtremeFinderCycle( nx=20, ny=20, lon_cycle=360, lat_cycle=None, lon_minmax=None, lat_minmax=(0, np.inf), ) grid_locator1 = angle_helper.LocatorDMS(12) tick_formatter1 = angle_helper.FormatterDMS() # Create GridHelperCurveLinear object grid_helper = GridHelperCurveLinear( tr, extreme_finder=extreme_finder, grid_locator1=grid_locator1, tick_formatter1=tick_formatter1, ) # compute hyperbola rot = Rotation.from_euler("ZXZ", (25, 75, 75)) points = rot.apply([[1.0 * cosh(a), 2 * sinh(a), 0] for a in np.arange(-3, 3, 0.01)]) fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, axes_class=HostAxes, grid_helper=grid_helper) # trajectory ax.plot(*points.T[:2], linewidth=3) ax.set_xlabel("X") ax.set_ylabel("Y") # Make ticklabels of right and top axis visible ax.axis["right"].major_ticklabels.set_visible(True) ax.axis["top"].major_ticklabels.set_visible(True) # Let right axis show ticklabels for 1st coordinate (angle) ax.axis["right"].get_helper().nth_coord_ticks = 0 # Let bottom axis show ticklabels for 2nd coordinate (radius) ax.axis["bottom"].get_helper().nth_coord_ticks = 1 # Add grid lines ax.grid(True, zorder=0, color="lightgrey", linewidth=0.5) ax.set_aspect("equal") plt.show()
实现方案
Matplotlib的3D轴无法直接复用2Daxisartist的极坐标网格系统,我们可以手动实现裁剪网格和标签:
核心思路
- 获取X-Y平面的可视边界:利用轴的X、Y范围构建矩形裁剪区域,确保网格仅显示在可视范围内。
- 绘制裁剪后的极坐标网格:生成极坐标曲线后,用路径裁剪过滤超出边界的部分。
- 添加角度与距离标签:在网格刻度位置添加文本,调整位置避免遮挡。
修改后的完整代码
from math import cos, cosh, floor, pi, sin, sinh import matplotlib.pyplot as plt import numpy as np from scipy.spatial.transform import Rotation from matplotlib.path import Path from matplotlib.patches import PathPatch def set_axes_equal_3d(ax): lims = [ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()] ranges = [x[1] - x[0] for x in lims] mids = [np.mean(x) for x in lims] plot_radius = 0.5 * max([abs(x) for x in ranges]) for setl, r, m in zip([ax.set_xlim3d, ax.set_ylim3d, ax.set_zlim3d], ranges, mids): setl(m - np.sign(r) * plot_radius, m + np.sign(r) * plot_radius) # 生成裁剪后的极坐标网格 def draw_clipped_polar_grid_3d(ax, r_list, angle_list, color="gray", linewidth=0.5, linestyle=":"): # 获取X-Y轴范围,构建裁剪路径 x_min, x_max = ax.get_xlim3d() y_min, y_max = ax.get_ylim3d() clip_path = Path([(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max), (x_min, y_min)]) # 绘制半径网格(圆形) theta = np.linspace(0, 2*pi, 500) for r in r_list: x = r * np.cos(theta) y = r * np.sin(theta) # 创建路径并裁剪 path = Path(np.column_stack((x, y))) patch = PathPatch(path, facecolor="none", edgecolor=color, linewidth=linewidth, linestyle=linestyle) patch.set_clip_path(clip_path, ax.transData) ax.add_patch(patch) # 添加半径标签 label_x = r * np.cos(0) label_y = r * np.sin(0) if x_min < label_x < x_max and y_min < label_y < y_max: ax.text(label_x, label_y, 0, f"{r}", fontsize=8, ha="right", va="center") # 绘制角度网格(射线) for ang in angle_list: # 计算射线与边界的交点,确定终点 x_dir = np.cos(ang) y_dir = np.sin(ang) t_list = [] if x_dir != 0: t_list.append((x_min - 0)/x_dir) t_list.append((x_max - 0)/x_dir) if y_dir != 0: t_list.append((y_min - 0)/y_dir) t_list.append((y_max - 0)/y_dir) # 取正的最小t(从原点到边界的距离) t_valid = [t for t in t_list if t > 0] if not t_valid: continue t_max = min(t_valid) x_end = x_dir * t_max y_end = y_dir * t_max # 绘制射线 ax.plot([0, x_end], [0, y_end], [0, 0], linestyle=linestyle, color=color, linewidth=linewidth) # 添加角度标签(转换为度) ang_deg = np.degrees(ang) label_offset = 0.5 label_x = x_end + label_offset * x_dir label_y = y_end + label_offset * y_dir if x_min < label_x < x_max and y_min < label_y < y_max: ax.text(label_x, label_y, 0, f"{ang_deg:.0f}°", fontsize=8, ha="center", va="center") rot = Rotation.from_euler("ZXZ", (25, 75, 75)) proj = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0]]) points = rot.apply([[1.0 * cosh(a), 2 * sinh(a), 0] for a in np.arange(-3, 3, 0.01)]) points += np.array([0, 0, -min(points[:, 2])]) fig = plt.figure(figsize=(10, 8)) ax = fig.add_subplot(111, projection="3d") # 绘制轨迹 ax.plot(*zip(points.T), linewidth=3) # 绘制投影 ax.plot(*zip(proj.dot(points.T)), "--", linewidth=1) # 绘制垂直辅助线 for idx in range(0, len(points), floor(len(points) / 6)): ax.plot( [points[idx][0]] * 2, [points[idx][1]] * 2, [0, points[idx][2]], "--", linewidth=1, ) # 绘制裁剪后的极坐标网格 r_list = [10, 15, 20, 25] angle_list = np.arange(0, 360, 30) * pi / 180 draw_clipped_polar_grid_3d(ax, r_list, angle_list) # 配置轴 ax.set_xlabel("X") ax.set_ylabel("Y") ax.set_zlabel("Z") ax.set_title("3D Hyperbola with Clipped Polar Grid") set_axes_equal_3d(ax) plt.show()
说明
- 函数
draw_clipped_polar_grid_3d负责生成裁剪后的网格和标签,通过计算网格与轴边界的交点实现精准裁剪。 - 半径标签放在0°方向的圆形刻度上,角度标签放在射线端点外侧,确保仅在可视范围内显示。
- 若需要隐藏坐标轴刻度但保留网格标签,可单独设置
ax.set_xticks([])、ax.set_yticks([])、ax.set_zticks([])。
内容的提问来源于stack exchange,提问作者troy
相关产品推荐
相关产品推荐

