高级主题:自定义绘图

当你掌握了 Matplotlib 的基础和常用图表后,可能希望创建更具个性和表现力的可视化作品。 本章将带你进入 Matplotlib 的高级自定义世界,包括直接操作艺术家对象、绘制任意形状、应用变换和路径效果, 以及创建复杂的复合图表。这些技巧将让你真正释放 Matplotlib 的全部潜力。

🎨 1. 理解艺术家(Artist)体系

Matplotlib 中的所有绘图元素都是 Artist 的子类,包括 Line2D, Rectangle, Text 等。 通过直接操作这些艺术家对象,可以实现精细控制:

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
x = np.linspace(0, 10, 100)
line, = ax.plot(x, np.sin(x))

# 直接修改艺术家属性
line.set_color('red')
line.set_linewidth(3)
line.set_alpha(0.7)

# 添加自定义矩形
rect = plt.Rectangle((2, -0.5), 1, 1, facecolor='lightblue', edgecolor='blue')
ax.add_patch(rect)

# 添加文本
text = ax.text(5, 0.5, '自定义文本', fontsize=12, ha='center')
text.set_bbox(dict(facecolor='yellow', alpha=0.5))

plt.show()
✔️ 每个绘图元素都是可独立配置的艺术家对象。

🖋️ 2. 使用Path绘制任意形状

matplotlib.path.Path 可以描述任意矢量路径,用于创建自定义形状:

import matplotlib.pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches

# 定义路径顶点和代码类型
verts = [
    (0., 0.),   # 起点
    (0.5, 1.),  # 曲线控制点1
    (1., 0.),   # 曲线控制点2
    (0., 0.5),  # 下一个起点
    (1., 0.5),  # 直线
    (0., 1.),   # 直线
    (0., 0.),   # 返回起点
]
codes = [Path.MOVETO,
         Path.CURVE3,
         Path.CURVE3,
         Path.MOVETO,
         Path.LINETO,
         Path.LINETO,
         Path.CLOSEPOLY]

path = Path(verts, codes)
patch = patches.PathPatch(path, facecolor='orange', lw=2)

fig, ax = plt.subplots()
ax.add_patch(patch)
ax.set_xlim(-0.5, 1.5)
ax.set_ylim(-0.2, 1.2)
ax.set_aspect('equal')
plt.title('使用Path自定义形状')
plt.show()
✔️ Path 支持 MOVETO、LINETO、CURVE3(二次贝塞尔)、CURVE4(三次贝塞尔)等操作。

🔄 3. 变换 (Transforms)

变换用于在不同坐标系之间转换,如数据坐标、轴坐标、图形坐标。利用变换可以绘制与数据无关的元素:

import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse

fig, ax = plt.subplots()
ax.plot([0,10], [0,10])

# 创建椭圆,使用轴坐标(相对位置)
ellipse = Ellipse(xy=(0.5, 0.5), width=0.2, height=0.1,
                  transform=ax.transAxes, facecolor='red', alpha=0.5)
ax.add_patch(ellipse)

# 混合变换:x使用数据坐标,y使用轴坐标
trans = ax.get_xaxis_transform()  # x数据,y轴坐标
rect = plt.Rectangle((5, 0.2), 2, 0.3, transform=trans, facecolor='blue', alpha=0.3)
ax.add_patch(rect)

plt.show()
✔️ ax.transAxes 使用0-1轴坐标,ax.transData 是默认数据坐标。

✨ 4. 路径效果 (PathEffects)

路径效果可以为线条或文本添加描边、阴影、渐变等特效:

import matplotlib.pyplot as plt
import matplotlib.patheffects as path_effects

fig, ax = plt.subplots()
text = ax.text(0.5, 0.5, '带阴影的文本', ha='center', va='center',
               fontsize=20, color='white')
text.set_path_effects([
    path_effects.Stroke(linewidth=3, foreground='black'),
    path_effects.Normal()
])

# 为线条添加阴影效果
line, = ax.plot([0,1], [0,1], linewidth=3, color='red')
line.set_path_effects([
    path_effects.SimpleLineShadow(shadow_color='gray', alpha=0.5),
    path_effects.Normal()
])

ax.set_xlim(0,1); ax.set_ylim(0,1)
plt.show()
✔️ Stroke 创建描边,SimpleLineShadow 添加阴影。

🔢 5. 自定义刻度格式化器和定位器

通过 ticker 模块可以创建完全自定义的刻度:

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np

x = np.linspace(0, 100, 100)
y = np.sin(x/10) * x

fig, ax = plt.subplots()
ax.plot(x, y)

# 自定义主刻度定位器:每25一个刻度
ax.xaxis.set_major_locator(ticker.MultipleLocator(25))
# 自定义主刻度格式化器:带单位的标签
ax.xaxis.set_major_formatter(ticker.FuncFormatter(lambda x, pos: f'{int(x)} km'))

# 自定义次刻度定位器:每5一个次刻度
ax.xaxis.set_minor_locator(ticker.MultipleLocator(5))

plt.show()

🔗 6. 连接子图 (ConnectionPatch)

ConnectionPatch 可以在不同子图之间绘制连接线,常用于关联分析:

import matplotlib.pyplot as plt
from matplotlib.patches import ConnectionPatch
import numpy as np

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10,4))

x = np.linspace(0, 2*np.pi, 100)
ax1.plot(x, np.sin(x))
ax2.plot(x, np.cos(x))

# 连接ax1中点(π/2,1)到ax2中点(π/2,0)
con = ConnectionPatch(xyA=(np.pi/2, 1), xyB=(np.pi/2, 0),
                      coordsA="data", coordsB="data",
                      axesA=ax1, axesB=ax2,
                      color="red", linewidth=2, arrowstyle="->")
ax2.add_artist(con)

plt.show()
✔️ 注意 coordsAcoordsB 指定坐标类型。

📦 7. 嵌入子图 (inset_axes)

在主图中嵌入一个小图用于放大显示局部细节:

from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset

fig, ax = plt.subplots()
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x))

# 创建嵌入子图
inset = inset_axes(ax, width="30%", height="30%", loc='upper right')
inset.plot(x, np.sin(x))
inset.set_xlim(2, 4)   # 放大局部
inset.set_ylim(0, 1)
inset.set_xticks([])   # 可选隐藏刻度
inset.set_yticks([])

# 标记放大区域
mark_inset(ax, inset, loc1=1, loc2=3, fc="none", ec="gray")

plt.show()
✔️ inset_axes 可灵活控制位置和大小。

🏷️ 8. 自定义图例句柄

通过创建自定义的图例句柄,可以表示非标准元素:

import matplotlib.pyplot as plt
from matplotlib.patches import Patch, Circle
from matplotlib.lines import Line2D

# 创建自定义图例句柄
legend_elements = [
    Line2D([0], [0], color='b', lw=4, label='粗蓝线'),
    Line2D([0], [0], marker='o', color='w', markerfacecolor='r', markersize=10, label='红点'),
    Patch(facecolor='orange', alpha=0.7, label='橙色区域'),
    Circle((0,0), radius=0.1, facecolor='green', label='自定义圆形')
]

plt.figure()
plt.legend(handles=legend_elements, loc='center')
plt.axis('off')
plt.show()

🏆 9. 综合示例:自定义仪表板

import matplotlib.pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches
import matplotlib.patheffects as path_effects
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
import numpy as np

# 创建主图
fig, ax = plt.subplots(figsize=(10,6))
x = np.linspace(0, 10, 200)
y = np.sin(x) * np.exp(-0.1*x)
ax.plot(x, y, 'b-', linewidth=2, label='衰减振荡')

# 添加自定义标注:使用路径画一个星形
star_verts = []
for i in range(5):
    angle = i * 2*np.pi/5 - np.pi/2
    star_verts.append((0.5*np.cos(angle), 0.5*np.sin(angle)))
    angle2 = angle + np.pi/5
    star_verts.append((0.25*np.cos(angle2), 0.25*np.sin(angle2)))
star_verts.append(star_verts[0])
star_codes = [Path.MOVETO] + [Path.LINETO]*9 + [Path.CLOSEPOLY]
star_path = Path(star_verts, star_codes)
star_patch = patches.PathPatch(star_path, facecolor='gold', edgecolor='orange', linewidth=2,
                               transform=ax.transData, zorder=10)
# 放置在峰值点附近
star_patch.set_transform(ax.transData)
# 需要设置位置(通过偏移变换太麻烦,这里简化)
ax.add_patch(star_patch)

# 添加路径效果文本
text = ax.text(5, 0.6, '✨ 关键区域 ✨', ha='center', fontsize=14,
               bbox=dict(facecolor='yellow', alpha=0.5))
text.set_path_effects([path_effects.Stroke(linewidth=2, foreground='black'),
                       path_effects.Normal()])

# 嵌入放大图
inset = inset_axes(ax, width="30%", height="30%", loc='lower left')
inset.plot(x, y)
inset.set_xlim(2, 3)
inset.set_ylim(0, 0.5)
inset.set_title('局部放大', fontsize=9)

# 自定义图例(添加一个代表星形的图例)
from matplotlib.patches import Polygon
star_legend = Polygon(star_verts, closed=True, facecolor='gold', edgecolor='orange')
ax.legend(handles=[ax.get_lines()[0], star_legend], labels=['信号', '峰值点'])

plt.show()
✔️ 综合运用了路径、路径效果、嵌入子图、自定义图例。

📚 10. 更多资源


通过本章的高级自定义技巧,你可以突破标准图表的限制,创造出真正独特且富有表现力的可视化作品。不断实践,你将能随心所欲地控制每一个细节。