|
|
|
|
@ -31,8 +31,8 @@ class TrendWidgets(QWidget):
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.setWindowTitle("历史趋势浏览器")
|
|
|
|
|
self.setMinimumSize(120, 120)
|
|
|
|
|
# 设置窗口标志,禁用窗口拖拽
|
|
|
|
|
self.setWindowFlags(QtCore.Qt.WindowType.Window)
|
|
|
|
|
self.setObjectName("trendMainWidget")
|
|
|
|
|
|
|
|
|
|
# 初始化数据
|
|
|
|
|
self.historyDB = None
|
|
|
|
|
self.currentVarName = None
|
|
|
|
|
@ -45,111 +45,178 @@ class TrendWidgets(QWidget):
|
|
|
|
|
# 初始化历史缩放记录
|
|
|
|
|
self.history = []
|
|
|
|
|
self.historyIndex = 0
|
|
|
|
|
# 主布局
|
|
|
|
|
|
|
|
|
|
self.initializeUI()
|
|
|
|
|
self.connectSignals()
|
|
|
|
|
Globals.setValue('HistoryWidget', self)
|
|
|
|
|
|
|
|
|
|
def initializeUI(self):
|
|
|
|
|
"""初始化用户界面"""
|
|
|
|
|
mainLayout = QHBoxLayout(self)
|
|
|
|
|
mainLayout.setSpacing(10)
|
|
|
|
|
mainLayout.setContentsMargins(10, 10, 10, 10)
|
|
|
|
|
# 左侧变量列表区域
|
|
|
|
|
mainLayout.setSpacing(8)
|
|
|
|
|
mainLayout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
|
|
|
|
# 左侧变量列表区域 (占25%宽度)
|
|
|
|
|
self.createVariableListPanel()
|
|
|
|
|
mainLayout.addWidget(self.variableListGroup, 1) # 1份宽度
|
|
|
|
|
# 右侧趋势图区域
|
|
|
|
|
mainLayout.addWidget(self.variableListGroup, 1)
|
|
|
|
|
|
|
|
|
|
# 右侧趋势图区域 (占75%宽度)
|
|
|
|
|
self.trendViewerGroup = QWidget()
|
|
|
|
|
self.trendViewerGroup.setObjectName("trendViewerGroup")
|
|
|
|
|
self.createTrendViewerPanel()
|
|
|
|
|
mainLayout.addWidget(self.trendViewerGroup, 6) # 6份宽度
|
|
|
|
|
# 连接信号
|
|
|
|
|
self.connectSignals()
|
|
|
|
|
Globals.setValue('HistoryWidget', self)
|
|
|
|
|
mainLayout.addWidget(self.trendViewerGroup, 10)
|
|
|
|
|
|
|
|
|
|
def createVariableListPanel(self):
|
|
|
|
|
# 创建变量列表面板
|
|
|
|
|
"""创建变量列表面板"""
|
|
|
|
|
self.variableListGroup = QGroupBox("变量列表")
|
|
|
|
|
self.variableListGroup.setObjectName("trendVariableListGroup")
|
|
|
|
|
layout = QVBoxLayout(self.variableListGroup)
|
|
|
|
|
# 搜索框单独一行
|
|
|
|
|
searchInputLayout = QHBoxLayout()
|
|
|
|
|
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)
|
|
|
|
|
self.searchInput.setMinimumWidth(220)
|
|
|
|
|
searchInputLayout.addWidget(self.searchInput)
|
|
|
|
|
layout.addLayout(searchInputLayout)
|
|
|
|
|
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)
|
|
|
|
|
# 时间选择+便捷查询区域
|
|
|
|
|
searchLayout = QHBoxLayout()
|
|
|
|
|
|
|
|
|
|
# 时间选择区域
|
|
|
|
|
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)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
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.setText("查询")
|
|
|
|
|
self.queryBtn.setObjectName("trendQueryBtn")
|
|
|
|
|
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)
|
|
|
|
|
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._query_time_range = (self.startTimeEdit.dateTime().toPyDateTime(), self.endTimeEdit.dateTime().toPyDateTime())
|
|
|
|
|
self._queryTimeRange = (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()
|
|
|
|
|
layout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
layout.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
# 图表区域
|
|
|
|
|
chartGroup = QGroupBox("趋势图表")
|
|
|
|
|
chartGroup.setObjectName("trendChartGroup")
|
|
|
|
|
chartLayout = QVBoxLayout(chartGroup)
|
|
|
|
|
chartLayout.setContentsMargins(5, 5, 5, 5)
|
|
|
|
|
# 创建matplotlib图表,设置主图和缩略图比例为6:1
|
|
|
|
|
chartLayout.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
|
|
|
|
# 创建matplotlib图表
|
|
|
|
|
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.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, 20)
|
|
|
|
|
# 底部信息栏(含状态显示)
|
|
|
|
|
infoLayout = QHBoxLayout()
|
|
|
|
|
infoLayout.setContentsMargins(5, 5, 5, 5)
|
|
|
|
|
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.setFont(QtGui.QFont("Arial", 9))
|
|
|
|
|
self.statusLabel.setStyleSheet("color: #555; padding: 2px;")
|
|
|
|
|
self.statusLabel.setObjectName("trendStatusLabel")
|
|
|
|
|
|
|
|
|
|
infoLayout.addWidget(self.varNameLabel)
|
|
|
|
|
infoLayout.addStretch()
|
|
|
|
|
infoLayout.addWidget(self.dataCountLabel)
|
|
|
|
|
@ -157,44 +224,54 @@ class TrendWidgets(QWidget):
|
|
|
|
|
infoLayout.addWidget(self.timeRangeLabel)
|
|
|
|
|
infoLayout.addStretch()
|
|
|
|
|
infoLayout.addWidget(self.statusLabel)
|
|
|
|
|
chartLayout.addLayout(infoLayout, 1)
|
|
|
|
|
layout.addWidget(chartGroup)
|
|
|
|
|
# 悬浮信息气泡label
|
|
|
|
|
|
|
|
|
|
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.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.setObjectName("trendInfoBubble")
|
|
|
|
|
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.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._is_panning = False
|
|
|
|
|
self._pan_start_x = None
|
|
|
|
|
self._pan_start_xlim = None
|
|
|
|
|
# 初始化选择器
|
|
|
|
|
self.selector = None
|
|
|
|
|
self.is_selecting = False
|
|
|
|
|
self.zoom_start = None
|
|
|
|
|
self._isPanning = False
|
|
|
|
|
self._panStartX = None
|
|
|
|
|
self._panStartXlim = None
|
|
|
|
|
|
|
|
|
|
# 添加SpanSelector用于区间选择
|
|
|
|
|
self.span = SpanSelector(
|
|
|
|
|
self.ax_overview, self.on_select, 'horizontal',
|
|
|
|
|
self.axOverview, self.onSelect, '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轴显示更多时间内容
|
|
|
|
|
self.canvas.mpl_connect('button_press_event', self.onOverviewDoubleClick)
|
|
|
|
|
self._selectedRange = 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'))
|
|
|
|
|
self.axOverview.xaxis.set_major_locator(mdates.AutoDateLocator())
|
|
|
|
|
self.axOverview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
|
|
|
|
|
|
|
|
|
|
def connectSignals(self):
|
|
|
|
|
# 连接所有信号
|
|
|
|
|
self.ax_overview.callbacks.connect('xlim_changed', self.on_overview_xlim_changed)
|
|
|
|
|
"""连接所有信号"""
|
|
|
|
|
self.axOverview.callbacks.connect('xlim_changed', self.onOverviewXlimChanged)
|
|
|
|
|
|
|
|
|
|
def exchangeProject(self):
|
|
|
|
|
# 项目切换时调用
|
|
|
|
|
@ -256,153 +333,184 @@ class TrendWidgets(QWidget):
|
|
|
|
|
self.statusLabel.setText("选中的变量已在趋势图中")
|
|
|
|
|
|
|
|
|
|
def onTimeRangeQuery(self):
|
|
|
|
|
# 查询按钮点击,记录时间范围
|
|
|
|
|
self._query_time_range = (
|
|
|
|
|
"""查询按钮点击,记录时间范围"""
|
|
|
|
|
self._queryTimeRange = (
|
|
|
|
|
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.loadVarData(varName)
|
|
|
|
|
|
|
|
|
|
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')}")
|
|
|
|
|
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} 数据...")
|
|
|
|
|
import datetime
|
|
|
|
|
if hasattr(self, '_query_time_range') and self._query_time_range:
|
|
|
|
|
start_time, end_time = self._query_time_range
|
|
|
|
|
|
|
|
|
|
# 获取时间范围
|
|
|
|
|
if hasattr(self, '_queryTimeRange') and self._queryTimeRange:
|
|
|
|
|
startTime, endTime = self._queryTimeRange
|
|
|
|
|
else:
|
|
|
|
|
start_time = None
|
|
|
|
|
end_time = None
|
|
|
|
|
startTime = None
|
|
|
|
|
endTime = 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]
|
|
|
|
|
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
|
|
|
|
|
# 更新多变量图表
|
|
|
|
|
self.ax_main.clear()
|
|
|
|
|
self.ax_overview.clear()
|
|
|
|
|
from matplotlib.ticker import MaxNLocator
|
|
|
|
|
|
|
|
|
|
# 清空图表
|
|
|
|
|
self.axMain.clear()
|
|
|
|
|
self.axOverview.clear()
|
|
|
|
|
|
|
|
|
|
# 设置网格
|
|
|
|
|
self.ax_main.grid(True, alpha=0.3)
|
|
|
|
|
self.ax_overview.grid(True, alpha=0.3)
|
|
|
|
|
# 复用数据处理逻辑,提升性能
|
|
|
|
|
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']:
|
|
|
|
|
# 处理断线数据(复用逻辑)
|
|
|
|
|
x_processed, y_processed = self._process_break_line_data(data['x'], data['y'])
|
|
|
|
|
# 处理断线数据
|
|
|
|
|
xProcessed, yProcessed = self.processBreakLineData(data['x'], data['y'])
|
|
|
|
|
|
|
|
|
|
# 缩略图:显示全量数据
|
|
|
|
|
self.ax_overview.plot(x_processed, y_processed, color=self.varColors[varName], linewidth=1, alpha=0.7)
|
|
|
|
|
self.axOverview.plot(xProcessed, yProcessed,
|
|
|
|
|
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'))
|
|
|
|
|
# 增加主图和缩略图的坐标轴刻度数量,并显示完整日期时间
|
|
|
|
|
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.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.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.ax_main.set_title('历史趋势图', fontsize=12, fontweight='bold')
|
|
|
|
|
self.ax_main.set_ylabel('数值')
|
|
|
|
|
self.ax_overview.set_xlabel('时间')
|
|
|
|
|
self.ax_overview.set_ylabel('数值')
|
|
|
|
|
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.ax_main.legend(loc='upper right')
|
|
|
|
|
self.axMain.legend(loc='upper right', fontsize=10)
|
|
|
|
|
|
|
|
|
|
# 自动调整布局
|
|
|
|
|
self.figure.tight_layout()
|
|
|
|
|
# 刷新画布
|
|
|
|
|
self.canvas.draw()
|
|
|
|
|
|
|
|
|
|
def _process_break_line_data(self, x_raw, y_raw):
|
|
|
|
|
"""处理断线数据:间隔大于2分钟的点不连线"""
|
|
|
|
|
def processBreakLineDataOld(self, xRaw, yRaw):
|
|
|
|
|
"""处理断线数据:间隔大于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:
|
|
|
|
|
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)
|
|
|
|
|
prev_time = xi
|
|
|
|
|
prevTime = xi
|
|
|
|
|
return x, y
|
|
|
|
|
|
|
|
|
|
def filterVarList(self, text):
|
|
|
|
|
@ -460,17 +568,17 @@ class TrendWidgets(QWidget):
|
|
|
|
|
self.statusLabel.setText(f"{varName} 趋势已显示")
|
|
|
|
|
|
|
|
|
|
def updateChart(self):
|
|
|
|
|
# 用当前数据更新图表
|
|
|
|
|
self.ax_main.clear()
|
|
|
|
|
self.ax_overview.clear()
|
|
|
|
|
"""用当前数据更新图表"""
|
|
|
|
|
self.axMain.clear()
|
|
|
|
|
self.axOverview.clear()
|
|
|
|
|
|
|
|
|
|
# 设置网格
|
|
|
|
|
self.ax_main.grid(True, alpha=0.3)
|
|
|
|
|
self.ax_overview.grid(True, alpha=0.3)
|
|
|
|
|
self.axMain.grid(True, alpha=0.3)
|
|
|
|
|
self.axOverview.grid(True, alpha=0.3)
|
|
|
|
|
|
|
|
|
|
# 绘制数据
|
|
|
|
|
if self.xData and self.yData:
|
|
|
|
|
self.ax_main.plot(self.xData, self.yData,
|
|
|
|
|
self.axMain.plot(self.xData, self.yData,
|
|
|
|
|
color='blue',
|
|
|
|
|
linewidth=2,
|
|
|
|
|
marker='o',
|
|
|
|
|
@ -479,26 +587,26 @@ class TrendWidgets(QWidget):
|
|
|
|
|
|
|
|
|
|
# 概览图(降采样)
|
|
|
|
|
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,
|
|
|
|
|
xOverview = self.xData[::step]
|
|
|
|
|
yOverview = self.yData[::step]
|
|
|
|
|
self.axOverview.plot(xOverview, yOverview,
|
|
|
|
|
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.axMain.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
|
|
|
|
|
self.axOverview.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('数值')
|
|
|
|
|
self.axMain.set_title('历史趋势图', fontsize=12, fontweight='bold')
|
|
|
|
|
self.axMain.set_ylabel('数值')
|
|
|
|
|
self.axOverview.set_xlabel('时间')
|
|
|
|
|
self.axOverview.set_ylabel('数值')
|
|
|
|
|
|
|
|
|
|
# 添加图例
|
|
|
|
|
if self.currentVarName:
|
|
|
|
|
self.ax_main.legend(loc='upper right')
|
|
|
|
|
self.axMain.legend(loc='upper right')
|
|
|
|
|
|
|
|
|
|
# 自动调整布局
|
|
|
|
|
self.figure.tight_layout()
|
|
|
|
|
@ -506,101 +614,106 @@ class TrendWidgets(QWidget):
|
|
|
|
|
# 刷新画布
|
|
|
|
|
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 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 on_mouse_release(self, event):
|
|
|
|
|
# 鼠标左键松开,结束平移
|
|
|
|
|
if self._is_panning:
|
|
|
|
|
self._is_panning = False
|
|
|
|
|
self._pan_start_x = None
|
|
|
|
|
self._pan_start_xlim = None
|
|
|
|
|
# 拖拽结束后再重绘
|
|
|
|
|
def onMouseRelease(self, event):
|
|
|
|
|
"""鼠标释放事件"""
|
|
|
|
|
if self._isPanning:
|
|
|
|
|
self._isPanning = False
|
|
|
|
|
self._panStartX = None
|
|
|
|
|
self._panStartXlim = 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)
|
|
|
|
|
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)
|
|
|
|
|
# self.canvas.draw() # 性能优化:去除重绘
|
|
|
|
|
|
|
|
|
|
def on_mouse_leave(self, event):
|
|
|
|
|
# 鼠标离开主图时隐藏气泡
|
|
|
|
|
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 find_closest_point(self, x_data, target_x):
|
|
|
|
|
# 找到最接近目标X值的数据点索引
|
|
|
|
|
if not x_data:
|
|
|
|
|
def findClosestPoint(self, xData, targetX):
|
|
|
|
|
"""找到最接近目标X值的数据点索引"""
|
|
|
|
|
if not xData:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# 将datetime转换为matplotlib日期
|
|
|
|
|
if isinstance(x_data[0], datetime.datetime):
|
|
|
|
|
x_nums = mdates.date2num(x_data)
|
|
|
|
|
if isinstance(xData[0], datetime.datetime):
|
|
|
|
|
xNums = mdates.date2num(xData)
|
|
|
|
|
else:
|
|
|
|
|
x_nums = x_data
|
|
|
|
|
xNums = xData
|
|
|
|
|
|
|
|
|
|
# 使用二分查找
|
|
|
|
|
left = 0
|
|
|
|
|
right = len(x_nums) - 1
|
|
|
|
|
right = len(xNums) - 1
|
|
|
|
|
while left <= right:
|
|
|
|
|
mid = (left + right) // 2
|
|
|
|
|
if x_nums[mid] == target_x:
|
|
|
|
|
if xNums[mid] == targetX:
|
|
|
|
|
return mid
|
|
|
|
|
elif x_nums[mid] < target_x:
|
|
|
|
|
elif xNums[mid] < targetX:
|
|
|
|
|
left = mid + 1
|
|
|
|
|
else:
|
|
|
|
|
right = mid - 1
|
|
|
|
|
|
|
|
|
|
# 找到最接近的点
|
|
|
|
|
if left >= len(x_nums):
|
|
|
|
|
return len(x_nums) - 1
|
|
|
|
|
if left >= len(xNums):
|
|
|
|
|
return len(xNums) - 1
|
|
|
|
|
elif right < 0:
|
|
|
|
|
return 0
|
|
|
|
|
else:
|
|
|
|
|
if abs(x_nums[left] - target_x) < abs(x_nums[right] - target_x):
|
|
|
|
|
if abs(xNums[left] - targetX) < abs(xNums[right] - targetX):
|
|
|
|
|
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)
|
|
|
|
|
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.ax_main.set_xlim(min(self.xData), max(self.xData))
|
|
|
|
|
self.ax_overview.set_xlim(min(self.xData), max(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):
|
|
|
|
|
@ -618,10 +731,10 @@ class TrendWidgets(QWidget):
|
|
|
|
|
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)
|
|
|
|
|
def applyZoom(self, zoomRange):
|
|
|
|
|
"""应用指定的缩放范围"""
|
|
|
|
|
x1, x2 = zoomRange
|
|
|
|
|
self.axMain.set_xlim(x1, x2)
|
|
|
|
|
self.canvas.draw()
|
|
|
|
|
|
|
|
|
|
def enableRectZoom(self):
|
|
|
|
|
@ -635,14 +748,14 @@ class TrendWidgets(QWidget):
|
|
|
|
|
# 项目更改时处理
|
|
|
|
|
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.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("当前变量: 无")
|
|
|
|
|
@ -650,27 +763,22 @@ class TrendWidgets(QWidget):
|
|
|
|
|
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)
|
|
|
|
|
def onOverviewDoubleClick(self, event):
|
|
|
|
|
"""双击下方缩略图恢复全局视图"""
|
|
|
|
|
if event.dblclick and event.inaxes == self.axOverview:
|
|
|
|
|
self.axMain.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
|
|
|
|
|
def onMouseWheel(self, event):
|
|
|
|
|
"""鼠标滚轮缩放主趋势图X轴"""
|
|
|
|
|
if event.inaxes == self.axMain and event.xdata is not None:
|
|
|
|
|
xlim = self.axMain.get_xlim()
|
|
|
|
|
xCenter = 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)
|
|
|
|
|
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):
|
|
|
|
|
|