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.

822 lines
32 KiB
Python

7 months ago
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QSize, Qt, QTimer, QEvent
7 months ago
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
7 months ago
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
7 months ago
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
7 months ago
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)
3 months ago
# 变量列表
self.varListWidget = QListWidget()
self.varListWidget.setObjectName("trendVarListWidget")
3 months ago
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)
3 months ago
def exchangeProject(self):
# 项目切换时调用
3 months ago
self.historyDB = Globals.getValue('historyDBManage')
self.refreshVarList()
3 months ago
def refreshVarList(self):
# 刷新变量列表
3 months ago
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())
# 添加排序后的变量到列表
3 months ago
for name in varNames:
item = QListWidgetItem(name)
3 months ago
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("请先选择要添加的变量")
3 months ago
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()
3 months ago
)
# 重新加载所有已选变量的数据
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中插入Nonex继续插入当前时间戳
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_())