优化趋势界面显示效果

main
zcwBit 5 months ago
parent 51531b3a37
commit 38fc80f819

@ -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;

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

Loading…
Cancel
Save