You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

714 lines
31 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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中插入Nonex继续插入当前时间戳
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_())