目录20
文章/Matplotlib/第 39 章
视角、投影与深度线索
Matplotlib · 第 39 / 43 章
view_init:视角就是信息本身
Axes3D.view_init(elev=None, azim=None, roll=None, vertical_axis='z', share=False)| 参数 | 一句话 | 默认值的影响 |
|---|---|---|
elev |
仰角(度),抬头为正 | 30,从斜上方俯视 |
azim |
方位角(度),绕 z 轴转 | -60,从右前方向后看 |
roll |
画面自身旋转(度) | 0;非零会让人误以为是数据倾斜 |
vertical_axis |
哪根轴竖着 | 'z';设成 'y' 就把 y 立在画面中间 |
share |
与其他 3D 轴共享视角 | False |
最小示例
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-3, 3, 40)
X, Y = np.meshgrid(x, x)
Z = np.sin(np.hypot(X, Y))
fig = plt.figure(figsize=(4.4, 3.8))
ax = fig.add_subplot(projection='3d')
ax.plot_surface(X, Y, Z, cmap='viridis', linewidth=0)
ax.view_init(elev=26, azim=-58)
print(ax.elev, ax.azim, ax.roll)
# 输出: 26 -58 0
plt.close(fig)
同一份数据,四个视角给出的信息完全不同:elev=0 时退化成一条投影带,elev=90 时等价于热力图。换视角不是“调好看”,是在换能看到什么——所以交付前至少看三个角度,确认没有因为遮挡而漏掉关键结构。
elev:仰角,且不做范围校验
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.plot([0, 1], [0, 1], [0, 1])
for elev in [0, 45, 90, 100, 180]:
ax.view_init(elev=elev, azim=-60)
print(elev, '->', ax.elev)
# 输出: 0 -> 0
# 输出: 45 -> 45
# 输出: 90 -> 90
# 输出: 100 -> 100
# 输出: 180 -> 180
plt.close(fig)elev 没有取值范围检查:给 180 也接受,然后整个画面上下颠倒(z 轴朝下)。规范范围是 -90 ~ 90,越界不报错但图会翻过去——这是“静默出错”,只能靠看图发现。
azim:方位角,也不取模
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.plot([0, 1], [0, 1], [0, 1])
ax.view_init(elev=30, azim=400)
print(ax.azim)
# 输出: 400
ax.view_init(elev=30, azim=-60)
print(ax.azim)
# 输出: -60
plt.close(fig)azim=400 读回来还是 400,不会归到 40。渲染时按 400 mod 360 = 40 处理,但属性值和实际角度不一致——如果你的代码靠 ax.azim 判断“当前转到哪了”,会算错。自己传之前先取模。
roll:绕视线转画面
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-3, 3, 30)
X, Y = np.meshgrid(x, x)
Z = np.sin(np.hypot(X, Y))
fig = plt.figure(figsize=(4.0, 3.4))
ax = fig.add_subplot(projection='3d')
ax.plot_surface(X, Y, Z, cmap='viridis', linewidth=0)
ax.view_init(elev=26, azim=-58, roll=30)
print(ax.roll)
# 输出: 30
plt.close(fig)roll 转的是画布,不是数据。斜着放有时候能塞进窄栏,但坐标轴刻度也跟着斜,读者会先花两秒搞清方向——除非图窄到必须斜放,否则保持 0。
vertical_axis:把哪根轴立起来
view_init(vertical_axis='y') 让人从“y 朝上”的角度看,适合 y 是时间轴、想让它竖着长的图。它是参数不是属性,设完读不回来:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.plot([0, 1], [0, 1], [0, 1])
ax.view_init(elev=25, azim=-60, vertical_axis='y')
print(hasattr(ax, 'vertical_axis'))
# 输出: False
plt.close(fig)交互旋转
plt.show() 的窗口里可以按住左键拖动旋转,走的是 axes3d.mouserotationstyle:
import matplotlib.pyplot as plt
print(plt.rcParams['axes3d.mouserotationstyle'])
# 输出: arcball
print(plt.rcParams['axes3d.trackballsize'], plt.rcParams['axes3d.trackballborder'])
# 输出: 0.667 0.2'arcball' 是球面轨迹(转起来像抓着球),'trackball' 是平面轨迹(更像转盘)。静态导出时这两项完全不影响结果,只影响你调视角的手感。
注意事项
view_init()不传参 = 恢复默认30 / -60 / 0,不是“保持不动”。- 视角变了,3D 的自动排序会重算,透明图元的前后关系跟着变;同一份数据两个角度的观感差异可能很大。
- 想让多个子图同步转,用
share=True传一个已存在的 3D 轴。 elev/azim的符号容易记反:azim增大是逆时针(俯视看),elev增大是往正上方抬。
投影与盒子:set_proj_type / set_box_aspect
Axes3D.set_proj_type(proj_type, focal_length=None)
Axes3D.set_box_aspect(aspect, *, zoom=1)| 参数 | 一句话 | 默认值的影响 |
|---|---|---|
proj_type |
'persp' 透视 / 'ortho' 正交 |
'persp',近大远小 |
focal_length |
透视强度,越小越夸张 | 1;ortho 下被设成 inf |
aspect |
三根轴的相对长度 | 不设等价于 4:4:3 |
zoom |
整体缩放(不是比例) | 1 |
最小示例
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(figsize=(4.2, 3.4))
ax = fig.add_subplot(projection='3d')
for y0, c in [(0, '#3266a8'), (4, '#4c9a5c'), (8, '#c98a2e')]:
ax.bar3d([1.0], [y0], [0.0], 1.6, 1.6, 1.6, color=c, shade=False)
ax.set_proj_type('ortho')
ax.view_init(elev=18, azim=-72)
ax.set_box_aspect((1, 2.2, 0.55))
print(ax._focal_length)
# 输出: inf
plt.close(fig)
三个立方体实际边长完全一样。persp 下近的那个明显更大,ortho 下三个同大。判据:要“像照片”用 persp,要“比较大小”用 ortho——数据图里通常该用正交,透视会让读者误判远端的量。
focal_length:越小越夸张
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
for f in [0.3, 1, 10]:
ax.set_proj_type('persp', focal_length=f)
print(f, '->', ax._focal_length)
# 输出: 0.3 -> 0.3
# 输出: 1 -> 1
# 输出: 10 -> 10
try:
ax.set_proj_type('persp', focal_length=0)
except ValueError as e:
print('ValueError', e)
# 输出: ValueError focal_length = 0 must be greater than 0
plt.close(fig)focal_length 必须在 (0, ∞) 开区间。小于 0.3 的强透视在数据图里几乎总是错的——远端元素小到看不清刻度。
proj_type 只有两个取值
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
for s in ['persp', 'ortho', 'fisheye']:
try:
ax.set_proj_type(s)
print(s, 'ok')
except ValueError as e:
print(s, '->', str(e)[:78])
# 输出: persp ok
# 输出: ortho ok
# 输出: fisheye -> 'fisheye' is not a valid value for proj_type; supported values are
plt.close(fig)'fisheye' 是 Axes3D 里没有的模式(有些教程把它和 figure 的相机投影混在一起)。大小写也敏感,'PERSP' 同样报 ValueError。
set_box_aspect:三维图的纵横比
这是 3D 里最该改、又最常被忘的一个参数。默认盒子等价于 4:4:3,z 方向被压到 0.75 倍——数据 z 的范围小的时候看起来还行,z 范围一大就顶到天花板。
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
print([round(float(v), 4) for v in ax.get_box_aspect()])
# 输出: [1.1905, 1.1905, 0.8929]
for asp in [(1, 1, 0.5), (4, 4, 3), (2, 1, 1)]:
ax.set_box_aspect(asp)
print(asp, '->', [round(float(v), 4) for v in ax.get_box_aspect()])
# 输出: (1, 1, 0.5) -> [1.2705, 1.2705, 0.6352]
# 输出: (4, 4, 3) -> [1.1905, 1.1905, 0.8929]
# 输出: (2, 1, 1) -> [1.556, 0.778, 0.778]
ax.set_box_aspect(None)
print([round(float(v), 4) for v in ax.get_box_aspect()])
# 输出: [1.1905, 1.1905, 0.8929]
plt.close(fig)get_box_aspect() 返回的是 numpy 数组,直接 print 会打出 np.float64(1.1905) 这种长串,所以上面用 round(float(v), 4) 转成普通浮点数。
传进去的元组会被归一化(等比缩放到一个固定体积),所以读回来的值不等于你给的数——但比例是对的。给 (4, 4, 3) 得到的就是默认值,这也证实了默认盒子是 4:4:3。

曲线数据用 (1, 1, 0.5) 上下的扁平盒子最舒服:既保留了立体感,又不让立方体的空角吃掉一半画布。柱子数据相反,(1, 1, 1.2) 之类让高度显出来。
注意事项
set_aspect('equal')在 3D 上能调但不影响真实比例——它管的是二维后备层,3D 的纵横比只认set_box_aspect:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')
print(ax.get_aspect())
# 输出: equal
try:
ax.set_aspect('1:2')
except ValueError as e:
print('ValueError', str(e)[:52])
# 输出: ValueError '1:2' is not a valid value for aspect; sup
plt.close(fig)box_aspect与figsize无关:3×3 和 6×2 的画布上set_box_aspect((1, 1, 1))读回来都是[1.1003, 1.1003, 1.1003]。想整体放大缩小用zoom=,别改 figsize 硬凑。zoom只是把盒子乘一个系数,不改比例;zoom>1会把轴标签推出画布外,bbox_inches='tight'也救不回来。
颜色、光照与深度线索
3D 少了“前后”这条视觉通道,就要靠颜色、光照、透明度把它补回来。
cmap + norm / vmin / vmax
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import Normalize
x = np.linspace(-3, 3, 40)
X, Y = np.meshgrid(x, x)
Z = np.sin(np.hypot(X, Y))
fig = plt.figure(figsize=(4.4, 3.6))
ax = fig.add_subplot(projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='viridis',
norm=Normalize(vmin=-1, vmax=1)) # 固定色标范围
print(round(surf.get_clim()[0], 3), round(surf.get_clim()[1], 3))
# 输出: -1.0 1.0
plt.close(fig)固定 vmin / vmax 是画多张 3D 图时的硬要求:不固定的话每张图按自己的极值映射,读者会把“这张更亮”误读成“这张值更大”。多图共用一个 Normalize 对象,或者统一 vmin=-1, vmax=1。
lightsource / shade
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import LightSource
x = np.linspace(-3, 3, 40)
X, Y = np.meshgrid(x, x)
Z = np.sin(np.hypot(X, Y))
ls = LightSource(azdeg=315, altdeg=45)
fig = plt.figure(figsize=(4.4, 3.6))
ax = fig.add_subplot(projection='3d')
s1 = ax.plot_surface(X, Y, Z, cmap='viridis', lightsource=ls)
print(len(s1.get_facecolor()))
# 输出: 1521
ax2 = plt.figure().add_subplot(projection='3d')
ax2.plot_surface(X, Y, Z, color='#4c9a5c', shade=True) # 单色 + 自动光照
print('shade ok')
# 输出: shade ok
plt.close('all')shade=True 是 bar3d 的默认行为(所以柱子看着有明暗面),plot_surface 默认 shade=True 但只在给了 color= 时才看得见——给了 cmap 之后颜色由数据决定,光照会跟颜色映射打架,看起来脏。要光照就用单色 + lightsource。
depthshade:散点唯一的深度线索
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(0)
xs, ys, zs = rng.normal(size=(3, 40))
for flag in [True, False]:
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.view_init(elev=20, azim=-60)
sc = ax.scatter(xs, ys, zs, s=60, depthshade=flag)
fig.canvas.draw() # alpha 在 draw 时才写进去
al = [round(float(c[-1]), 3) for c in sc.get_facecolors()]
print(flag, min(al), max(al))
plt.close(fig)
# 输出: True 0.3 1.0
# 输出: False 1.0 1.0
True 时最远的点 alpha 压到 0.3、最近的 1.0,中间线性过渡。这个 0.3 是写死的,没有 rcParam、也没有参数能改:
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
try:
ax.scatter([1], [2], [3], depthshade_minalpha=0.9)
except AttributeError as e:
print('AttributeError', str(e)[:66])
# 输出: AttributeError PathCollection.set() got an unexpected keyword argument 'depthsh
plt.close(fig)depthshade=False 之后所有点同色,读者完全没有距离感——除非你用颜色本身编码深度(c=zs),否则别关。
computed_zorder:3D 自己决定谁在前
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
print(ax.computed_zorder)
# 输出: True
ax.computed_zorder = False
print(ax.computed_zorder)
# 输出: False
plt.close(fig)默认 True:每次绘制时按投影后的深度重新排队,zorder= 基本失效。设成 False 就退回“按 zorder 画”的二维逻辑——有时候能修好某个图元的遮挡错误,但通常会让整体看起来更糟。这是个调试开关,不是排版手段。
注意事项
- 3D 的
zorder在computed_zorder=True下不生效,想控制前后只能改数据或视角。 - 透明曲面(
alpha<1)很好看但排序会出错:plot_surface的面片按单一深度值排序,互相穿插的曲面会画错。降低rcount让面片变大反而更稳。 - 同一张图上放两个
plot_surface时,两个都要给alpha才有“透过看”的效果,只给一个会显得像贴纸。 - 色标(colorbar)不算 3D 对象,用
fig.colorbar(surf, ax=ax, shrink=0.6)正常调用即可。