diff --git a/Static/Main.qss b/Static/Main.qss
index 2017598..83b1999 100644
--- a/Static/Main.qss
+++ b/Static/Main.qss
@@ -909,6 +909,200 @@ QListView#trendListView::item{
}
+/* 历史趋势界面样式 */
+QWidget#trendMainWidget {
+ background-color: #F5F5F5;
+ border-radius: 8px;
+}
+
+QGroupBox#trendVariableListGroup {
+ font: bold 16px "PingFangSC-Medium";
+ color: #2277EF;
+ border: 2px solid #E0E0E0;
+ border-radius: 8px;
+ margin-top: 12px;
+ padding-top: 8px;
+ background-color: #FFFFFF;
+}
+
+QGroupBox#trendVariableListGroup::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 8px 0 8px;
+ background-color: #FFFFFF;
+}
+
+QGroupBox#trendTimeGroupBox, QGroupBox#trendButtonGroupBox {
+ font: bold 14px "PingFangSC-Medium";
+ color: #555555;
+ border: 1px solid #D0D0D0;
+ border-radius: 6px;
+ margin-top: 10px;
+ padding-top: 6px;
+ background-color: #FAFAFA;
+}
+
+QGroupBox#trendTimeGroupBox::title, QGroupBox#trendButtonGroupBox::title {
+ subcontrol-origin: margin;
+ left: 8px;
+ padding: 0 6px 0 6px;
+ background-color: #FAFAFA;
+}
+
+QLineEdit#trendSearchInput {
+ border: 2px solid #E0E0E0;
+ border-radius: 6px;
+ padding: 8px 12px;
+ font: 14px "PingFangSC-Regular";
+ background-color: #FFFFFF;
+ color: #333333;
+}
+
+QLineEdit#trendSearchInput:focus {
+ border-color: #2277EF;
+ background-color: #F8F9FF;
+}
+
+QListWidget#trendVarListWidget {
+ border: 1px solid #E0E0E0;
+ border-radius: 6px;
+ background-color: #FFFFFF;
+ font: 14px "PingFangSC-Regular";
+ selection-background-color: #E3F2FD;
+ selection-color: #1976D2;
+}
+
+QListWidget#trendVarListWidget::item {
+ padding: 8px 12px;
+ border-bottom: 1px solid #F0F0F0;
+}
+
+QListWidget#trendVarListWidget::item:hover {
+ background-color: #F5F5F5;
+}
+
+QListWidget#trendVarListWidget::item:selected {
+ background-color: #E3F2FD;
+ color: #1976D2;
+}
+
+QComboBox#trendQuickRangeCombo {
+ border: 1px solid #D0D0D0;
+ border-radius: 4px;
+ padding: 6px 8px;
+ font: 13px "PingFangSC-Regular";
+ background-color: #FFFFFF;
+ min-width: 120px;
+}
+
+QComboBox#trendQuickRangeCombo::drop-down {
+ subcontrol-origin: padding;
+ subcontrol-position: top right;
+ width: 20px;
+ border: none;
+}
+
+QComboBox#trendQuickRangeCombo::down-arrow {
+ image: url(./Static/down.png);
+ width: 12px;
+ height: 12px;
+}
+
+QDateTimeEdit#trendStartTimeEdit, QDateTimeEdit#trendEndTimeEdit {
+ border: 1px solid #D0D0D0;
+ border-radius: 4px;
+ padding: 6px 8px;
+ font: 13px "PingFangSC-Regular";
+ background-color: #FFFFFF;
+}
+
+QDateTimeEdit#trendStartTimeEdit:focus, QDateTimeEdit#trendEndTimeEdit:focus {
+ border-color: #2277EF;
+}
+
+QToolButton#trendQueryBtn, QToolButton#trendRefreshBtn,
+QToolButton#trendAddBtn, QToolButton#trendClearBtn {
+ border: 2px solid #2277EF;
+ border-radius: 6px;
+ padding: 8px 16px;
+ font: bold 14px "PingFangSC-Medium";
+ color: #2277EF;
+ background-color: #FFFFFF;
+ min-height: 32px;
+}
+
+QToolButton#trendQueryBtn:hover, QToolButton#trendRefreshBtn:hover,
+QToolButton#trendAddBtn:hover, QToolButton#trendClearBtn:hover {
+ background-color: #2277EF;
+ color: #FFFFFF;
+}
+
+QToolButton#trendQueryBtn:pressed, QToolButton#trendRefreshBtn:pressed,
+QToolButton#trendAddBtn:pressed, QToolButton#trendClearBtn:pressed {
+ background-color: #1A5FCC;
+ border-color: #1A5FCC;
+}
+
+QWidget#trendViewerGroup {
+ background-color: #FFFFFF;
+ border-radius: 8px;
+}
+
+QGroupBox#trendChartGroup {
+ font: bold 16px "PingFangSC-Medium";
+ color: #2277EF;
+ border: 2px solid #E0E0E0;
+ border-radius: 8px;
+ margin-top: 12px;
+ padding-top: 8px;
+ background-color: #FFFFFF;
+}
+
+QGroupBox#trendChartGroup::title {
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 8px 0 8px;
+ background-color: #FFFFFF;
+}
+
+QGroupBox#trendInfoGroup {
+ font: bold 14px "PingFangSC-Medium";
+ color: #555555;
+ border: 1px solid #E0E0E0;
+ border-radius: 6px;
+ margin-top: 8px;
+ padding-top: 6px;
+ background-color: #F8F9FA;
+}
+
+QGroupBox#trendInfoGroup::title {
+ subcontrol-origin: margin;
+ left: 8px;
+ padding: 0 6px 0 6px;
+ background-color: #F8F9FA;
+}
+
+QLabel#trendVarNameLabel, QLabel#trendDataCountLabel,
+QLabel#trendTimeRangeLabel, QLabel#trendStatusLabel {
+ font: 13px "PingFangSC-Regular";
+ color: #666666;
+ padding: 4px 8px;
+}
+
+QLabel#trendStatusLabel {
+ color: #2277EF;
+ font-weight: bold;
+}
+
+QLabel#trendInfoBubble {
+ background: rgba(255, 255, 255, 0.95);
+ border: 2px solid #2277EF;
+ border-radius: 8px;
+ padding: 8px 12px;
+ font: 12px "Consolas", "Monaco", monospace;
+ color: #333333;
+}
+
QMessageBox {
background-color: #ffffff;
border-radius: 16px;
diff --git a/UI/TrendManage/TrendWidget.py b/UI/TrendManage/TrendWidget.py
index eb36462..9bdcdc3 100644
--- a/UI/TrendManage/TrendWidget.py
+++ b/UI/TrendManage/TrendWidget.py
@@ -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')}
"
- 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}
"
- self.infoBubble.setText(f"{info_text}")
- 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')}
"
+
+ 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}
"
+
+ self.infoBubble.setText(f"{infoText}")
+ 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):