AI 生成一个工业数据可视化工具:从日志文件到漂亮图表
引子
工厂里的设备每天都在产生日志,数据格式固定,但盯着文本文件看是一件极其痛苦的事。
于是有了 DataDashboard——一个专门为这种日志文件打造的数据可视化桌面工具。选一个文件,自动解析,秒出图表,支持悬停查看数值,多曲线并行显示,还能统计数据。
一千行 Python 代码,一个 exe,没有数据库,没有云服务,开箱即用。
它解决了什么问题
工业设备日志的典型格式是这样的:
curve=P1 start=26-04-23 14:45:40 furnance=162
items=timeTick voltage power temperature pid[T] pid[P] vol[T] current timeStamp machineState quickActState
2.000 0 0 0 100 88 110 0 23-14:45:42.194 0 none
2.000 0 0 0 0 0 0 0 23-14:45:44.195 0 none
2.000 0 0 0 0 0 0 0 23-14:45:46.194 0 none
第1行是设备信息,第2行是列头,第3行开始是数据。每秒一条,一天下来就是 86400 行。拿 Excel 打开卡死,拿 notepad++ 找数据眼瞎。
DataDashboard 的做法是:指定一个文件夹,自动扫描所有 diagram_*.log 文件,点哪个就渲染哪个。文件追加了新数据?自动检测并刷新。
技术选型:为什么是这些工具
customtkinter + matplotlib,没有 Electron,没有 Qt,没有 Web 技术栈。
理由很简单:这个工具完全离线运行,部署目标是工厂现场的 Windows 工控机。Python + 自带 GUI 框架是最薄的依赖层——装一个 Python 环境,安装三个包,打包成一个 exe,扔进去就能跑。
customtkinter 负责窗口、按钮、标签页这些控件,matplotlib 负责绑图表,TkAgg backend 让 matplotlib 的 Figure 嵌入到 tkinter 窗口里。
import customtkinter as ctk
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
self._canvas = FigureCanvasTkAgg(fig, master=self.chart_frame)
self._canvas.get_tk_widget().pack(fill="both", expand=True)
这一行 matplotlib.use('TkAgg') 必须放在任何 matplotlib 导入之前,否则后端选择会出问题。
核心难点:多纵轴模式下的悬停显示
这是这个工具里踩坑最多的一个功能。
多条曲线并行显示时,数值范围差异巨大——温度几十度,功率几百万,电流零点几。放在同一个 Y 轴上,要么温度曲线被压成一条直线,要么功率曲线看不到细节。
解决方案是每条曲线使用独立的 Y 轴:
if i == 0:
ax = self._ax # 第一条用主轴
else:
ax = self._ax.twinx() # 后续创建 twin 轴
self._multi_axes[key] = ax
ax.set_navigate(False) # 关键:避免拦截鼠标事件
twinx() 会创建一个共享 X 轴的新 Y 轴,各自独立缩放。这里有一个关键细节:twin 轴必须 set_navigate(False),否则它会拦截主轴的鼠标事件,导致悬停失效。
悬停功能的实现走了一点弯路。最初用的是 matplotlib 的 annotate,每次移动更新 set_text、set_visible、xy= 属性。在单轴模式下正常,多纵轴模式下坐标反算会出问题。
最终方案是用 ax.text 替代 annotate,每次 hover 事件先删除旧文本再重建新文本:
def_on_hover(self, event):
# ...
if self._hover_text isnotNone:
try:
self._hover_text.remove()
except Exception:
pass
self._hover_text = self._ax.text(
x_pos, y_pos, ann_text,
bbox=dict(boxstyle="round,pad=0.4", fc="#1e2a3a", ec="#4ECDC4", alpha=0.97),
zorder=1000
)
self._fig.canvas.draw_idle()
matplotlib 的 text 对象删除/重建比 annotation 的原地更新更稳定,特别是在 axes 被 clear 后重建的场景。
数据解析:正则之外的朴素逻辑
日志格式固定,不需要正则表达式。逐行 split,按列头映射字段:
raw_header = lines[1].strip().split()
if raw_header[0].startswith("items="):
raw_header[0] = raw_header[0][6:] # 去掉 items= 前缀
data = {"timestamps": [], "timeTick": [], "raw": {k: [] for k in COLUMNS}}
for line in lines[2:]:
parts = line.strip().split()
iflen(parts) < len(raw_header):
continue
row = dict(zip(raw_header, parts))
ts = row.get("timeStamp", "")
data["timestamps"].append(ts)
# 数值转换
for key, info in COLUMNS.items():
val = float(row.get(info["col"], "0"))
data[key].append(val / info["div"]) # 显示值做除法换算
COLUMNS 配置里定义每条曲线对应的原始列名、除数、颜色:
COLUMNS = {
"temperature": {"col": "temperature", "div": 100, "color": "#FF6B6B"},
"power": {"col": "power", "div": 1000, "color": "#4ECDC4"},
"voltage": {"col": "voltage", "div": 1, "color": "#FFE66D"},
"current": {"col": "current", "div": 1000, "color": "#A8E6CF"},
# ...
}
div 的意义是把原始整数值还原为物理量——温度原始值 2500 表示 25.0°C,所以除以 100。功率数值极大,使用 semilogy 对数坐标。
配置持久化:exe 同目录的问题
这是一个典型的 PyInstaller 打包问题。
开发环境下,__file__ 指向脚本路径,settings.json 放在脚本同目录没问题。但打包成 exe 后,__file__ 变成 PyInstaller 临时解压目录——每次运行都是不同的路径,导致配置文件无法定位。
解决方案:
import sys
ifgetattr(sys, 'frozen', False):
_APP_DIR = os.path.dirname(sys.executable) # exe 所在目录
else:
_APP_DIR = os.path.dirname(os.path.abspath(__file__)) # 源码目录
SETTINGS_FILE = os.path.join(_APP_DIR, "settings.json")
sys.frozen 在打包环境下为 True,此时用 sys.executable 所在目录作为应用根目录。
暗色主题:没有设计稿的配色实践
没有设计师,没有设计稿,配色凭感觉调。
背景色深蓝黑 #0f0f1a,图表区更黑 #0d0d1e,左侧栏 #16213e,强调色是青绿色 #4ECDC4。曲线颜色在彩虹色范围内分布,红色系给温度,青绿给功率,黄给电压,薄荷绿给电流。
matplotlib 的图表配色和 UI 配色统一用十六进制字符串,matplotlib 原生支持:
self._ax.plot(range(n), y, color="#FF6B6B", linewidth=1.2, alpha=0.85)
中文字体是另一个问题。微软雅黑在 matplotlib 里不一定能直接找到,通过 font_manager 遍历系统字体池:
def_find_cfont(size=9):
names = ["Microsoft YaHei", "微软雅黑", "SimHei", "SimSun",
"Noto Sans CJK SC", "Source Han Sans CN", "Arial Unicode MS"]
for name in names:
for f in fm.fontManager.ttflist:
if name.lower() in f.name.lower() or name == f.name:
return fm.FontProperties(fname=f.fname, size=size)
return fm.FontProperties(size=size)
按优先级遍历系统字体列表,找到了返回 FontProperties,找不到就 fallback 回默认大小。这个函数在项目里被缓存起来避免重复扫描。
打包:一条命令出一个 exe
pyinstaller --onefile --noconsole --name DataDashboard \
--distpath E:\DataDashboard\dist \
--workpath E:\DataDashboard\build \
--specpath E:\DataDashboard\build \
E:\DataDashboard\main.py
--noconsole 去掉命令行窗口,--onefile 打包成单一 exe 文件。生成的 DataDashboard.exe 大约 150MB,直接扔到 U 盘里在任意 Windows 机器上运行。
总结
这个工具没有什么技术含量——没有微服务,没有数据库,没有容器化。解决的是一个具体问题:把工业日志变成可读的图表。
用到的技术都是成熟稳定的:Python + customtkinter + matplotlib + PyInstaller。这套组合的好处是依赖极薄、打包简单、部署零难度。工厂现场的环境通常不会给你装 Node.js 或 Java,但装一个 Python 轻车熟路。
一千行代码,一个 exe,解决一个问题。够了。
夜雨聆风