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

如何在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的极坐标网格系统,我们可以手动实现裁剪网格和标签:

核心思路

  1. 获取X-Y平面的可视边界:利用轴的X、Y范围构建矩形裁剪区域,确保网格仅显示在可视范围内。
  2. 绘制裁剪后的极坐标网格:生成极坐标曲线后,用路径裁剪过滤超出边界的部分。
  3. 添加角度与距离标签:在网格刻度位置添加文本,调整位置避免遮挡。

修改后的完整代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.11 13:05:54