第8章 数据可视化:matplotlib工程图表规范#
本章核心理念:一张合格的工程图表,不是”把数据画出来”那么简单——它是测试报告中最有说服力的”证据”,是向老板、客户、同行解释技术方案时最锐利的”武器”。本章教你用 matplotlib 画出能直接放进正式报告的专业图表,以及用 Plotly 在调试阶段快速探索数据。
8.1 场景与痛点:默认图表太丑,无法放入正式测试报告#
8.1.1 一个让人尴尬的汇报现场#
上周的项目评审会上,小王打开了一张他用 matplotlib 默认设置画的温升曲线图:
import matplotlib.pyplot as plt
import numpy as np
# 小王的"朴素"代码
x = np.linspace(0, 3600, 3600)
y = 25 + 45 * (1 - np.exp(-x / 800)) + np.random.randn(3600) * 0.3
plt.plot(x, y)
plt.show()python这张图的问题清单:
- 横轴标签是
3.6e3这样的科学计数法——没人能一眼看出这是3600秒。 - 没有标题、没有轴标签——看图的人需要猜”这是温度?还是电压?”
- 线条太细、颜色太淡——投影仪一打,后排根本看不清。
- 分辨率太低——截屏粘贴到 Word 里模糊一片。
- 没有标注关键信息——阈值线在哪?异常区间在哪?峰值是多少?
老板看了一眼,说:“这图能放到发给客户的测试报告里吗?” 小王沉默了。
痛点总结:matplotlib 的默认样式是为”快速查看”设计的,而不是为”正式展示”设计的。工程师需要的是一套可复用的图表规范,让每一张图都能达到”测试报告级”的品质。
8.1.2 我们的目标#
本章结束后,你将能够:
- 用一套统一的规范配置,让所有图表自动达到工程报告的品质要求。
- 熟练绘制嵌入式场景中最常用的图表类型:时序曲线、多子图对比、双Y轴、异常区域标注。
- 用 Plotly 在调试阶段交互式探索数据(缩放、悬停查看数值、动态筛选)。
- 清楚地知道什么时候用 matplotlib,什么时候用 Plotly。
8.2 核心方法论:工程图表规范#
8.2.1 一次配置,全局生效——工程级样式初始化#
matplotlib 支持 rcParams(runtime configuration parameters),你可以一次性设置全局默认值,之后所有的图表都会自动遵循这些规范。这就像是给你的画笔”调好色盘”。
将以下代码保存为 plot_config.py,在每个绘图脚本开头导入它:
"""
plot_config.py —— 工程图表全局样式配置
用法:在绘图脚本开头添加 `import plot_config`
"""
import matplotlib.pyplot as plt
import matplotlib as mpl
# ============================================================
# 1. 中文显示配置(最常见的"翻车"点)
# ============================================================
# Windows 系统常用字体
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans']
# macOS 系统
# plt.rcParams['font.sans-serif'] = ['PingFang SC', 'Heiti SC', 'DejaVu Sans']
# Linux 系统(需自行安装中文字体,如 Noto Sans CJK)
# plt.rcParams['font.sans-serif'] = ['Noto Sans CJK SC', 'WenQuanYi Micro Hei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题
# ============================================================
# 2. 图表尺寸与分辨率
# ============================================================
plt.rcParams['figure.figsize'] = (12, 6) # 默认图表尺寸(英寸),宽12高6
plt.rcParams['figure.dpi'] = 150 # 屏幕显示分辨率
plt.rcParams['savefig.dpi'] = 300 # 保存文件分辨率(打印级)
plt.rcParams['savefig.bbox'] = 'tight' # 保存时自动裁剪空白边距
plt.rcParams['savefig.pad_inches'] = 0.1 # 保存时边距留白
# ============================================================
# 3. 坐标轴与刻度
# ============================================================
plt.rcParams['axes.labelsize'] = 12 # 轴标签字号
plt.rcParams['axes.titlesize'] = 14 # 标题字号
plt.rcParams['axes.titleweight'] = 'bold' # 标题加粗
plt.rcParams['axes.labelpad'] = 8 # 轴标签与轴线的距离
plt.rcParams['axes.spines.top'] = False # 隐藏上边框(减少视觉噪音)
plt.rcParams['axes.spines.right'] = False # 隐藏右边框
plt.rcParams['xtick.labelsize'] = 10 # X轴刻度字号
plt.rcParams['ytick.labelsize'] = 10 # Y轴刻度字号
plt.rcParams['xtick.direction'] = 'in' # 刻度线朝内(工程规范)
plt.rcParams['ytick.direction'] = 'in'
# ============================================================
# 4. 线条与标记
# ============================================================
plt.rcParams['lines.linewidth'] = 1.8 # 默认线宽(比默认值0.8粗一倍)
plt.rcParams['lines.markersize'] = 5 # 标记点大小
# ============================================================
# 5. 图例
# ============================================================
plt.rcParams['legend.fontsize'] = 10 # 图例字号
plt.rcParams['legend.frameon'] = True # 图例带边框
plt.rcParams['legend.framealpha'] = 0.8 # 图例半透明背景
plt.rcParams['legend.edgecolor'] = '#cccccc' # 图例边框颜色
# ============================================================
# 6. 网格线
# ============================================================
plt.rcParams['axes.grid'] = True # 默认显示网格
plt.rcParams['grid.alpha'] = 0.3 # 网格线透明度
plt.rcParams['grid.linestyle'] = '--' # 网格线用虚线
plt.rcParams['grid.color'] = '#cccccc' # 网格线颜色
# ============================================================
# 7. 工程配色方案
# ============================================================
# 使用一组辨识度高、在黑白打印下也能区分的工程配色
ENGINEERING_COLORS = [
'#1f77b4', # 蓝色 - 主通道
'#ff7f0e', # 橙色 - 第二通道
'#2ca02c', # 绿色 - 第三通道
'#d62728', # 红色 - 警告/异常
'#9467bd', # 紫色 - 辅助通道
'#8c564b', # 棕色
'#e377c2', # 粉色
'#7f7f7f', # 灰色 - 参考线
]
plt.rcParams['axes.prop_cycle'] = mpl.cycler(color=ENGINEERING_COLORS)
print("✅ 工程图表样式已加载")python使用方式:
import matplotlib.pyplot as plt
import plot_config # 只需这一行,后续所有 plt 调用自动应用规范
# 从这里开始,你的图表就是"工程级"的了
plt.plot([1, 2, 3], [10, 25, 15])
plt.xlabel('时间 (s)')
plt.ylabel('温度 (°C)')
plt.title('某板卡温升曲线')
plt.savefig('result.png') # 自动以300 DPI、裁剪白边保存
plt.show()python8.2.2 工程图表规范清单#
在正式出图之前,用这张清单逐项自查。它可以打印出来贴在工位上:
| 序号 | 检查项 | 规范要求 | 常见错误 |
|---|---|---|---|
| 1 | 中文显示 | 能正常显示中文,不会出现 □ 方块 | 忘记配置 font.sans-serif |
| 2 | 轴标签 | X轴和Y轴都有标签,且包含物理量名称和单位 | 只写了”温度”,漏了”(°C)“ |
| 3 | 标题 | 有简洁明确的标题,说明这张图展示的是什么 | 没有标题,或者标题是 “Figure 1” |
| 4 | 图例 | 多条曲线必须有图例,位置不遮挡关键数据 | 图例挡住了曲线的峰值区域 |
| 5 | 刻度 | 刻度值合理,不出现科学计数法(如 3.6e3) | 横轴时间用秒数,显示为 1e3 |
| 6 | 网格 | 有淡色辅助网格线,方便读数 | 网格线太浓喧宾夺主,或者没有网格 |
| 7 | 分辨率 | 保存图片 ≥ 300 DPI | 默认72 DPI,打印出来模糊 |
| 8 | 配色 | 颜色在彩色和黑白打印下都能区分 | 用了相近的浅色,黑白打印下分不清 |
| 9 | 边框 | 隐藏上方和右侧边框(Tufte 数据墨水比原则) | 四边都画了粗框,视觉噪音大 |
| 10 | 图幅 | 图表尺寸适合目标载体(A4纸/投影仪/PPT) | 默认太小,缩放后看不清 |
8.2.3 多子图布局:一图胜千言#
当需要对比多个维度的数据时,多子图(subplots)是最常用的布局方式。
基础用法:2行1列布局
import matplotlib.pyplot as plt
import plot_config
import numpy as np
# 模拟数据
t = np.linspace(0, 60, 600) # 60秒,10Hz采样
temperature = 25 + 35 * (1 - np.exp(-t / 15)) + np.random.randn(600) * 0.5
voltage = 3.3 - 0.05 * (1 - np.exp(-t / 20)) + np.random.randn(600) * 0.01
# 创建2行1列的子图,共享X轴(关键!这样两个图的时间轴对齐)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
# 子图1:温度
ax1.plot(t, temperature, color='#d62728', label='板卡温度')
ax1.axhline(y=55, color='#ff7f0e', linestyle='--', linewidth=1.2, label='温度阈值 (55°C)')
ax1.set_ylabel('温度 (°C)')
ax1.set_title('60秒温升测试数据', fontweight='bold')
ax1.legend(loc='upper left')
# 子图2:电压
ax2.plot(t, voltage, color='#1f77b4', label='供电电压')
ax2.axhline(y=3.25, color='#ff7f0e', linestyle='--', linewidth=1.2, label='电压下限 (3.25V)')
ax2.set_xlabel('时间 (s)')
ax2.set_ylabel('电压 (V)')
ax2.legend(loc='lower left')
plt.tight_layout() # 自动调整子图间距,避免标签重叠
plt.savefig('dual_channel_test.png')
plt.show()python进阶布局:不规则网格(GridSpec)
当你需要一个大图占上半部分,两个小图并排占下半部分时:
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import plot_config
import numpy as np
fig = plt.figure(figsize=(14, 10))
gs = gridspec.GridSpec(2, 2, height_ratios=[1.2, 1]) # 上面的行高是下面的1.2倍
# 上方:大图(横跨两列)
ax_main = fig.add_subplot(gs[0, :])
t = np.linspace(0, 120, 1200)
temp_data = 25 + 40 * (1 - np.exp(-t / 30)) + np.random.randn(1200) * 0.8
ax_main.plot(t, temp_data, color='#d62728', linewidth=1.5)
ax_main.fill_between(t, temp_data - 2, temp_data + 2, alpha=0.15, color='#d62728', label='±2σ 范围')
ax_main.set_title('完整温升曲线(含不确定度范围)', fontweight='bold')
ax_main.set_ylabel('温度 (°C)')
ax_main.legend()
# 左下:直方图(温度分布)
ax_hist = fig.add_subplot(gs[1, 0])
ax_hist.hist(temp_data, bins=30, color='#1f77b4', alpha=0.7, edgecolor='white')
ax_hist.set_xlabel('温度 (°C)')
ax_hist.set_ylabel('频次')
ax_hist.set_title('温度分布直方图')
# 右下:最后30秒的放大图
ax_zoom = fig.add_subplot(gs[1, 1])
mask = t >= 90
ax_zoom.plot(t[mask], temp_data[mask], color='#2ca02c', linewidth=1.5)
ax_zoom.axhline(y=60, color='#ff7f0e', linestyle='--', label='阈值 60°C')
ax_zoom.set_xlabel('时间 (s)')
ax_zoom.set_ylabel('温度 (°C)')
ax_zoom.set_title('最后30秒放大')
ax_zoom.legend()
plt.tight_layout()
plt.savefig('grid_layout_demo.png')
plt.show()python8.2.4 双Y轴:当两个物理量”量纲”不同#
嵌入式测试中经常需要在一张图上展示温度(几十度)和电流(几百毫安),它们的数值范围完全不同。双Y轴是标准做法。
import matplotlib.pyplot as plt
import plot_config
import numpy as np
t = np.linspace(0, 60, 600)
temperature = 25 + 30 * (1 - np.exp(-t / 15))
current = 150 + 80 * (1 - np.exp(-t / 10)) # 毫安
fig, ax1 = plt.subplots(figsize=(12, 5))
# 左Y轴:温度
color_temp = '#d62728'
ax1.plot(t, temperature, color=color_temp, label='温度')
ax1.set_xlabel('时间 (s)')
ax1.set_ylabel('温度 (°C)', color=color_temp)
ax1.tick_params(axis='y', labelcolor=color_temp)
# 右Y轴:电流(共享X轴)
ax2 = ax1.twinx() # 创建共享X轴的第二个Y轴
color_curr = '#1f77b4'
ax2.plot(t, current, color=color_curr, linestyle='-', label='电流')
ax2.set_ylabel('电流 (mA)', color=color_curr)
ax2.tick_params(axis='y', labelcolor=color_curr)
# 合并图例(关键技巧:分别获取两个轴的图例句柄和标签)
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='center right')
plt.title('温升与电流关联曲线', fontweight='bold')
plt.tight_layout()
plt.savefig('dual_y_axis.png')
plt.show()python注意事项:
- 两条曲线要使用不同的颜色,且颜色要和对应Y轴的刻度颜色一致,否则看图的人会困惑。
- 图例要合并放在一个位置,不要分两处。
- 双Y轴图表容易产生”视觉欺骗”——两条曲线的走势可能因为Y轴刻度范围不同而看起来”相关”或”不相关”,使用时要诚实标注刻度范围。
8.2.5 交互式图表 (Plotly) 快速入门#
matplotlib 的局限#
matplotlib 画出来的是静态图片——你不能缩放局部、不能悬停查看具体数值、不能动态筛选数据范围。在开发调试阶段,你需要快速地、反复地探索数据的细节,这时候静态图片就力不从心了。
Plotly 的杀手级功能#
Plotly 生成的是交互式 HTML 图表,用户可以:
- 🔍 缩放:鼠标框选任意区域放大查看细节
- 📍 悬停:鼠标移到数据点上,自动显示精确的 X/Y 值
- 📊 动态筛选:点击图例隐藏/显示某条曲线
- 📸 截图:工具栏自带”下载PNG”按钮
- 💾 导出数据:部分场景下可以直接导出数据点
安装#
pip install plotly
# 如果你使用 Jupyter Notebook,还需要安装:
pip install nbformatbash基础用法:3行代码搞定交互式图表#
import plotly.graph_objects as go
import numpy as np
# 模拟数据
t = np.linspace(0, 60, 600)
temperature = 25 + 35 * (1 - np.exp(-t / 15)) + np.random.randn(600) * 0.5
# 创建交互式图表
fig = go.Figure()
fig.add_trace(go.Scatter(
x=t, y=temperature,
mode='lines',
name='板卡温度',
line=dict(color='#d62728', width=2)
))
# 添加阈值线
fig.add_hline(y=55, line_dash='dash', line_color='orange',
annotation_text='温度阈值 55°C')
fig.update_layout(
title='温升测试曲线(可交互)',
xaxis_title='时间 (s)',
yaxis_title='温度 (°C)',
template='plotly_white', # 简洁白色背景主题
hovermode='x unified', # 悬停时显示该X坐标下所有曲线的值
)
# 保存为HTML文件(可以发给同事,他们在浏览器中打开就能交互)
fig.write_html('temperature_interactive.html')
# 如果在 Jupyter Notebook 中,直接调用 fig.show() 就能看到交互式图表
fig.show()pythonPlotly 多通道对比#
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
t = np.linspace(0, 120, 1200)
temp_ch1 = 25 + 30 * (1 - np.exp(-t / 20)) + np.random.randn(1200) * 0.3
temp_ch2 = 25 + 25 * (1 - np.exp(-t / 25)) + np.random.randn(1200) * 0.4
temp_ch3 = 25 + 35 * (1 - np.exp(-t / 15)) + np.random.randn(1200) * 0.2
# 创建上下两行的子图,共享X轴
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
subplot_titles=('三个通道温度对比', '温差(相对通道1)'),
vertical_spacing=0.08
)
# 上图:三条温度曲线
fig.add_trace(go.Scatter(x=t, y=temp_ch1, name='CH1 - CPU核心', line=dict(color='#d62728')), row=1, col=1)
fig.add_trace(go.Scatter(x=t, y=temp_ch2, name='CH2 - 电源模块', line=dict(color='#1f77b4')), row=1, col=1)
fig.add_trace(go.Scatter(x=t, y=temp_ch3, name='CH3 - 散热片', line=dict(color='#2ca02c')), row=1, col=1)
# 下图:温差曲线
fig.add_trace(go.Scatter(x=t, y=temp_ch1 - temp_ch2, name='CH1-CH2 温差', line=dict(color='#ff7f0e')), row=2, col=1)
fig.add_trace(go.Scatter(x=t, y=temp_ch1 - temp_ch3, name='CH1-CH3 温差', line=dict(color='#9467bd')), row=2, col=1)
fig.update_layout(
height=700,
template='plotly_white',
hovermode='x unified',
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)
)
fig.update_xaxes(title_text='时间 (s)', row=2, col=1)
fig.update_yaxes(title_text='温度 (°C)', row=1, col=1)
fig.update_yaxes(title_text='温差 (°C)', row=2, col=1)
fig.write_html('multi_channel_interactive.html')
fig.show()python8.2.6 matplotlib vs Plotly:场景分工决策指南#
这是本节最重要的部分——不是谁比谁好,而是在什么场景用什么工具。
| 维度 | matplotlib | Plotly |
|---|---|---|
| 输出格式 | 静态图片(PNG, PDF, SVG) | 交互式 HTML(也可导出静态图片) |
| 最佳场景 | 正式测试报告、论文、PPT、打印文档 | 开发调试阶段的数据探索、在线仪表盘 |
| 交互能力 | 无(Jupyter 中有 %matplotlib notebook 但功能有限) | 缩放、悬停、筛选、动画,开箱即用 |
| 精确排版控制 | 极强:像素级控制图表的每个元素 | 较弱:布局由浏览器渲染引擎决定 |
| 矢量图导出 | 支持 PDF/SVG,无限放大不失真 | HTML 本身就是矢量,静态导出支持 SVG |
| 学习曲线 | API 风格偏底层,需要手动配置较多 | API 高级封装好,几行代码出漂亮图表 |
| 嵌入报告 | 直接插入 Word/LaTeX/PPT | 需要先导出为静态图片,或嵌入HTML |
| 离线使用 | 完全离线 | 需要浏览器打开 HTML 文件 |
| 性能 | 处理百万级数据点表现良好 | 数据量超过10万点时交互可能卡顿 |
决策流程图:
你要画一张图,它的用途是什么?
│
├── 放进正式测试报告 / 论文 / 打印 → matplotlib ✅
│
├── 做 PPT 汇报,需要高清晰度截图 → matplotlib ✅
│
├── 调试阶段,需要反复缩放查看细节 → Plotly ✅
│
├── 要发给同事/客户在线查看 → Plotly (HTML) ✅
│
├── 要做实时数据监控仪表盘 → Plotly (Dash) ✅
│
└── 数据量超过10万点 + 需要交互 → matplotlib 画静态概览
+ Plotly 画局部放大(组合使用)plaintext实践建议:两者并不冲突,很多工程师的工作流是:
- 调试阶段:用 Plotly 快速探索数据,找到感兴趣的模式和异常。
- 报告阶段:用 matplotlib 按照工程规范,精确绘制最终的图表。
8.3 核心实战:批量绘制多通道传感器数据曲线#
8.3.1 场景设定#
延续第7章的场景:你有500块板子的温升测试数据。现在需要为每块板子生成一张”测试报告级”的图表,包含:
- 多通道温度曲线(假设每块板子有3个温度传感器)
- 供电电压曲线(双Y轴)
- 温度阈值线和异常区间标注
- 峰值和均值的统计线
- 图表标题中包含板子编号和测试时间
500张图,如果手动画,你会画到怀疑人生。所以我们要用脚本批量生成。
8.3.2 完整代码#
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np
import pandas as pd
from pathlib import Path
import plot_config # 加载我们之前定义的工程图表样式
# ============================================================
# 配置区
# ============================================================
INPUT_DIR = Path('./test_logs/')
OUTPUT_DIR = Path('./report_figures/')
OUTPUT_DIR.mkdir(exist_ok=True)
TEMP_THRESHOLD = 55.0 # 温度阈值 (°C)
SENSORS = ['NTC_1', 'NTC_2', 'NTC_3'] # 传感器通道名称
SENSOR_LABELS = {
'NTC_1': 'CPU核心',
'NTC_2': '电源模块',
'NTC_3': '散热片',
}
SENSOR_COLORS = {
'NTC_1': '#d62728', # 红色
'NTC_2': '#1f77b4', # 蓝色
'NTC_3': '#2ca02c', # 绿色
}
# ============================================================
# 核心绘图函数:为一块板子生成一张完整图表
# ============================================================
def plot_board_report(board_id: str, df_board: pd.DataFrame, save_dir: Path):
"""
为单块板子绘制完整的测试报告图表
参数:
board_id: 板子编号(如 '001')
df_board: 该板子的数据(已清洗),包含 timestamp, sensor_id, temperature, voltage
save_dir: 图片保存目录
"""
# --- 数据准备 ---
# 将宽表拆分为各传感器数据
sensor_data = {}
for sensor in SENSORS:
mask = df_board['sensor_id'] == sensor
sensor_data[sensor] = df_board[mask].sort_values('timestamp')
# 电压数据(假设所有传感器共享同一电压记录,取其中一个)
voltage_data = sensor_data[SENSORS[0]][['timestamp', 'voltage']].dropna()
voltage_data = voltage_data.drop_duplicates(subset='timestamp').sort_values('timestamp')
# --- 创建画布 ---
fig, ax_temp = plt.subplots(figsize=(14, 6))
# --- 左Y轴:温度 ---
for sensor in SENSORS:
data = sensor_data[sensor]
if data.empty:
continue
ax_temp.plot(
data['timestamp'], data['temperature'],
color=SENSOR_COLORS[sensor],
label=f"{SENSOR_LABELS.get(sensor, sensor)} ({sensor})",
alpha=0.85
)
# 温度阈值线
ax_temp.axhline(
y=TEMP_THRESHOLD, color='#ff7f0e', linestyle='--', linewidth=1.5,
label=f'温度阈值 ({TEMP_THRESHOLD}°C)', zorder=5
)
# 统计均值线(所有传感器的总体均值)
overall_mean = df_board['temperature'].mean()
ax_temp.axhline(
y=overall_mean, color='#7f7f7f', linestyle=':', linewidth=1.2,
label=f'均值 ({overall_mean:.1f}°C)', zorder=5
)
# 标注异常区间(温度超过阈值的时间段)
exceeded = df_board[df_board['temperature'] > TEMP_THRESHOLD]
if not exceeded.empty:
# 找到所有超过阈值的时间点,用 fill_between 标注
# 为了标注连续区间,我们用所有数据点作为X轴
all_times = df_board['timestamp'].sort_values().unique()
all_temp_max = df_board.groupby('timestamp')['temperature'].max()
# 重新索引到统一时间轴
temp_max_series = all_temp_max.reindex(all_times)
# 创建遮罩
mask_exceeded = temp_max_series > TEMP_THRESHOLD
if mask_exceeded.any():
ax_temp.fill_between(
all_times, TEMP_THRESHOLD, temp_max_series,
where=mask_exceeded,
alpha=0.15, color='#d62728', label='超温区间'
)
# 标注峰值
peak_temp = df_board['temperature'].max()
peak_idx = df_board['temperature'].idxmax()
peak_time = df_board.loc[peak_idx, 'timestamp']
ax_temp.annotate(
f'峰值 {peak_temp:.1f}°C',
xy=(peak_time, peak_temp),
xytext=(20, 15), textcoords='offset points',
fontsize=10, fontweight='bold', color='#d62728',
arrowprops=dict(arrowstyle='->', color='#d62728', lw=1.5),
bbox=dict(boxstyle='round,pad=0.3', facecolor='#fff0f0', edgecolor='#d62728', alpha=0.9)
)
ax_temp.set_xlabel('时间')
ax_temp.set_ylabel('温度 (°C)', color='#d62728')
ax_temp.tick_params(axis='y', labelcolor='#d62728')
# --- 右Y轴:电压 ---
ax_volt = ax_temp.twinx()
ax_volt.plot(
voltage_data['timestamp'], voltage_data['voltage'],
color='#9467bd', linewidth=1.2, alpha=0.6, label='供电电压'
)
ax_volt.set_ylabel('电压 (V)', color='#9467bd')
ax_volt.tick_params(axis='y', labelcolor='#9467bd')
# --- 标题与图例 ---
# 从数据中提取测试时间范围
time_start = df_board['timestamp'].min().strftime('%Y-%m-%d %H:%M')
time_end = df_board['timestamp'].max().strftime('%H:%M')
fig.suptitle(
f'温升测试报告 — 板卡 #{board_id}',
fontsize=16, fontweight='bold', y=0.98
)
ax_temp.set_title(f'测试时间: {time_start} ~ {time_end}', fontsize=11, color='#666666', pad=10)
# 合并两个Y轴的图例
lines1, labels1 = ax_temp.get_legend_handles_labels()
lines2, labels2 = ax_volt.get_legend_handles_labels()
ax_temp.legend(
lines1 + lines2, labels1 + labels2,
loc='upper left', fontsize=9, ncol=2
)
# --- X轴时间格式化 ---
ax_temp.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
ax_temp.xaxis.set_major_locator(mdates.AutoDateLocator())
fig.autofmt_xdate(rotation=30) # 时间标签倾斜30度,避免重叠
# --- 保存 ---
plt.tight_layout()
save_path = save_dir / f'board_{board_id}_report.png'
plt.savefig(save_path, dpi=200, bbox_inches='tight')
plt.close(fig) # 关闭图表释放内存(批量绘图时必须!)
return save_path
# ============================================================
# 批量生成
# ============================================================
def batch_generate_reports(input_dir: Path, output_dir: Path):
"""读取所有板子数据,批量生成图表"""
csv_files = sorted(input_dir.glob('*.csv'))
if not csv_files:
print(f"❌ 在 {input_dir} 下未找到CSV文件")
return
print(f"📊 开始批量生成图表,共 {len(csv_files)} 个文件...\n")
for i, file_path in enumerate(csv_files, 1):
board_id = file_path.stem.split('_')[1]
try:
df = pd.read_csv(file_path)
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['temperature'] = pd.to_numeric(df['temperature'], errors='coerce')
df['voltage'] = pd.to_numeric(df['voltage'], errors='coerce')
df['sensor_id'] = df['sensor_id'].str.strip()
# 简单清洗
df = df.drop_duplicates(subset=['timestamp', 'sensor_id'])
df['temperature'] = df['temperature'].interpolate(method='linear')
# 绘图
save_path = plot_board_report(board_id, df, output_dir)
if i % 50 == 0 or i == len(csv_files):
print(f" 进度: {i}/{len(csv_files)} - 最新: {save_path.name}")
except Exception as e:
print(f" ⚠️ 板子 {board_id} 处理失败: {e}")
continue
print(f"\n✅ 全部完成!图表保存在: {output_dir.resolve()}")
# ============================================================
# 主程序
# ============================================================
if __name__ == '__main__':
batch_generate_reports(INPUT_DIR, OUTPUT_DIR)python8.3.3 代码设计要点#
1. 为什么 plt.close(fig) 是必须的?
批量绘图时,每调用一次 plt.subplots() 就会在内存中创建一个新的 figure 对象。如果你不关闭它,画500张图就意味着同时有500个图表对象驻留在内存中——轻则几GB内存占用,重则程序直接崩溃。
规则:批量绘图必须 plt.close(fig),就像文件读写必须 f.close() 一样。
2. 图表元素的”分层”思维
一张信息丰富的工程图表,可以理解为多层叠加:
第5层: 标注(峰值箭头、注释文字)
第4层: 参考线(阈值线、均值线)
第3层: 异常区间(fill_between 半透明色块)
第2层: 数据曲线(温度线、电压线)
第1层: 坐标轴框架(轴标签、刻度、网格、标题)plaintext先画第1层(坐标轴),然后逐层叠加。这个顺序很重要——如果先画数据再画坐标轴,坐标轴会盖住数据。
3. 颜色一致性
每个传感器通道的颜色在整个项目中必须保持一致。这就是为什么我们把颜色定义放在 SENSOR_COLORS 字典里——不管是画单张图还是批量图,NTC_1 永远是红色,NTC_2 永远是蓝色。
8.4 AI协作指南:让AI根据数据特征推荐图表类型并生成绘图代码#
8.4.1 数据特征 → 图表类型的映射#
不同类型的数据显示关系,需要不同类型的图表来表达。当你不确定该用什么图表时,参考以下映射表:
| 你想展示的数据关系 | 推荐图表类型 | matplotlib 函数 | Plotly 函数 |
|---|---|---|---|
| 随时间变化的趋势 | 折线图 | plt.plot() | go.Scatter(mode='lines') |
| 数据的分布情况 | 直方图、箱线图 | plt.hist() / plt.boxplot() | go.Histogram() / go.Box() |
| 两个变量的关联 | 散点图 | plt.scatter() | go.Scatter(mode='markers') |
| 多个类别的对比 | 柱状图 | plt.bar() | go.Bar() |
| 占比关系 | 饼图、环形图 | plt.pie() | go.Pie() |
| 多维数据的相关性 | 热力图 | plt.imshow() / sns.heatmap() | go.Heatmap() |
| 同一指标多次测量 | 箱线图(对比分布) | plt.boxplot() | go.Box() |
嵌入式工程师最常用的 Top 3:
- 折线图:温升曲线、电压变化、传感器时序数据(几乎占80%的使用场景)
- 散点图:两路传感器的相关性分析(如温度 vs 电流)
- 直方图/箱线图:一批产品的测试结果分布(如500块板子的最高温度分布)
8.4.2 万能 Prompt 模板#
【数据描述】
我有一个 pandas DataFrame,列名及数据类型如下:
- timestamp: datetime64, 采样时间,范围 2024-05-20 08:00 ~ 09:00
- sensor_id: str, 值为 "NTC_1", "NTC_2", "NTC_3"
- temperature: float64, 温度值 (°C),范围 25~85
- voltage: float64, 电压值 (V),范围 3.20~3.35
- status: str, "OK" 或 "ERROR"
数据量约60000行,每通道约20000行,采样频率约10Hz
【绘图需求】
我想画一张图来展示以下信息:
1. 三个传感器的温度随时间变化曲线(不同颜色区分)
2. 温度阈值 70°C 的参考线
3. 温度超过阈值的区间用浅红色背景标注
4. 右Y轴显示电压(双Y轴图)
5. 图表需要放入正式测试报告,所以要求:
- 中文标签
- 300 DPI 保存
- 隐藏上/右边框
- 图例放在不遮挡数据的位置
【输出要求】
- 使用 matplotlib
- 代码可以直接运行
- 包含中文注释
- 保存为 PNG 文件plaintext8.4.3 AI 审查重点清单#
AI 生成的绘图代码,重点审查以下容易出错的地方:
| 审查项 | 常见问题 | 正确做法 |
|---|---|---|
| 中文显示 | 没有配置中文字体,输出全是方块 | 在代码开头配置 plt.rcParams['font.sans-serif'] |
| 双Y轴图例 | 两个Y轴的图例分开显示,混乱 | 用 get_legend_handles_labels() 合并到一个 legend() 调用 |
| 时间轴格式 | X轴显示 1.68e9 这样的 Unix 时间戳 | 转为 datetime 类型,用 DateFormatter 格式化 |
| 内存泄漏 | 批量绘图时没有 plt.close() | 循环中每次 plt.savefig() 后紧跟 plt.close(fig) |
tight_layout 位置 | 在 suptitle() 之前调用 tight_layout() 导致标题被裁剪 | 先设置所有标签和标题,最后调用 tight_layout();或者在 savefig 中用 bbox_inches='tight' |
| 颜色对比度 | 多条曲线颜色太接近,黑白打印时分不清 | 使用色相差异大的颜色组合,或配合不同的线型(实线、虚线、点划线) |
| 图例遮挡 | 图例默认放在”最佳位置”但恰好盖住关键数据 | 手动指定 loc='upper left' 等位置,或者放在图外 (bbox_to_anchor) |
8.4.4 进阶 Prompt:让AI帮你设计图表风格指南#
当你需要在整个项目(或整个团队)中保持图表风格一致时,可以让 AI 帮你生成一套完整的样式配置文件:
我正在为一个嵌入式测试团队制定 matplotlib 图表风格指南。
请帮我编写一个 Python 模块 `engineering_plot_style.py`,要求:
1. 定义一套 rcParams 全局配置,满足以下工程规范:
- 支持中文(Windows/Mac/Linux 三种系统的字体配置,自动选择可用的)
- 默认图表尺寸 12x6 英寸
- 保存分辨率 300 DPI
- 隐藏上方和右侧边框
- 刻度线朝内
- 显示淡灰色虚线网格
- 定义一组 8 色的工程配色方案(确保颜色在彩色和黑白打印下都能区分)
2. 提供一个 `apply_style()` 函数,一行调用即可应用所有配置
3. 提供一个 `save_figure()` 函数,封装常用的保存逻辑:
- 自动裁剪白边
- 自动创建输出目录
- 支持同时保存 PNG 和 SVG 两种格式
- 文件名自动加上时间戳
4. 提供一个快速创建双Y轴图的辅助函数 `create_dual_axis_plot()`
5. 提供一个快速创建多子图布局的辅助函数 `create_subplots()`
请包含详细的中文注释和使用示例。plaintext本章小结#
| 你学到了什么 | 解决了什么问题 |
|---|---|
rcParams 全局样式配置 | 告别默认样式的”丑图”,一次配置,全局生效 |
| 工程图表规范清单 | 10项自查标准,确保每张图都能放进正式报告 |
多子图布局与 GridSpec | 在一张图中展示多个维度的数据对比 |
| 双Y轴图表 | 在同一张图中展示量纲不同的物理量 |
| Plotly 交互式图表 | 调试阶段快速缩放、悬停查看数据细节 |
| matplotlib vs Plotly 决策指南 | 明确”什么时候用什么工具”,不纠结 |
| 批量绘图模板 | 500块板子,一个脚本自动出图 |
| AI Prompt 模板 | 让 AI 根据数据特征推荐图表类型并生成代码 |
下一章预告:图表让数据”看得见”,但有时候你需要一个实时运行的桌面程序——带按钮、带波形显示、能操作硬件。第9章我们将学习如何用 PyQt5 构建嵌入式上位机,以及那个让无数工程师翻车的致命问题:跨线程更新UI。