《边学边练:Python 从零开始:无人机直流电机测试数据采集与可视化全攻略 4》

目录

  1. 项目背景与需求分析
  2. 开发环境搭建(Windows/Linux)
  3. Python 基础语法速成
  4. 串口数据通信原理与实现
  5. 多设备 CSV 文件管理系统
  6. 数据分析核心技术
  7. 可视化图表设计与实现
  8. 完整项目架构与代码
  9. 部署与运行指南
  10. 故障排除与优化
  11. 进阶学习路径

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值