|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
|
from PyQt5.QtCore import QSize, Qt, QTimer, QEvent
|
|
|
from PyQt5.QtGui import QPixmap, QIcon
|
|
|
from PyQt5.QtWidgets import (QApplication, QGridLayout, QListWidget, QListWidgetItem,
|
|
|
QToolBar, QAction, QLabel, QWidget, QVBoxLayout, QHBoxLayout,
|
|
|
QSplitter, QGroupBox, QLineEdit, QComboBox, QTextEdit,
|
|
|
QCheckBox, QFrame, QSpacerItem, QSizePolicy, QHeaderView, QToolButton)
|
|
|
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
|
|
from utils import Globals
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
import matplotlib.dates as mdates
|
|
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
|
from matplotlib.backends.backend_qt import NavigationToolbar2QT as NavigationToolbar
|
|
|
from matplotlib.figure import Figure
|
|
|
import numpy as np
|
|
|
import datetime
|
|
|
import sys
|
|
|
import pandas as pd
|
|
|
import bisect
|
|
|
from matplotlib.widgets import RectangleSelector
|
|
|
import matplotlib.patches as patches
|
|
|
from matplotlib.widgets import SpanSelector
|
|
|
from matplotlib.gridspec import GridSpec
|
|
|
|
|
|
# 配置中文字体支持
|
|
|
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
|
|
|
plt.rcParams['axes.unicode_minus'] = False
|
|
|
class TrendWidgets(QWidget):
|
|
|
def __init__(self):
|
|
|
super().__init__()
|
|
|
self.setWindowTitle("历史趋势浏览器")
|
|
|
self.setMinimumSize(120, 120)
|
|
|
# 设置窗口标志,禁用窗口拖拽
|
|
|
self.setWindowFlags(QtCore.Qt.WindowType.Window)
|
|
|
# 初始化数据
|
|
|
self.historyDB = None
|
|
|
self.currentVarName = None
|
|
|
self.xData = []
|
|
|
self.yData = []
|
|
|
# 多变量支持
|
|
|
self.multiVarData = {} # 存储多个变量的数据 {varName: {'x': [], 'y': []}}
|
|
|
self.selectedVars = [] # 当前选中的变量列表
|
|
|
self.varColors = {} # 存储变量颜色 {varName: color}
|
|
|
# 初始化历史缩放记录
|
|
|
self.history = []
|
|
|
self.historyIndex = 0
|
|
|
# 主布局
|
|
|
mainLayout = QHBoxLayout(self)
|
|
|
mainLayout.setSpacing(10)
|
|
|
mainLayout.setContentsMargins(10, 10, 10, 10)
|
|
|
# 左侧变量列表区域
|
|
|
self.createVariableListPanel()
|
|
|
mainLayout.addWidget(self.variableListGroup, 1) # 1份宽度
|
|
|
# 右侧趋势图区域
|
|
|
self.trendViewerGroup = QWidget()
|
|
|
self.createTrendViewerPanel()
|
|
|
mainLayout.addWidget(self.trendViewerGroup, 6) # 6份宽度
|
|
|
# 连接信号
|
|
|
self.connectSignals()
|
|
|
Globals.setValue('HistoryWidget', self)
|
|
|
|
|
|
def createVariableListPanel(self):
|
|
|
# 创建变量列表面板
|
|
|
self.variableListGroup = QGroupBox("变量列表")
|
|
|
layout = QVBoxLayout(self.variableListGroup)
|
|
|
# 搜索框单独一行
|
|
|
searchInputLayout = QHBoxLayout()
|
|
|
self.searchInput = QLineEdit()
|
|
|
self.searchInput.setPlaceholderText("搜索变量...")
|
|
|
self.searchInput.textChanged.connect(self.filterVarList)
|
|
|
self.searchInput.setMinimumWidth(220)
|
|
|
searchInputLayout.addWidget(self.searchInput)
|
|
|
layout.addLayout(searchInputLayout)
|
|
|
# 变量列表
|
|
|
self.varListWidget = QListWidget()
|
|
|
self.varListWidget.itemDoubleClicked.connect(self.showVarTrend)
|
|
|
self.varListWidget.itemSelectionChanged.connect(self.onVarSelected)
|
|
|
layout.addWidget(self.varListWidget)
|
|
|
# 时间选择+便捷查询区域
|
|
|
searchLayout = QHBoxLayout()
|
|
|
from PyQt5.QtCore import QDateTime
|
|
|
self.startTimeEdit = QtWidgets.QDateTimeEdit()
|
|
|
self.startTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
|
|
|
self.startTimeEdit.setCalendarPopup(True)
|
|
|
self.endTimeEdit = QtWidgets.QDateTimeEdit()
|
|
|
self.endTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
|
|
|
self.endTimeEdit.setCalendarPopup(True)
|
|
|
now = QDateTime.currentDateTime()
|
|
|
self.startTimeEdit.setDateTime(now.addDays(-1))
|
|
|
self.endTimeEdit.setDateTime(now)
|
|
|
searchLayout.addWidget(self.startTimeEdit)
|
|
|
searchLayout.addWidget(self.endTimeEdit)
|
|
|
self.quickRangeCombo = QtWidgets.QComboBox()
|
|
|
self.quickRangeCombo.addItems(["最近一天", "最近6小时", "最近1小时", "最近30分钟", "自定义"])
|
|
|
self.quickRangeCombo.currentIndexChanged.connect(self.onQuickRangeChanged)
|
|
|
searchLayout.addWidget(self.quickRangeCombo)
|
|
|
self.queryBtn = QToolButton()
|
|
|
self.queryBtn.setText("查询")
|
|
|
self.queryBtn.setToolTip("按时间范围查询变量数据")
|
|
|
self.queryBtn.clicked.connect(self.onTimeRangeQuery)
|
|
|
searchLayout.addWidget(self.queryBtn)
|
|
|
layout.addLayout(searchLayout)
|
|
|
# 按钮区域
|
|
|
buttonLayout = QHBoxLayout()
|
|
|
# 刷新按钮
|
|
|
refreshBtn = QToolButton()
|
|
|
refreshBtn.setText("刷新列表")
|
|
|
refreshBtn.clicked.connect(self.refreshVarList)
|
|
|
buttonLayout.addWidget(refreshBtn)
|
|
|
# 添加到趋势图按钮
|
|
|
addToTrendBtn = QToolButton()
|
|
|
addToTrendBtn.setText("添加到趋势图")
|
|
|
addToTrendBtn.setToolTip("将选中的变量添加到趋势图")
|
|
|
addToTrendBtn.clicked.connect(self.addSelectedVarsToTrend)
|
|
|
buttonLayout.addWidget(addToTrendBtn)
|
|
|
# 清除所有变量按钮
|
|
|
clearAllBtn = QToolButton()
|
|
|
clearAllBtn.setText("清除所有")
|
|
|
clearAllBtn.setToolTip("清除趋势图中的所有变量")
|
|
|
clearAllBtn.clicked.connect(self.clearAllVars)
|
|
|
buttonLayout.addWidget(clearAllBtn)
|
|
|
layout.addLayout(buttonLayout)
|
|
|
self.variableListGroup.setLayout(layout)
|
|
|
# 记录当前时间范围
|
|
|
self._query_time_range = (self.startTimeEdit.dateTime().toPyDateTime(), self.endTimeEdit.dateTime().toPyDateTime())
|
|
|
|
|
|
def createTrendViewerPanel(self):
|
|
|
# 创建趋势图查看器面板
|
|
|
layout = QVBoxLayout(self.trendViewerGroup)
|
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
|
layout.setSpacing(5)
|
|
|
# 创建包含图表的组框
|
|
|
chartGroup = QGroupBox()
|
|
|
chartLayout = QVBoxLayout(chartGroup)
|
|
|
chartLayout.setContentsMargins(5, 5, 5, 5)
|
|
|
# 创建matplotlib图表,设置主图和缩略图比例为6:1
|
|
|
self.figure = Figure(figsize=(12, 8), dpi=100)
|
|
|
gs = GridSpec(7, 1, figure=self.figure)
|
|
|
self.ax_main = self.figure.add_subplot(gs[:6, 0]) # 主图,占6份
|
|
|
self.ax_overview = self.figure.add_subplot(gs[6, 0]) # 缩略图,占1份
|
|
|
self.canvas = FigureCanvas(self.figure)
|
|
|
chartLayout.addWidget(self.canvas, 20)
|
|
|
# 底部信息栏(含状态显示)
|
|
|
infoLayout = QHBoxLayout()
|
|
|
infoLayout.setContentsMargins(5, 5, 5, 5)
|
|
|
self.varNameLabel = QLabel("当前变量: 无")
|
|
|
self.dataCountLabel = QLabel("数据点: 0")
|
|
|
self.timeRangeLabel = QLabel("时间范围: 无数据")
|
|
|
self.statusLabel = QLabel("就绪")
|
|
|
self.statusLabel.setFont(QtGui.QFont("Arial", 9))
|
|
|
self.statusLabel.setStyleSheet("color: #555; padding: 2px;")
|
|
|
infoLayout.addWidget(self.varNameLabel)
|
|
|
infoLayout.addStretch()
|
|
|
infoLayout.addWidget(self.dataCountLabel)
|
|
|
infoLayout.addStretch()
|
|
|
infoLayout.addWidget(self.timeRangeLabel)
|
|
|
infoLayout.addStretch()
|
|
|
infoLayout.addWidget(self.statusLabel)
|
|
|
chartLayout.addLayout(infoLayout, 1)
|
|
|
layout.addWidget(chartGroup)
|
|
|
# 悬浮信息气泡label
|
|
|
self.infoBubble = QLabel(self.trendViewerGroup)
|
|
|
self.infoBubble.setStyleSheet("background:rgba(255,255,220,0.95); border:1px solid #aaa; border-radius:4px; padding:4px; color:#222;")
|
|
|
self.infoBubble.setFont(QtGui.QFont("Consolas", 10))
|
|
|
self.infoBubble.setVisible(False)
|
|
|
# 连接鼠标事件,支持拖拽平移
|
|
|
self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
|
|
|
self.canvas.mpl_connect('axes_leave_event', self.on_mouse_leave)
|
|
|
self.canvas.mpl_connect('scroll_event', self.on_mouse_wheel)
|
|
|
self.canvas.mpl_connect('button_press_event', self.on_mouse_press)
|
|
|
self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
|
|
|
# 拖拽状态
|
|
|
self._is_panning = False
|
|
|
self._pan_start_x = None
|
|
|
self._pan_start_xlim = None
|
|
|
# 初始化选择器
|
|
|
self.selector = None
|
|
|
self.is_selecting = False
|
|
|
self.zoom_start = None
|
|
|
# 添加SpanSelector用于区间选择
|
|
|
self.span = SpanSelector(
|
|
|
self.ax_overview, self.on_select, 'horizontal',
|
|
|
useblit=True, interactive=True, props=dict(alpha=0.3, facecolor='orange')
|
|
|
)
|
|
|
# 绑定双击事件恢复全局视图
|
|
|
self.canvas.mpl_connect('button_press_event', self.on_overview_double_click)
|
|
|
self._selected_range = None # 记录选中区间
|
|
|
# 设置下方缩略图x轴显示更多时间内容
|
|
|
import matplotlib.dates as mdates
|
|
|
from matplotlib.ticker import MaxNLocator
|
|
|
self.ax_overview.xaxis.set_major_locator(mdates.AutoDateLocator())
|
|
|
self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
|
|
|
|
|
def connectSignals(self):
|
|
|
# 连接所有信号
|
|
|
self.ax_overview.callbacks.connect('xlim_changed', self.on_overview_xlim_changed)
|
|
|
|
|
|
def exchangeProject(self):
|
|
|
# 项目切换时调用
|
|
|
self.historyDB = Globals.getValue('historyDBManage')
|
|
|
self.refreshVarList()
|
|
|
|
|
|
def refreshVarList(self):
|
|
|
# 刷新变量列表
|
|
|
self.varListWidget.clear()
|
|
|
self.statusLabel.setText("正在加载变量列表...")
|
|
|
if not self.historyDB or not hasattr(self.historyDB, 'getAllVarNames'):
|
|
|
self.statusLabel.setText("未连接历史数据库,无法获取变量列表")
|
|
|
return
|
|
|
varNames = self.historyDB.getAllVarNames()
|
|
|
# print(varNames)
|
|
|
# 对变量名称进行排序
|
|
|
varNames.sort(key=lambda x: x.lower())
|
|
|
# 添加排序后的变量到列表
|
|
|
for name in varNames:
|
|
|
item = QListWidgetItem(name)
|
|
|
self.varListWidget.addItem(item)
|
|
|
self.statusLabel.setText(f"已加载 {len(varNames)} 个变量(已排序)")
|
|
|
|
|
|
def sortVarList(self):
|
|
|
# 手动排序变量列表
|
|
|
try:
|
|
|
varNames = []
|
|
|
for i in range(self.varListWidget.count()):
|
|
|
item = self.varListWidget.item(i)
|
|
|
if item and not item.isHidden():
|
|
|
varNames.append(item.text())
|
|
|
varNames.sort(key=lambda x: x.lower())
|
|
|
self.varListWidget.clear()
|
|
|
for name in varNames:
|
|
|
item = QListWidgetItem(name)
|
|
|
self.varListWidget.addItem(item)
|
|
|
self.statusLabel.setText(f"已排序 {len(varNames)} 个变量")
|
|
|
except Exception as e:
|
|
|
self.statusLabel.setText(f"排序失败: {str(e)}")
|
|
|
|
|
|
def addSelectedVarsToTrend(self):
|
|
|
# 将选中的变量添加到趋势图
|
|
|
selectedItems = self.varListWidget.selectedItems()
|
|
|
if not selectedItems:
|
|
|
self.statusLabel.setText("请先选择要添加的变量")
|
|
|
return
|
|
|
|
|
|
addedCount = 0
|
|
|
for item in selectedItems:
|
|
|
varName = item.text()
|
|
|
if varName not in self.selectedVars:
|
|
|
self.selectedVars.append(varName)
|
|
|
self.loadVarData(varName)
|
|
|
addedCount += 1
|
|
|
if addedCount > 0:
|
|
|
self.updateMultiVarChart()
|
|
|
self.statusLabel.setText(f"已添加 {addedCount} 个变量到趋势图")
|
|
|
else:
|
|
|
self.statusLabel.setText("选中的变量已在趋势图中")
|
|
|
|
|
|
def onTimeRangeQuery(self):
|
|
|
# 查询按钮点击,记录时间范围
|
|
|
self._query_time_range = (
|
|
|
self.startTimeEdit.dateTime().toPyDateTime(),
|
|
|
self.endTimeEdit.dateTime().toPyDateTime()
|
|
|
)
|
|
|
import datetime
|
|
|
# 重新加载所有已选变量的数据
|
|
|
for varName in self.selectedVars:
|
|
|
start_time, end_time = self._query_time_range
|
|
|
if isinstance(start_time, datetime.datetime):
|
|
|
start_time = start_time.isoformat()
|
|
|
if isinstance(end_time, datetime.datetime):
|
|
|
end_time = end_time.isoformat()
|
|
|
if self.historyDB and hasattr(self.historyDB, 'queryVarHistory'):
|
|
|
data, timeList = self.historyDB.queryVarHistory(varName, start_time, end_time)
|
|
|
xData = []
|
|
|
for t in timeList:
|
|
|
dt = None
|
|
|
if isinstance(t, datetime.datetime):
|
|
|
dt = t
|
|
|
elif hasattr(t, 'to_pydatetime'):
|
|
|
dt = t.to_pydatetime()
|
|
|
elif isinstance(t, str):
|
|
|
try:
|
|
|
dt = datetime.datetime.fromisoformat(t)
|
|
|
except:
|
|
|
continue
|
|
|
if dt:
|
|
|
xData.append(dt)
|
|
|
self.multiVarData[varName] = {'x': xData, 'y': data}
|
|
|
self.updateMultiVarChart()
|
|
|
self.statusLabel.setText(f"已设置时间范围: {self._query_time_range[0].strftime('%Y-%m-%d %H:%M:%S')} ~ {self._query_time_range[1].strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
|
|
def loadVarData(self, varName):
|
|
|
# 加载单个变量的数据,按选定时间范围
|
|
|
try:
|
|
|
self.statusLabel.setText(f"加载 {varName} 数据...")
|
|
|
import datetime
|
|
|
if hasattr(self, '_query_time_range') and self._query_time_range:
|
|
|
start_time, end_time = self._query_time_range
|
|
|
else:
|
|
|
start_time = None
|
|
|
end_time = None
|
|
|
# 从数据库获取数据
|
|
|
if self.historyDB and hasattr(self.historyDB, 'queryVarHistory'):
|
|
|
print(start_time, end_time)
|
|
|
data, timeList = self.historyDB.queryVarHistory(varName, start_time, end_time)
|
|
|
# 直接使用数据库返回的时间,不做任何时区转换
|
|
|
xData = []
|
|
|
for t in timeList:
|
|
|
if isinstance(t, datetime.datetime):
|
|
|
xData.append(t)
|
|
|
elif isinstance(t, pd.Timestamp):
|
|
|
xData.append(t.to_pydatetime())
|
|
|
elif isinstance(t, str):
|
|
|
try:
|
|
|
# 直接按本地时间字符串解析
|
|
|
xData.append(datetime.datetime.fromisoformat(t))
|
|
|
except:
|
|
|
xData.append(datetime.datetime.now())
|
|
|
else:
|
|
|
xData.append(datetime.datetime.now())
|
|
|
# 存储数据
|
|
|
self.multiVarData[varName] = {'x': xData, 'y': data}
|
|
|
# 分配颜色
|
|
|
if varName not in self.varColors:
|
|
|
colors = ['blue', 'red', 'green', 'cyan', 'magenta', 'yellow', 'black']
|
|
|
colorIndex = len(self.varColors) % len(colors)
|
|
|
self.varColors[varName] = colors[colorIndex]
|
|
|
except Exception as e:
|
|
|
self.statusLabel.setText(f"加载 {varName} 数据失败: {str(e)}")
|
|
|
|
|
|
def updateMultiVarChart(self):
|
|
|
import matplotlib.dates as mdates
|
|
|
# 更新多变量图表
|
|
|
self.ax_main.clear()
|
|
|
self.ax_overview.clear()
|
|
|
# 设置网格
|
|
|
self.ax_main.grid(True, alpha=0.3)
|
|
|
self.ax_overview.grid(True, alpha=0.3)
|
|
|
# 复用数据处理逻辑,提升性能
|
|
|
for varName in self.selectedVars:
|
|
|
if varName in self.multiVarData:
|
|
|
data = self.multiVarData[varName]
|
|
|
if data['x'] and data['y']:
|
|
|
# 处理断线数据(复用逻辑)
|
|
|
x_processed, y_processed = self._process_break_line_data(data['x'], data['y'])
|
|
|
|
|
|
# 缩略图:显示全量数据
|
|
|
self.ax_overview.plot(x_processed, y_processed, color=self.varColors[varName], linewidth=1, alpha=0.7)
|
|
|
|
|
|
# 主图:根据选中区间显示数据
|
|
|
if self._selected_range is not None:
|
|
|
xlim = self._selected_range
|
|
|
# 筛选区间数据
|
|
|
x_num = [mdates.date2num(xx) for xx in x_processed if xx is not None]
|
|
|
x_filtered, y_filtered = [], []
|
|
|
for xx, yy in zip(x_processed, y_processed):
|
|
|
if xx is not None:
|
|
|
xn = mdates.date2num(xx)
|
|
|
if xlim[0] <= xn <= xlim[1]:
|
|
|
x_filtered.append(xx)
|
|
|
y_filtered.append(yy)
|
|
|
x_main, y_main = x_filtered, y_filtered
|
|
|
else:
|
|
|
# 没有选择范围时,主图显示全部数据
|
|
|
x_main, y_main = x_processed, y_processed
|
|
|
|
|
|
self.ax_main.plot(x_main, y_main, color=self.varColors[varName], linewidth=2, marker='o', markersize=4, label=varName)
|
|
|
# 设置时间格式
|
|
|
self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
|
|
|
# 增加主图和缩略图的坐标轴刻度数量,并显示完整日期时间
|
|
|
from matplotlib.ticker import MaxNLocator
|
|
|
self.ax_main.xaxis.set_major_locator(mdates.AutoDateLocator())
|
|
|
self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M:%S'))
|
|
|
self.ax_main.yaxis.set_major_locator(MaxNLocator(nbins=16, prune=None))
|
|
|
self.ax_overview.xaxis.set_major_locator(mdates.AutoDateLocator())
|
|
|
self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M:%S'))
|
|
|
self.ax_overview.yaxis.set_major_locator(MaxNLocator(nbins=16, prune=None))
|
|
|
# 刻度数量更多
|
|
|
self.ax_main.xaxis.set_major_locator(MaxNLocator(nbins=16, prune=None))
|
|
|
self.ax_overview.xaxis.set_major_locator(MaxNLocator(nbins=16, prune=None))
|
|
|
# 设置标签
|
|
|
self.ax_main.set_title('历史趋势图', fontsize=12, fontweight='bold')
|
|
|
self.ax_main.set_ylabel('数值')
|
|
|
self.ax_overview.set_xlabel('时间')
|
|
|
self.ax_overview.set_ylabel('数值')
|
|
|
# 添加图例
|
|
|
if self.selectedVars:
|
|
|
self.ax_main.legend(loc='upper right')
|
|
|
# 自动调整布局
|
|
|
self.figure.tight_layout()
|
|
|
# 刷新画布
|
|
|
self.canvas.draw()
|
|
|
|
|
|
def _process_break_line_data(self, x_raw, y_raw):
|
|
|
"""处理断线数据:间隔大于2分钟的点不连线"""
|
|
|
x, y = [], []
|
|
|
prev_time = None
|
|
|
for xi, yi in zip(x_raw, y_raw):
|
|
|
if prev_time is not None and (xi - prev_time).total_seconds() > 60:
|
|
|
# 断线时只在y中插入None,x继续插入当前时间戳
|
|
|
x.append(xi)
|
|
|
y.append(None)
|
|
|
x.append(xi)
|
|
|
y.append(yi)
|
|
|
prev_time = xi
|
|
|
return x, y
|
|
|
|
|
|
def filterVarList(self, text):
|
|
|
# 本地过滤变量列表
|
|
|
for i in range(self.varListWidget.count()):
|
|
|
item = self.varListWidget.item(i)
|
|
|
if item is not None:
|
|
|
item.setHidden(text.lower() not in item.text().lower())
|
|
|
|
|
|
def onVarSelected(self):
|
|
|
# 当选中一个变量时(非双击)
|
|
|
selectedItems = self.varListWidget.selectedItems()
|
|
|
if selectedItems:
|
|
|
self.statusLabel.setText(f"已选择: {selectedItems[0].text()}")
|
|
|
|
|
|
def showVarTrend(self, item):
|
|
|
# 显示选中变量的趋势图(与多变量流程一致)
|
|
|
varName = item.text()
|
|
|
self.currentVarName = varName
|
|
|
self.statusLabel.setText(f"加载 {varName} 历史趋势...")
|
|
|
# 清空多变量数据,只保留当前变量
|
|
|
self.selectedVars = [varName]
|
|
|
self.multiVarData = {}
|
|
|
self.varColors = {}
|
|
|
self.loadVarData(varName)
|
|
|
# 更新UI信息
|
|
|
if varName in self.multiVarData:
|
|
|
data = self.multiVarData[varName]['y']
|
|
|
xdata = self.multiVarData[varName]['x']
|
|
|
else:
|
|
|
data = []
|
|
|
xdata = []
|
|
|
self.varNameLabel.setText(f"当前变量: {varName}")
|
|
|
self.dataCountLabel.setText(f"数据点: {len(data)}")
|
|
|
if xdata:
|
|
|
import datetime
|
|
|
import pandas as pd
|
|
|
def to_dt(t):
|
|
|
if isinstance(t, datetime.datetime):
|
|
|
return t
|
|
|
elif isinstance(t, pd.Timestamp):
|
|
|
return t.to_pydatetime()
|
|
|
elif isinstance(t, (int, float)):
|
|
|
return datetime.datetime.fromtimestamp(t)
|
|
|
else:
|
|
|
return datetime.datetime.now()
|
|
|
start_time = to_dt(xdata[0]).strftime('%Y-%m-%d %H:%M')
|
|
|
end_time = to_dt(xdata[-1]).strftime('%Y-%m-%d %H:%M')
|
|
|
self.timeRangeLabel.setText(f"时间范围: {start_time} - {end_time}")
|
|
|
else:
|
|
|
self.timeRangeLabel.setText("时间范围: 无数据")
|
|
|
# 刷新多变量趋势图
|
|
|
self.updateMultiVarChart()
|
|
|
self.resetZoom()
|
|
|
self.statusLabel.setText(f"{varName} 趋势已显示")
|
|
|
|
|
|
def updateChart(self):
|
|
|
# 用当前数据更新图表
|
|
|
self.ax_main.clear()
|
|
|
self.ax_overview.clear()
|
|
|
|
|
|
# 设置网格
|
|
|
self.ax_main.grid(True, alpha=0.3)
|
|
|
self.ax_overview.grid(True, alpha=0.3)
|
|
|
|
|
|
# 绘制数据
|
|
|
if self.xData and self.yData:
|
|
|
self.ax_main.plot(self.xData, self.yData,
|
|
|
color='blue',
|
|
|
linewidth=2,
|
|
|
marker='o',
|
|
|
markersize=4,
|
|
|
label=self.currentVarName)
|
|
|
|
|
|
# 概览图(降采样)
|
|
|
step = max(1, len(self.xData) // 10)
|
|
|
x_overview = self.xData[::step]
|
|
|
y_overview = self.yData[::step]
|
|
|
self.ax_overview.plot(x_overview, y_overview,
|
|
|
color='blue',
|
|
|
linewidth=1,
|
|
|
alpha=0.7)
|
|
|
|
|
|
# 设置时间格式
|
|
|
self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
|
|
|
self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
|
|
|
|
|
|
# 设置标签
|
|
|
self.ax_main.set_title('历史趋势图', fontsize=12, fontweight='bold')
|
|
|
self.ax_main.set_ylabel('数值')
|
|
|
self.ax_overview.set_xlabel('时间')
|
|
|
self.ax_overview.set_ylabel('数值')
|
|
|
|
|
|
# 添加图例
|
|
|
if self.currentVarName:
|
|
|
self.ax_main.legend(loc='upper right')
|
|
|
|
|
|
# 自动调整布局
|
|
|
self.figure.tight_layout()
|
|
|
|
|
|
# 刷新画布
|
|
|
self.canvas.draw()
|
|
|
|
|
|
def on_mouse_press(self, event):
|
|
|
# 鼠标左键按下,准备平移
|
|
|
if event.inaxes == self.ax_main and event.button == 1 and event.xdata is not None:
|
|
|
self._is_panning = True
|
|
|
self._pan_start_x = event.xdata
|
|
|
self._pan_start_xlim = self.ax_main.get_xlim()
|
|
|
|
|
|
def on_mouse_release(self, event):
|
|
|
# 鼠标左键松开,结束平移
|
|
|
if self._is_panning:
|
|
|
self._is_panning = False
|
|
|
self._pan_start_x = None
|
|
|
self._pan_start_xlim = None
|
|
|
# 拖拽结束后再重绘
|
|
|
self.canvas.draw_idle()
|
|
|
|
|
|
def on_mouse_move(self, event):
|
|
|
# 鼠标移动事件,显示悬浮气泡+平移主图
|
|
|
if self._is_panning and event.inaxes == self.ax_main and event.xdata is not None and self._pan_start_x is not None:
|
|
|
dx = event.xdata - self._pan_start_x
|
|
|
xlim0, xlim1 = self._pan_start_xlim
|
|
|
self.ax_main.set_xlim(xlim0 - dx, xlim1 - dx)
|
|
|
# 拖拽时不重绘,提高流畅度
|
|
|
# self.canvas.draw_idle()
|
|
|
# 悬浮气泡逻辑(不变)
|
|
|
if event.inaxes == self.ax_main and event.xdata is not None and event.ydata is not None:
|
|
|
info_text = f"时间: {mdates.num2date(event.xdata).strftime('%Y-%m-%d %H:%M:%S')}<br>"
|
|
|
if self.selectedVars:
|
|
|
for varName in self.selectedVars:
|
|
|
if varName in self.multiVarData:
|
|
|
data = self.multiVarData[varName]
|
|
|
if data['x'] and data['y']:
|
|
|
closest_idx = self.find_closest_point(data['x'], event.xdata)
|
|
|
if closest_idx is not None and closest_idx < len(data['y']):
|
|
|
value = data['y'][closest_idx]
|
|
|
info_text += f"{varName}: {value:.3f}<br>"
|
|
|
self.infoBubble.setText(f"<span style='white-space:pre'>{info_text}</span>")
|
|
|
x = int(event.guiEvent.x())
|
|
|
y = int(event.guiEvent.y())
|
|
|
self.infoBubble.move(x + 16, y + 8)
|
|
|
self.infoBubble.adjustSize()
|
|
|
self.infoBubble.setVisible(True)
|
|
|
else:
|
|
|
self.infoBubble.setVisible(False)
|
|
|
# self.canvas.draw() # 性能优化:去除重绘
|
|
|
|
|
|
def on_mouse_leave(self, event):
|
|
|
# 鼠标离开主图时隐藏气泡
|
|
|
self.infoBubble.setVisible(False)
|
|
|
|
|
|
def find_closest_point(self, x_data, target_x):
|
|
|
# 找到最接近目标X值的数据点索引
|
|
|
if not x_data:
|
|
|
return None
|
|
|
|
|
|
# 将datetime转换为matplotlib日期
|
|
|
if isinstance(x_data[0], datetime.datetime):
|
|
|
x_nums = mdates.date2num(x_data)
|
|
|
else:
|
|
|
x_nums = x_data
|
|
|
|
|
|
# 使用二分查找
|
|
|
left = 0
|
|
|
right = len(x_nums) - 1
|
|
|
while left <= right:
|
|
|
mid = (left + right) // 2
|
|
|
if x_nums[mid] == target_x:
|
|
|
return mid
|
|
|
elif x_nums[mid] < target_x:
|
|
|
left = mid + 1
|
|
|
else:
|
|
|
right = mid - 1
|
|
|
# 找到最接近的点
|
|
|
if left >= len(x_nums):
|
|
|
return len(x_nums) - 1
|
|
|
elif right < 0:
|
|
|
return 0
|
|
|
else:
|
|
|
if abs(x_nums[left] - target_x) < abs(x_nums[right] - target_x):
|
|
|
return left
|
|
|
else:
|
|
|
return right
|
|
|
|
|
|
def on_overview_xlim_changed(self, event_ax):
|
|
|
# 当概览图范围变化时更新主图
|
|
|
if event_ax == self.ax_overview:
|
|
|
x1, x2 = self.ax_overview.get_xlim()
|
|
|
self.ax_main.set_xlim(x1)
|
|
|
self.canvas.draw()
|
|
|
|
|
|
def resetZoom(self):
|
|
|
# 重置到完整视图
|
|
|
if self.xData:
|
|
|
self.ax_main.set_xlim(min(self.xData), max(self.xData))
|
|
|
self.ax_overview.set_xlim(min(self.xData), max(self.xData))
|
|
|
self.canvas.draw()
|
|
|
|
|
|
def zoomBack(self):
|
|
|
# 退到上一个视图
|
|
|
if self.history and self.historyIndex > 0:
|
|
|
self.historyIndex -= 1
|
|
|
self.applyZoom(self.history[self.historyIndex])
|
|
|
self.backBtn.setEnabled(self.historyIndex > 0)
|
|
|
self.forwardBtn.setEnabled(True)
|
|
|
|
|
|
def zoomForward(self):
|
|
|
# 进到下一个视图
|
|
|
if self.history and self.historyIndex < len(self.history) - 1:
|
|
|
self.historyIndex += 1
|
|
|
self.applyZoom(self.history[self.historyIndex])
|
|
|
self.forwardBtn.setEnabled(self.historyIndex < len(self.history) - 1)
|
|
|
self.backBtn.setEnabled(True)
|
|
|
def applyZoom(self, zoom_range):
|
|
|
# 应用指定的缩放范围
|
|
|
x1, x2 = zoom_range
|
|
|
self.ax_main.set_xlim(x1, x2)
|
|
|
self.canvas.draw()
|
|
|
|
|
|
def enableRectZoom(self):
|
|
|
# 启用框选缩放模式
|
|
|
self.statusLabel.setText("模式: 框选缩放 - 在概览图中拖动选择区域")
|
|
|
|
|
|
def enablePanMode(self):
|
|
|
# 启用平移模式
|
|
|
self.statusLabel.setText("模式: 平移视图 - 在主图中拖动移动视图")
|
|
|
def onProjectChanged(self):
|
|
|
# 项目更改时处理
|
|
|
self.exchangeProject()
|
|
|
def clearAllVars(self):
|
|
|
# 清除趋势图中的所有变量
|
|
|
self.selectedVars = []
|
|
|
self.multiVarData = {}
|
|
|
self.varColors = {}
|
|
|
self.ax_main.clear()
|
|
|
self.ax_overview.clear()
|
|
|
self.ax_main.grid(True, alpha=0.3)
|
|
|
self.ax_overview.grid(True, alpha=0.3)
|
|
|
self.canvas.draw()
|
|
|
self.statusLabel.setText("趋势图已清除")
|
|
|
self.varNameLabel.setText("当前变量: 无")
|
|
|
self.dataCountLabel.setText("数据点: 0")
|
|
|
self.timeRangeLabel.setText("时间范围: 无数据")
|
|
|
self.resetZoom()
|
|
|
|
|
|
def on_select(self, xmin, xmax):
|
|
|
# SpanSelector回调,记录选中区间并更新主趋势图
|
|
|
self._selected_range = (xmin, xmax)
|
|
|
self.updateMultiVarChart()
|
|
|
|
|
|
def on_overview_double_click(self, event):
|
|
|
# 双击下方缩略图恢复全局视图
|
|
|
if event.dblclick and event.inaxes == self.ax_overview:
|
|
|
self.ax_main.set_xlim(auto=True)
|
|
|
self.canvas.draw_idle()
|
|
|
|
|
|
def on_mouse_wheel(self, event):
|
|
|
# 鼠标滚轮缩放主趋势图X轴
|
|
|
if event.inaxes == self.ax_main and event.xdata is not None:
|
|
|
xlim = self.ax_main.get_xlim()
|
|
|
x_center = event.xdata
|
|
|
# 缩放因子,滚轮向上放大,向下缩小
|
|
|
scale_factor = 0.8 if event.button == 'up' else 1.25
|
|
|
x_left = x_center - (x_center - xlim[0]) * scale_factor
|
|
|
x_right = x_center + (xlim[1] - x_center) * scale_factor
|
|
|
self.ax_main.set_xlim(x_left, x_right)
|
|
|
self.canvas.draw_idle()
|
|
|
|
|
|
def onQuickRangeChanged(self, idx):
|
|
|
from PyQt5.QtCore import QDateTime
|
|
|
now = QDateTime.currentDateTime()
|
|
|
if idx == 0: # 最近一天
|
|
|
self.startTimeEdit.setDateTime(now.addDays(-1))
|
|
|
self.endTimeEdit.setDateTime(now)
|
|
|
elif idx == 1: # 最近6小时
|
|
|
self.startTimeEdit.setDateTime(now.addSecs(-6*3600))
|
|
|
self.endTimeEdit.setDateTime(now)
|
|
|
elif idx == 2: # 最近1小时
|
|
|
self.startTimeEdit.setDateTime(now.addSecs(-3600))
|
|
|
self.endTimeEdit.setDateTime(now)
|
|
|
elif idx == 3: # 最近30分钟
|
|
|
self.startTimeEdit.setDateTime(now.addSecs(-1800))
|
|
|
self.endTimeEdit.setDateTime(now)
|
|
|
# idx==4为自定义,不做处理
|
|
|
# 自动触发查询
|
|
|
if idx != 4:
|
|
|
self.onTimeRangeQuery()
|
|
|
|
|
|
def performSearch(self, searchText):
|
|
|
"""统一搜索接口,供顶部搜索栏调用"""
|
|
|
try:
|
|
|
self.filterVarList(searchText)
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
print(f"趋势界面搜索失败: {e}")
|
|
|
return False
|
|
|
|
|
|
def getSearchPlaceholder(self):
|
|
|
"""获取搜索占位符文本"""
|
|
|
return "搜索历史变量..."
|
|
|
|
|
|
# 使用示例
|
|
|
if __name__ == "__main__":
|
|
|
app = QApplication(sys.argv)
|
|
|
window = TrendWidgets()
|
|
|
window.show()
|
|
|
sys.exit(app.exec_()) |