目录
- 项目背景与需求分析
- 开发环境搭建(Windows/Linux)
- Python 基础语法速成
- 串口数据通信原理与实现
- 多设备 CSV 文件管理系统
- 数据分析核心技术
- 可视化图表设计与实现
- 完整项目架构与代码
- 部署与运行指南
- 故障排除与优化
- 进阶学习路径
7. 可视化图表设计与实现
7.1 可视化模块的软件工程设计
7.1.1 模块架构与类设计
电机测试数据可视化模块需要实现多种图表展示、交互控制和数据联动功能。采用 MVC(模型 - 视图 - 控制器)架构模式,确保数据、界面和控制逻辑的分离。
模块架构图
plaintext
┌─────────────────────────────────────────────────────────┐
│ 可视化模块 (MotorVisualization) │
├─────────────┬────────────────┬─────────────────────────┤
│ 模型层 │ 视图层 │ 控制层 │
│ (Model) │ (View) │ (Controller) │
├─────────────┼────────────────┼─────────────────────────┤
│ 数据管理器 │ 主窗口 │ 交互控制器 │
│ 图表配置器 │ 仪表盘视图 │ 数据筛选控制器 │
│ 样式管理器 │ 曲线图表视图 │ 图表联动控制器 │
│ 状态管理器 │ 热力图视图 │ 导出控制器 │
│ │ 3D视图 │ 布局控制器 │
│ │ 表格视图 │ │
└─────────────┴────────────────┴─────────────────────────┘
核心类设计
模型层核心类
python
运行
class VisualizationModel:
"""可视化模块的数据模型,管理所有数据和配置"""
def __init__(self, analysis_results: dict):
"""
初始化模型
:param analysis_results: 数据分析结果
"""
self.analysis_results = analysis_results
self.raw_data = analysis_results.get('raw_data', pd.DataFrame())
self.processed_data = analysis_results.get('processed_data', pd.DataFrame())
# 图表配置
self.chart_configs = {
'time_series': self._default_time_series_config(),
'scatter': self._default_scatter_config(),
'histogram': self._default_histogram_config(),
'heatmap': self._default_heatmap_config(),
'gauge': self._default_gauge_config()
}
# 状态管理
self.state = {
'selected_parameters': ['voltage', 'current', 'rpm'],
'time_range': None, # 默认为全部时间范围
'motor_ids': self._get_available_motors(),
'selected_motor': None,
'downsample_factor': 1,
'theme': 'light'
}
# 观察者列表(视图)
self.observers = []
def _get_available_motors(self) -> list:
"""获取可用的电机ID列表"""
if 'motor_id' in self.raw_data.columns:
return sorted(self.raw_data['motor_id'].unique().tolist())
return [1] # 默认电机ID
def register_observer(self, observer) -> None:
"""注册观察者(视图)"""
if observer not in self.observers:
self.observers.append(observer)
def unregister_observer(self, observer) -> None:
"""移除观察者"""
if observer in self.observers:
self.observers.remove(observer)
def notify_observers(self, change_type: str, data: dict = None) -> None:
"""通知所有观察者数据或状态已更改"""
for observer in self.observers:
observer.on_model_change(change_type, data)
def set_parameter_selection(self, parameters: list) -> None:
"""设置选中的参数"""
valid_params = [p for p in parameters if p in self.processed_data.columns]
self.state['selected_parameters'] = valid_params
self.notify_observers('parameter_selection', {'parameters': valid_params})
def set_time_range(self, start_time, end_time) -> None:
"""设置时间范围"""
if start_time and end_time and start_time < end_time:
self.state['time_range'] = (start_time, end_time)
self.notify_observers('time_range', {'start': start_time, 'end': end_time})
def get_filtered_data(self) -> pd.DataFrame:
"""获取根据当前状态过滤后的数据"""
data = self.processed_data.copy()
# 过滤时间范围
if self.state['time_range']:
start, end = self.state['time_range']
data = data[(data.index >= start) & (data.index <= end)]
# 过滤电机ID
if self.state['selected_motor'] and 'motor_id' in data.columns:
data = data[data['motor_id'] == self.state['selected_motor']]
# 降采样
if self.state['downsample_factor'] > 1:
data = data.iloc[::self.state['downsample_factor']]
return data
# 默认配置方法...
def _default_time_series_config(self) -> dict:
return {
'line_width': 1.5,
'show_points': False,
'show_grid': True,
'show_legend': True,
'stacked': False,
'y_axis_type': 'linear',
'highlight_abnormalities': True,
'time_format': '%H:%M:%S'
}
# 其他默认配置方法...
视图层核心类
python
运行
class MotorVisualizationView(Tk.Frame):
"""电机测试数据可视化主视图"""
def __init__(self, parent, controller, model):
"""
初始化视图
:param parent: 父窗口
:param controller: 控制器
:param model: 数据模型
"""
super().__init__(parent)
self.parent = parent
self.controller = controller
self.model = model
self.model.register_observer(self)
# 设置窗口标题
self.parent.title("无人机电机测试数据可视化")
# 配置主题
self.configure_theme()
# 创建布局
self._create_layout()
# 初始化子视图
self._init_subviews()
def _create_layout(self) -> None:
"""创建界面布局"""
# 主网格布局
self.grid_rowconfigure(0, weight=0) # 控制栏
self.grid_rowconfigure(1, weight=1) # 主内容区
self.grid_columnconfigure(0, weight=1)
# 创建控制栏
self.control_frame = Tk.Frame(self)
self.control_frame.grid(row=0, column=0, sticky='ew', padx=5, pady=5)
# 创建主内容区
self.content_frame = Tk.Frame(self)
self.content_frame.grid(row=1, column=0, sticky='nsew', padx=5, pady=5)
# 配置内容区布局
self.content_frame.grid_rowconfigure(0, weight=1)
self.content_frame.grid_rowconfigure(1, weight=1)
self.content_frame.grid_columnconfigure(0, weight=1)
self.content_frame.grid_columnconfigure(1, weight=1)
def _init_subviews(self) -> None:
"""初始化子视图"""
# 创建控制组件
self._create_control_widgets()
# 创建主图表(时间序列)
self.time_series_chart = TimeSeriesChartView(
self.content_frame,
self.controller,
self.model
)
self.time_series_chart.grid(row=0, column=0, columnspan=2, sticky='nsew', padx=5, pady=5)
# 创建散点图
self.scatter_chart = ScatterChartView(
self.content_frame,
self.controller,
self.model
)
self.scatter_chart.grid(row=1, column=0, sticky='nsew', padx=5, pady=5)
# 创建统计信息面板
self.stats_panel = StatisticsPanelView(
self.content_frame,
self.controller,
self.model
)
self.stats_panel.grid(row=1, column=1, sticky='nsew', padx=5, pady=5)
# 创建仪表盘视图(悬浮窗)
self.gauge_window = GaugeDashboardView(
self.parent,
self.controller,
self.model
)
self.gauge_window.withdraw() # 初始隐藏
def _create_control_widgets(self) -> None:
"""创建控制组件"""
# 参数选择
Tk.Label(self.control_frame, text="参数:").pack(side=LEFT, padx=5)
self.param_frame = Tk.Frame(self.control_frame)
self.param_frame.pack(side=LEFT, padx=5)
# 电机选择
Tk.Label(self.control_frame, text="电机:").pack(side=LEFT, padx=5)
self.motor_combobox = ttk.Combobox(self.control_frame, state="readonly")
self.motor_combobox.pack(side=LEFT, padx=5)
self.motor_combobox.bind("<<ComboboxSelected>>",
lambda e: self.controller.on_motor_selected(
self.motor_combobox.get()))
# 时间范围选择
self.time_range_btn = Tk.Button(
self.control_frame,
text="选择时间范围",
command=self.controller.on_time_range_select
)
self.time_range_btn.pack(side=LEFT, padx=5)
# 视图切换按钮
self.dashboard_btn = Tk.Button(
self.control_frame,
text="显示仪表盘",
command=self.controller.on_toggle_dashboard
)
self.dashboard_btn.pack(side=LEFT, padx=5)
# 导出按钮
self.export_btn = Tk.Button(
self.control_frame,
text="导出图表",
command=self.controller.on_export_charts
)
self.export_btn.pack(side=LEFT, padx=5)
# 主题切换
self.theme_btn = Tk.Button(
self.control_frame,
text="切换主题",
command=self.controller.on_toggle_theme
)
self.theme_btn.pack(side=LEFT, padx=5)
def configure_theme(self, theme: str = 'light') -> None:
"""配置界面主题"""
if theme == 'dark':
self.configure(bg='#2d2d2d')
self.control_frame.configure(bg='#2d2d2d')
for widget in self.control_frame.winfo_children():
if hasattr(widget, 'configure'):
try:
widget.configure(bg='#2d2d2d', fg='#ffffff')
except:
pass
self.content_frame.configure(bg='#2d2d2d')
else:
self.configure(bg='#ffffff')
self.control_frame.configure(bg='#ffffff')
for widget in self.control_frame.winfo_children():
if hasattr(widget, 'configure'):
try:
widget.configure(bg='#ffffff', fg='#000000')
except:
pass
self.content_frame.configure(bg='#ffffff')
# 通知子视图更新主题
for child in self.content_frame.winfo_children():
if hasattr(child, 'configure_theme'):
child.configure_theme(theme)
def on_model_change(self, change_type: str, data: dict = None) -> None:
"""响应模型变化"""
if change_type == 'parameter_selection':
# 更新参数选择UI
self._update_parameter_checkboxes()
elif change_type == 'motor_ids':
# 更新电机选择下拉框
self.motor_combobox['values'] = self.model.state['motor_ids']
if self.model.state['motor_ids']:
self.motor_combobox.current(0)
self.model.state['selected_motor'] = self.model.state['motor_ids'][0]
elif change_type == 'theme':
# 更新主题
self.configure_theme(self.model.state['theme'])
# 其他变化类型由子视图处理
def _update_parameter_checkboxes(self) -> None:
"""更新参数选择复选框"""
# 清除现有复选框
for widget in self.param_frame.winfo_children():
widget.destroy()
# 创建新的复选框
available_params = self.model.state['selected_parameters']
for param in available_params:
var = Tk.BooleanVar(value=True)
cb = Tk.Checkbutton(
self.param_frame,
text=param,
variable=var,
command=lambda p=param, v=var: self.controller.on_parameter_toggled(p, v.get())
)
cb.pack(side=LEFT, padx=2)
控制层核心类
python
运行
class VisualizationController:
"""可视化模块的控制器,处理用户交互和业务逻辑"""
def __init__(self, model):
"""
初始化控制器
:param model: 数据模型
"""
self.model = model
self.views = []
def register_view(self, view) -> None:
"""注册视图"""
if view not in self.views:
self.views.append(view)
def on_parameter_toggled(self, parameter: str, is_selected: bool) -> None:
"""处理参数选择变化"""
current_selection = self.model.state['selected_parameters']
if is_selected and parameter not in current_selection:
new_selection = current_selection + [parameter]
elif not is_selected and parameter in current_selection:
new_selection = [p for p in current_selection if p != parameter]
else:
return # 没有变化
self.model.set_parameter_selection(new_selection)
def on_motor_selected(self, motor_id) -> None:
"""处理电机选择变化"""
try:
motor_id = int(motor_id)
if motor_id in self.model.state['motor_ids']:
self.model.state['selected_motor'] = motor_id
self.model.notify_observers('motor_selected', {'motor_id': motor_id})
except ValueError:
pass
def on_time_range_select(self) -> None:
"""处理时间范围选择"""
# 获取数据的时间范围
data = self.model.get_filtered_data()
if data.empty:
return
min_time = data.index.min()
max_time = data.index.max()
# 创建时间范围选择对话框
dialog = TimeRangeSelectionDialog(
self.views[0].parent,
min_time,
max_time
)
self.views[0].parent.wait_window(dialog)
# 处理选择结果
if dialog.result:
start_time, end_time = dialog.result
self.model.set_time_range(start_time, end_time)
def on_toggle_dashboard(self) -> None:
"""切换仪表盘显示状态"""
for view in self.views:
if hasattr(view, 'gauge_window'):
if view.gauge_window.winfo_viewable():
view.gauge_window.withdraw()
else:
view.gauge_window.deiconify()
view.gauge_window.geometry("600x400+100+100")
return
def on_export_charts(self) -> None:
"""导出当前图表"""
# 打开文件对话框选择保存位置
from tkinter import filedialog
file_path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG files", "*.png"), ("PDF files", "*.pdf"), ("All files", "*.*")]
)
if file_path:
# 通知所有图表视图导出
for view in self.views:
if hasattr(view, 'export_to_file'):
view.export_to_file(file_path)
def on_toggle_theme(self) -> None:
"""切换主题"""
current_theme = self.model.state['theme']
new_theme = 'dark' if current_theme == 'light' else 'light'
self.model.state['theme'] = new_theme
self.model.notify_observers('theme', {'theme': new_theme})
def on_zoom_request(self, chart_id: str, zoom_level: float) -> None:
"""处理缩放请求"""
# 调整降采样因子实现缩放效果
current_factor = self.model.state['downsample_factor']
new_factor = max(1, int(current_factor * (1 / zoom_level)))
if new_factor != current_factor:
self.model.state['downsample_factor'] = new_factor
self.model.notify_observers('zoom', {'chart_id': chart_id, 'factor': new_factor})
def on_data_point_click(self, chart_id: str, point_data: dict) -> None:
"""处理数据点点击事件"""
# 通知所有视图高亮显示相关数据点
self.model.notify_observers(
'data_point_click',
{'chart_id': chart_id, 'data': point_data}
)
7.1.2 图表组件设计
针对电机测试数据的特点,设计了多种专用图表组件:
- 时间序列图表组件
电机测试数据时间序列图表组件
V1
time_series_chart.py
import tkinter as tk
from tkinter import ttk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import matplotlib.dates as mdates
import pandas as pd
import numpy as np
from matplotlib.widgets import SpanSelector
import matplotlib.patches as patches
class TimeSeriesChartView(tk.Frame):
"""时间序列图表视图,显示电机参数随时间的变化"""
def __init__(self, parent, controller, model, title="电机参数时间序列"):
"""
初始化时间序列图表
:param parent: 父容器
:param controller: 控制器
:param model: 数据模型
:param title: 图表标题
"""
super().__init__(parent)
self.parent = parent
self.controller = controller
self.model = model
self.model.register_observer(self)
self.title = title
# 图表配置
self.config = self.model.chart_configs['time_series'].copy()
# 创建Matplotlib图表
self._create_figure()
# 创建交互组件
self._create_interactive_widgets()
# 初始化数据
self.update_chart()
def _create_figure(self) -> None:
"""创建Matplotlib图表和画布"""
# 创建图表
self.figure = Figure(figsize=(10, 6), dpi=100)
self.axes = self.figure.add_subplot(111)
self.figure.subplots_adjust(bottom=0.2) # 留出空间给时间选择器
# 设置标题和轴标签
self.axes.set_title(self.title)
self.axes.set_xlabel("时间")
self.axes.set_ylabel("值")
# 配置网格
self.axes.grid(self.config['show_grid'], linestyle='--', alpha=0.7)
# 配置X轴日期格式
self.axes.xaxis.set_major_formatter(mdates.DateFormatter(self.config['time_format']))
self.figure.autofmt_xdate() # 自动旋转日期标签
# 创建Tkinter画布
self.canvas = FigureCanvasTkAgg(self.figure, master=self)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self.canvas.draw()
# 添加导航工具栏
self.toolbar = NavigationToolbar2Tk(self.canvas, self)
self.toolbar.update()
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
def _create_interactive_widgets(self) -> None:
"""创建交互组件"""
# 添加时间范围选择器
self.span_selector = SpanSelector(
self.axes,
self._on_span_select,
'horizontal',
useblit=True,
rectprops=dict(alpha=0.5, facecolor='red')
&n