|
|
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.setObjectName("trendMainWidget")
|
|
|
|
|
|
# 初始化数据
|
|
|
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
|
|
|
|
|
|
self.initializeUI()
|
|
|
self.connectSignals()
|
|
|
Globals.setValue('HistoryWidget', self)
|
|
|
|
|
|
def initializeUI(self):
|
|
|
"""初始化用户界面"""
|
|
|
mainLayout = QHBoxLayout(self)
|
|
|
mainLayout.setSpacing(8)
|
|
|
mainLayout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
# 左侧变量列表区域 (占25%宽度)
|
|
|
self.createVariableListPanel()
|
|
|
mainLayout.addWidget(self.variableListGroup, 1)
|
|
|
|
|
|
# 右侧趋势图区域 (占75%宽度)
|
|
|
self.trendViewerGroup = QWidget()
|
|
|
self.trendViewerGroup.setObjectName("trendViewerGroup")
|
|
|
self.createTrendViewerPanel()
|
|
|
mainLayout.addWidget(self.trendViewerGroup, 10)
|
|
|
|
|
|
def createVariableListPanel(self):
|
|
|
"""创建变量列表面板"""
|
|
|
self.variableListGroup = QGroupBox("变量列表")
|
|
|
self.variableListGroup.setObjectName("trendVariableListGroup")
|
|
|
layout = QVBoxLayout(self.variableListGroup)
|
|
|
layout.setSpacing(8)
|
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
# 搜索框
|
|
|
self.searchInput = QLineEdit()
|
|
|
self.searchInput.setObjectName("trendSearchInput")
|
|
|
self.searchInput.setPlaceholderText("搜索变量...")
|
|
|
self.searchInput.textChanged.connect(self.filterVarList)
|
|
|
layout.addWidget(self.searchInput)
|
|
|
|
|
|
# 变量列表
|
|
|
self.varListWidget = QListWidget()
|
|
|
self.varListWidget.setObjectName("trendVarListWidget")
|
|
|
self.varListWidget.itemDoubleClicked.connect(self.showVarTrend)
|
|
|
self.varListWidget.itemSelectionChanged.connect(self.onVarSelected)
|
|
|
layout.addWidget(self.varListWidget)
|
|
|
|
|
|
# 时间选择区域
|
|
|
timeGroupBox = QGroupBox("时间范围")
|
|
|
timeGroupBox.setObjectName("trendTimeGroupBox")
|
|
|
timeLayout = QVBoxLayout(timeGroupBox)
|
|
|
timeLayout.setSpacing(6)
|
|
|
|
|
|
# 快速选择
|
|
|
self.quickRangeCombo = QtWidgets.QComboBox()
|
|
|
self.quickRangeCombo.setObjectName("trendQuickRangeCombo")
|
|
|
self.quickRangeCombo.addItems(["最近一天", "最近6小时", "最近1小时", "最近30分钟", "自定义"])
|
|
|
self.quickRangeCombo.currentIndexChanged.connect(self.onQuickRangeChanged)
|
|
|
timeLayout.addWidget(self.quickRangeCombo)
|
|
|
|
|
|
# 时间选择器
|
|
|
from PyQt5.QtCore import QDateTime
|
|
|
timeEditLayout = QVBoxLayout()
|
|
|
|
|
|
self.startTimeEdit = QtWidgets.QDateTimeEdit()
|
|
|
self.startTimeEdit.setObjectName("trendStartTimeEdit")
|
|
|
self.startTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
|
|
|
self.startTimeEdit.setCalendarPopup(True)
|
|
|
|
|
|
self.endTimeEdit = QtWidgets.QDateTimeEdit()
|
|
|
self.endTimeEdit.setObjectName("trendEndTimeEdit")
|
|
|
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)
|
|
|
|
|
|
timeEditLayout.addWidget(QLabel("开始时间:"))
|
|
|
timeEditLayout.addWidget(self.startTimeEdit)
|
|
|
timeEditLayout.addWidget(QLabel("结束时间:"))
|
|
|
timeEditLayout.addWidget(self.endTimeEdit)
|
|
|
|
|
|
timeLayout.addLayout(timeEditLayout)
|
|
|
layout.addWidget(timeGroupBox)
|
|
|
|
|
|
# 按钮区域
|
|
|
buttonGroupBox = QGroupBox("操作")
|
|
|
buttonGroupBox.setObjectName("trendButtonGroupBox")
|
|
|
buttonLayout = QVBoxLayout(buttonGroupBox)
|
|
|
buttonLayout.setSpacing(6)
|
|
|
|
|
|
# 第一行按钮
|
|
|
firstRowLayout = QHBoxLayout()
|
|
|
firstRowLayout.setSpacing(6)
|
|
|
|
|
|
self.queryBtn = QToolButton()
|
|
|
self.queryBtn.setObjectName("trendQueryBtn")
|
|
|
self.queryBtn.setText("查询数据")
|
|
|
self.queryBtn.setToolTip("按时间范围查询变量数据")
|
|
|
self.queryBtn.clicked.connect(self.onTimeRangeQuery)
|
|
|
firstRowLayout.addWidget(self.queryBtn)
|
|
|
|
|
|
self.refreshBtn = QToolButton()
|
|
|
self.refreshBtn.setObjectName("trendRefreshBtn")
|
|
|
self.refreshBtn.setText("刷新列表")
|
|
|
self.refreshBtn.clicked.connect(self.refreshVarList)
|
|
|
firstRowLayout.addWidget(self.refreshBtn)
|
|
|
|
|
|
buttonLayout.addLayout(firstRowLayout)
|
|
|
|
|
|
# 第二行按钮
|
|
|
secondRowLayout = QHBoxLayout()
|
|
|
secondRowLayout.setSpacing(6)
|
|
|
|
|
|
self.addToTrendBtn = QToolButton()
|
|
|
self.addToTrendBtn.setObjectName("trendAddBtn")
|
|
|
self.addToTrendBtn.setText("添加变量")
|
|
|
self.addToTrendBtn.setToolTip("将选中的变量添加到趋势图")
|
|
|
self.addToTrendBtn.clicked.connect(self.addSelectedVarsToTrend)
|
|
|
secondRowLayout.addWidget(self.addToTrendBtn)
|
|
|
|
|
|
self.clearAllBtn = QToolButton()
|
|
|
self.clearAllBtn.setObjectName("trendClearBtn")
|
|
|
self.clearAllBtn.setText("清除所有")
|
|
|
self.clearAllBtn.setToolTip("清除趋势图中的所有变量")
|
|
|
self.clearAllBtn.clicked.connect(self.clearAllVars)
|
|
|
secondRowLayout.addWidget(self.clearAllBtn)
|
|
|
|
|
|
buttonLayout.addLayout(secondRowLayout)
|
|
|
|
|
|
layout.addWidget(buttonGroupBox)
|
|
|
|
|
|
# 记录当前时间范围
|
|
|
self._queryTimeRange = (self.startTimeEdit.dateTime().toPyDateTime(), self.endTimeEdit.dateTime().toPyDateTime())
|
|
|
|
|
|
def createTrendViewerPanel(self):
|
|
|
"""创建趋势图查看器面板"""
|
|
|
layout = QVBoxLayout(self.trendViewerGroup)
|
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
layout.setSpacing(8)
|
|
|
|
|
|
# 图表区域
|
|
|
chartGroup = QGroupBox("趋势图表")
|
|
|
chartGroup.setObjectName("trendChartGroup")
|
|
|
chartLayout = QVBoxLayout(chartGroup)
|
|
|
chartLayout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
# 创建matplotlib图表
|
|
|
self.figure = Figure(figsize=(12, 8), dpi=100)
|
|
|
gs = GridSpec(7, 1, figure=self.figure)
|
|
|
self.axMain = self.figure.add_subplot(gs[:6, 0]) # 主图,占6份
|
|
|
self.axOverview = self.figure.add_subplot(gs[6, 0]) # 缩略图,占1份
|
|
|
self.canvas = FigureCanvas(self.figure)
|
|
|
chartLayout.addWidget(self.canvas)
|
|
|
|
|
|
layout.addWidget(chartGroup, 20)
|
|
|
|
|
|
# 信息状态栏
|
|
|
infoGroup = QGroupBox("状态信息")
|
|
|
infoGroup.setObjectName("trendInfoGroup")
|
|
|
infoLayout = QHBoxLayout(infoGroup)
|
|
|
infoLayout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
self.varNameLabel = QLabel("当前变量: 无")
|
|
|
self.varNameLabel.setObjectName("trendVarNameLabel")
|
|
|
|
|
|
self.dataCountLabel = QLabel("数据点: 0")
|
|
|
self.dataCountLabel.setObjectName("trendDataCountLabel")
|
|
|
|
|
|
self.timeRangeLabel = QLabel("时间范围: 无数据")
|
|
|
self.timeRangeLabel.setObjectName("trendTimeRangeLabel")
|
|
|
|
|
|
self.statusLabel = QLabel("就绪")
|
|
|
self.statusLabel.setObjectName("trendStatusLabel")
|
|
|
|
|
|
infoLayout.addWidget(self.varNameLabel)
|
|
|
infoLayout.addStretch()
|
|
|
infoLayout.addWidget(self.dataCountLabel)
|
|
|
infoLayout.addStretch()
|
|
|
infoLayout.addWidget(self.timeRangeLabel)
|
|
|
infoLayout.addStretch()
|
|
|
infoLayout.addWidget(self.statusLabel)
|
|
|
|
|
|
layout.addWidget(infoGroup, 1)
|
|
|
|
|
|
# 初始化交互功能
|
|
|
self.initializeChartInteraction()
|
|
|
|
|
|
def onSelect(self, xmin, xmax):
|
|
|
"""SpanSelector回调,记录选中区间并更新主趋势图"""
|
|
|
self._selectedRange = (xmin, xmax)
|
|
|
self.updateMultiVarChart()
|
|
|
|
|
|
def initializeChartInteraction(self):
|
|
|
"""初始化图表交互功能"""
|
|
|
# 悬浮信息气泡
|
|
|
self.infoBubble = QLabel(self.trendViewerGroup)
|
|
|
self.infoBubble.setObjectName("trendInfoBubble")
|
|
|
self.infoBubble.setVisible(False)
|
|
|
|
|
|
# 连接鼠标事件
|
|
|
self.canvas.mpl_connect('motion_notify_event', self.onMouseMove)
|
|
|
self.canvas.mpl_connect('axes_leave_event', self.onMouseLeave)
|
|
|
self.canvas.mpl_connect('scroll_event', self.onMouseWheel)
|
|
|
self.canvas.mpl_connect('button_press_event', self.onMousePress)
|
|
|
self.canvas.mpl_connect('button_release_event', self.onMouseRelease)
|
|
|
|
|
|
# 拖拽状态
|
|
|
self._isPanning = False
|
|
|
self._panStartX = None
|
|
|
self._panStartXlim = None
|
|
|
|
|
|
# 添加SpanSelector用于区间选择
|
|
|
self.span = SpanSelector(
|
|
|
self.axOverview, self.onSelect, 'horizontal',
|
|
|
useblit=True, interactive=True, props=dict(alpha=0.3, facecolor='orange')
|
|
|
)
|
|
|
|
|
|
# 绑定双击事件恢复全局视图
|
|
|
self.canvas.mpl_connect('button_press_event', self.onOverviewDoubleClick)
|
|
|
self._selectedRange = None # 记录选中区间
|
|
|
|
|
|
# 设置缩略图x轴显示格式
|
|
|
import matplotlib.dates as mdates
|
|
|
self.axOverview.xaxis.set_major_locator(mdates.AutoDateLocator())
|
|
|
self.axOverview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
|
|
|
|
|
def connectSignals(self):
|
|
|
"""连接所有信号"""
|
|
|
self.axOverview.callbacks.connect('xlim_changed', self.onOverviewXlimChanged)
|
|
|
|
|
|
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._queryTimeRange = (
|
|
|
self.startTimeEdit.dateTime().toPyDateTime(),
|
|
|
self.endTimeEdit.dateTime().toPyDateTime()
|
|
|
)
|
|
|
|
|
|
# 重新加载所有已选变量的数据
|
|
|
for varName in self.selectedVars:
|
|
|
self.loadVarData(varName)
|
|
|
|
|
|
self.updateMultiVarChart()
|
|
|
startTime = self._queryTimeRange[0].strftime('%Y-%m-%d %H:%M:%S')
|
|
|
endTime = self._queryTimeRange[1].strftime('%Y-%m-%d %H:%M:%S')
|
|
|
self.statusLabel.setText(f"已设置时间范围: {startTime} ~ {endTime}")
|
|
|
|
|
|
def loadVarData(self, varName):
|
|
|
"""加载单个变量的数据"""
|
|
|
try:
|
|
|
self.statusLabel.setText(f"加载 {varName} 数据...")
|
|
|
|
|
|
# 获取时间范围
|
|
|
if hasattr(self, '_queryTimeRange') and self._queryTimeRange:
|
|
|
startTime, endTime = self._queryTimeRange
|
|
|
else:
|
|
|
startTime = None
|
|
|
endTime = None
|
|
|
|
|
|
# 从数据库获取数据
|
|
|
if self.historyDB and hasattr(self.historyDB, 'queryVarHistory'):
|
|
|
data, timeList = self.historyDB.queryVarHistory(varName, startTime, endTime)
|
|
|
xData = self.processTimeData(timeList)
|
|
|
|
|
|
# 存储数据
|
|
|
self.multiVarData[varName] = {'x': xData, 'y': data}
|
|
|
|
|
|
# 分配颜色
|
|
|
if varName not in self.varColors:
|
|
|
self.assignVarColor(varName)
|
|
|
|
|
|
except Exception as e:
|
|
|
self.statusLabel.setText(f"加载 {varName} 数据失败: {str(e)}")
|
|
|
|
|
|
def processTimeData(self, timeList):
|
|
|
"""处理时间数据"""
|
|
|
import datetime
|
|
|
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())
|
|
|
return xData
|
|
|
|
|
|
def assignVarColor(self, varName):
|
|
|
"""为变量分配颜色"""
|
|
|
colors = ['#2277EF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']
|
|
|
colorIndex = len(self.varColors) % len(colors)
|
|
|
self.varColors[varName] = colors[colorIndex]
|
|
|
|
|
|
def updateMultiVarChart(self):
|
|
|
"""更新多变量图表"""
|
|
|
import matplotlib.dates as mdates
|
|
|
from matplotlib.ticker import MaxNLocator
|
|
|
|
|
|
# 清空图表
|
|
|
self.axMain.clear()
|
|
|
self.axOverview.clear()
|
|
|
|
|
|
# 设置网格
|
|
|
self.axMain.grid(True, alpha=0.3)
|
|
|
self.axOverview.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']:
|
|
|
# 处理断线数据
|
|
|
xProcessed, yProcessed = self.processBreakLineData(data['x'], data['y'])
|
|
|
|
|
|
# 缩略图:显示全量数据
|
|
|
self.axOverview.plot(xProcessed, yProcessed,
|
|
|
color=self.varColors[varName],
|
|
|
linewidth=1, alpha=0.7)
|
|
|
|
|
|
# 主图:根据选中区间显示数据
|
|
|
xMain, yMain = self.filterDataByRange(xProcessed, yProcessed)
|
|
|
self.axMain.plot(xMain, yMain,
|
|
|
color=self.varColors[varName],
|
|
|
linewidth=2, marker='o', markersize=3,
|
|
|
label=varName)
|
|
|
|
|
|
# 设置格式和标签
|
|
|
self.setupChartFormat()
|
|
|
|
|
|
# 刷新画布
|
|
|
self.canvas.draw()
|
|
|
|
|
|
def processBreakLineData(self, xRaw, yRaw):
|
|
|
"""处理断线数据:间隔大于2分钟的点不连线"""
|
|
|
x, y = [], []
|
|
|
prevTime = None
|
|
|
|
|
|
for xi, yi in zip(xRaw, yRaw):
|
|
|
if prevTime is not None and (xi - prevTime).total_seconds() > 120:
|
|
|
# 断线时插入None
|
|
|
x.append(xi)
|
|
|
y.append(None)
|
|
|
x.append(xi)
|
|
|
y.append(yi)
|
|
|
prevTime = xi
|
|
|
|
|
|
return x, y
|
|
|
|
|
|
def filterDataByRange(self, xProcessed, yProcessed):
|
|
|
"""根据选中区间过滤数据"""
|
|
|
if self._selectedRange is not None:
|
|
|
import matplotlib.dates as mdates
|
|
|
xlim = self._selectedRange
|
|
|
xFiltered, yFiltered = [], []
|
|
|
|
|
|
for xx, yy in zip(xProcessed, yProcessed):
|
|
|
if xx is not None:
|
|
|
xn = mdates.date2num(xx)
|
|
|
if xlim[0] <= xn <= xlim[1]:
|
|
|
xFiltered.append(xx)
|
|
|
yFiltered.append(yy)
|
|
|
|
|
|
return xFiltered, yFiltered
|
|
|
else:
|
|
|
return xProcessed, yProcessed
|
|
|
|
|
|
def setupChartFormat(self):
|
|
|
"""设置图表格式"""
|
|
|
import matplotlib.dates as mdates
|
|
|
from matplotlib.ticker import MaxNLocator
|
|
|
|
|
|
# 设置时间格式
|
|
|
self.axMain.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
|
|
|
self.axMain.xaxis.set_major_locator(MaxNLocator(nbins=8))
|
|
|
self.axMain.yaxis.set_major_locator(MaxNLocator(nbins=8))
|
|
|
|
|
|
self.axOverview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
|
|
self.axOverview.xaxis.set_major_locator(MaxNLocator(nbins=6))
|
|
|
self.axOverview.yaxis.set_major_locator(MaxNLocator(nbins=4))
|
|
|
|
|
|
# 设置标签
|
|
|
self.axMain.set_title('历史趋势图', fontsize=14, fontweight='bold', color='#2277EF')
|
|
|
self.axMain.set_ylabel('数值', fontsize=12)
|
|
|
self.axOverview.set_xlabel('时间', fontsize=10)
|
|
|
self.axOverview.set_ylabel('数值', fontsize=10)
|
|
|
|
|
|
# 添加图例
|
|
|
if self.selectedVars:
|
|
|
self.axMain.legend(loc='upper right', fontsize=10)
|
|
|
|
|
|
# 自动调整布局
|
|
|
self.figure.tight_layout()
|
|
|
|
|
|
def processBreakLineDataOld(self, xRaw, yRaw):
|
|
|
"""处理断线数据:间隔大于2分钟的点不连线(旧版本兼容)"""
|
|
|
x, y = [], []
|
|
|
prevTime = None
|
|
|
for xi, yi in zip(xRaw, yRaw):
|
|
|
if prevTime is not None and (xi - prevTime).total_seconds() > 60:
|
|
|
# 断线时只在y中插入None,x继续插入当前时间戳
|
|
|
x.append(xi)
|
|
|
y.append(None)
|
|
|
x.append(xi)
|
|
|
y.append(yi)
|
|
|
prevTime = 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.axMain.clear()
|
|
|
self.axOverview.clear()
|
|
|
|
|
|
# 设置网格
|
|
|
self.axMain.grid(True, alpha=0.3)
|
|
|
self.axOverview.grid(True, alpha=0.3)
|
|
|
|
|
|
# 绘制数据
|
|
|
if self.xData and self.yData:
|
|
|
self.axMain.plot(self.xData, self.yData,
|
|
|
color='blue',
|
|
|
linewidth=2,
|
|
|
marker='o',
|
|
|
markersize=4,
|
|
|
label=self.currentVarName)
|
|
|
|
|
|
# 概览图(降采样)
|
|
|
step = max(1, len(self.xData) // 10)
|
|
|
xOverview = self.xData[::step]
|
|
|
yOverview = self.yData[::step]
|
|
|
self.axOverview.plot(xOverview, yOverview,
|
|
|
color='blue',
|
|
|
linewidth=1,
|
|
|
alpha=0.7)
|
|
|
|
|
|
# 设置时间格式
|
|
|
self.axMain.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
|
|
|
self.axOverview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
|
|
|
|
|
|
# 设置标签
|
|
|
self.axMain.set_title('历史趋势图', fontsize=12, fontweight='bold')
|
|
|
self.axMain.set_ylabel('数值')
|
|
|
self.axOverview.set_xlabel('时间')
|
|
|
self.axOverview.set_ylabel('数值')
|
|
|
|
|
|
# 添加图例
|
|
|
if self.currentVarName:
|
|
|
self.axMain.legend(loc='upper right')
|
|
|
|
|
|
# 自动调整布局
|
|
|
self.figure.tight_layout()
|
|
|
|
|
|
# 刷新画布
|
|
|
self.canvas.draw()
|
|
|
|
|
|
def onMousePress(self, event):
|
|
|
"""鼠标按下事件"""
|
|
|
if event.inaxes == self.axMain and event.button == 1 and event.xdata is not None:
|
|
|
self._isPanning = True
|
|
|
self._panStartX = event.xdata
|
|
|
self._panStartXlim = self.axMain.get_xlim()
|
|
|
|
|
|
def onMouseRelease(self, event):
|
|
|
"""鼠标释放事件"""
|
|
|
if self._isPanning:
|
|
|
self._isPanning = False
|
|
|
self._panStartX = None
|
|
|
self._panStartXlim = None
|
|
|
self.canvas.draw_idle()
|
|
|
|
|
|
def onMouseMove(self, event):
|
|
|
"""鼠标移动事件"""
|
|
|
# 处理拖拽平移
|
|
|
if self._isPanning and event.inaxes == self.axMain and event.xdata is not None and self._panStartX is not None:
|
|
|
dx = event.xdata - self._panStartX
|
|
|
xlim0, xlim1 = self._panStartXlim
|
|
|
self.axMain.set_xlim(xlim0 - dx, xlim1 - dx)
|
|
|
|
|
|
# 显示悬浮信息
|
|
|
if event.inaxes == self.axMain and event.xdata is not None and event.ydata is not None:
|
|
|
self.showInfoBubble(event)
|
|
|
else:
|
|
|
self.infoBubble.setVisible(False)
|
|
|
|
|
|
def onMouseLeave(self, event):
|
|
|
"""鼠标离开事件"""
|
|
|
self.infoBubble.setVisible(False)
|
|
|
|
|
|
def showInfoBubble(self, event):
|
|
|
"""显示信息气泡"""
|
|
|
infoText = 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']:
|
|
|
closestIdx = self.findClosestPoint(data['x'], event.xdata)
|
|
|
if closestIdx is not None and closestIdx < len(data['y']):
|
|
|
value = data['y'][closestIdx]
|
|
|
infoText += f"{varName}: {value:.3f}<br>"
|
|
|
|
|
|
self.infoBubble.setText(f"<span style='white-space:pre'>{infoText}</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)
|
|
|
|
|
|
def findClosestPoint(self, xData, targetX):
|
|
|
"""找到最接近目标X值的数据点索引"""
|
|
|
if not xData:
|
|
|
return None
|
|
|
|
|
|
# 将datetime转换为matplotlib日期
|
|
|
if isinstance(xData[0], datetime.datetime):
|
|
|
xNums = mdates.date2num(xData)
|
|
|
else:
|
|
|
xNums = xData
|
|
|
|
|
|
# 使用二分查找
|
|
|
left = 0
|
|
|
right = len(xNums) - 1
|
|
|
while left <= right:
|
|
|
mid = (left + right) // 2
|
|
|
if xNums[mid] == targetX:
|
|
|
return mid
|
|
|
elif xNums[mid] < targetX:
|
|
|
left = mid + 1
|
|
|
else:
|
|
|
right = mid - 1
|
|
|
|
|
|
# 找到最接近的点
|
|
|
if left >= len(xNums):
|
|
|
return len(xNums) - 1
|
|
|
elif right < 0:
|
|
|
return 0
|
|
|
else:
|
|
|
if abs(xNums[left] - targetX) < abs(xNums[right] - targetX):
|
|
|
return left
|
|
|
else:
|
|
|
return right
|
|
|
|
|
|
def onOverviewXlimChanged(self, eventAx):
|
|
|
"""当概览图范围变化时更新主图"""
|
|
|
if eventAx == self.axOverview:
|
|
|
x1, x2 = self.axOverview.get_xlim()
|
|
|
self.axMain.set_xlim(x1)
|
|
|
self.canvas.draw()
|
|
|
|
|
|
def resetZoom(self):
|
|
|
"""重置到完整视图"""
|
|
|
if self.xData:
|
|
|
self.axMain.set_xlim(min(self.xData), max(self.xData))
|
|
|
self.axOverview.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, zoomRange):
|
|
|
"""应用指定的缩放范围"""
|
|
|
x1, x2 = zoomRange
|
|
|
self.axMain.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.axMain.clear()
|
|
|
self.axOverview.clear()
|
|
|
self.axMain.grid(True, alpha=0.3)
|
|
|
self.axOverview.grid(True, alpha=0.3)
|
|
|
self.canvas.draw()
|
|
|
self.statusLabel.setText("趋势图已清除")
|
|
|
self.varNameLabel.setText("当前变量: 无")
|
|
|
self.dataCountLabel.setText("数据点: 0")
|
|
|
self.timeRangeLabel.setText("时间范围: 无数据")
|
|
|
self.resetZoom()
|
|
|
|
|
|
def onOverviewDoubleClick(self, event):
|
|
|
"""双击下方缩略图恢复全局视图"""
|
|
|
if event.dblclick and event.inaxes == self.axOverview:
|
|
|
self.axMain.set_xlim(auto=True)
|
|
|
self.canvas.draw_idle()
|
|
|
|
|
|
def onMouseWheel(self, event):
|
|
|
"""鼠标滚轮缩放主趋势图X轴"""
|
|
|
if event.inaxes == self.axMain and event.xdata is not None:
|
|
|
xlim = self.axMain.get_xlim()
|
|
|
xCenter = event.xdata
|
|
|
# 缩放因子,滚轮向上放大,向下缩小
|
|
|
scaleFactor = 0.8 if event.button == 'up' else 1.25
|
|
|
xLeft = xCenter - (xCenter - xlim[0]) * scaleFactor
|
|
|
xRight = xCenter + (xlim[1] - xCenter) * scaleFactor
|
|
|
self.axMain.set_xlim(xLeft, xRight)
|
|
|
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_()) |