添加历史数据光标

main
zcwBit 3 months ago
parent afa66d5c92
commit ec61dcbf67

@ -256,9 +256,17 @@ class TrendWidgets(QWidget):
self.infoBubble.setObjectName("trendInfoBubble")
self.infoBubble.setVisible(False)
# 初始化十字标线
self.crosshair_v = None # 垂直线
self.crosshair_h = None # 水平线
self.crosshair_visible = False
self.crosshair_point = None # 交叉点圆点
self.crosshair_text = None # 坐标文本标签
# 连接鼠标事件
self.canvas.mpl_connect('motion_notify_event', self.onMouseMove)
self.canvas.mpl_connect('axes_leave_event', self.onMouseLeave)
self.canvas.mpl_connect('axes_enter_event', self.onMouseEnter)
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)
@ -422,6 +430,9 @@ class TrendWidgets(QWidget):
self.axMain.clear()
self.axOverview.clear()
# 重置十字标线引用因为clear()会删除所有线条)
self._cleanupCrosshair()
# 设置网格
self.axMain.grid(True, alpha=0.3)
self.axOverview.grid(True, alpha=0.3)
@ -651,14 +662,23 @@ class TrendWidgets(QWidget):
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.updateCrosshair(event.xdata, event.ydata)
self.showInfoBubble(event)
else:
self.hideCrosshair()
self.infoBubble.setVisible(False)
def onMouseEnter(self, event):
"""鼠标进入事件"""
if event.inaxes == self.axMain:
self.showCrosshair()
def onMouseLeave(self, event):
"""鼠标离开事件"""
if event.inaxes == self.axMain:
self.hideCrosshair()
self.infoBubble.setVisible(False)
def showInfoBubble(self, event):
@ -716,6 +736,307 @@ class TrendWidgets(QWidget):
else:
return right
def createCrosshair(self):
"""创建十字标线 - 使用安全的方式,不影响已有数据线"""
# 只有在所有组件都不存在时才创建
if (self.crosshair_v is None or self.crosshair_h is None or
self.crosshair_point is None or self.crosshair_text is None):
# 先检查是否有数据线存在,如果没有则不创建十字标线
existing_lines = self.axMain.get_lines()
data_lines = []
for line in existing_lines:
# 检查是否是十字标线(通过颜色判断)
try:
color = line.get_color()
if color != '#FF4444' and color != '#ff4444':
data_lines.append(line)
except:
# 如果获取颜色失败,假设是数据线
data_lines.append(line)
if len(data_lines) == 0:
# 没有数据线时不创建十字标线
print("没有数据线,跳过十字标线创建")
return
try:
print(f"开始创建十字标线,发现{len(data_lines)}条数据线")
# 获取当前坐标轴范围,确保在有效范围内创建
xlim = self.axMain.get_xlim()
ylim = self.axMain.get_ylim()
# 使用Line2D对象而不是axvline/axhline更安全
from matplotlib.lines import Line2D
# 创建垂直线
self.crosshair_v = Line2D(
[xlim[0], xlim[0]], # 初始X坐标
[ylim[0], ylim[1]], # Y坐标范围
color='#FF4444',
linestyle='-',
linewidth=1.2,
alpha=0.8,
zorder=100, # 使用更高的zorder
visible=False
)
self.axMain.add_line(self.crosshair_v)
# 创建水平线
self.crosshair_h = Line2D(
[xlim[0], xlim[1]], # X坐标范围
[ylim[0], ylim[0]], # 初始Y坐标
color='#FF4444',
linestyle='-',
linewidth=1.2,
alpha=0.8,
zorder=100, # 使用更高的zorder
visible=False
)
self.axMain.add_line(self.crosshair_h)
# 创建交叉点圆点 - 使用scatter而不是plot
self.crosshair_point = self.axMain.scatter(
[], [], # 空的初始数据
s=36, # 大小 (6^2)
c='#FF4444', # 颜色
marker='o', # 圆形标记
edgecolors='white', # 边框颜色
linewidths=1.5, # 边框宽度
zorder=101, # 更高的层级
alpha=0.9
)
self.crosshair_point.set_visible(False)
# 创建坐标文本标签
self.crosshair_text = self.axMain.text(
xlim[0], ylim[0], # 初始位置设在坐标轴范围内
'', # 初始文本为空
fontsize=9,
color='#333333',
bbox=dict(
boxstyle='round,pad=0.3',
facecolor='white',
edgecolor='#FF4444',
alpha=0.9
),
zorder=102, # 最高层级
visible=False,
ha='left',
va='bottom'
)
print("✅ 十字标线组件创建完成")
except Exception as e:
# 如果创建失败,清理并重置引用
self._cleanupCrosshair()
print(f"❌ 创建十字标线失败: {e}")
def _cleanupCrosshair(self):
"""清理十字标线组件"""
try:
if self.crosshair_v is not None:
self.crosshair_v.remove()
if self.crosshair_h is not None:
self.crosshair_h.remove()
if self.crosshair_point is not None:
self.crosshair_point.remove()
if self.crosshair_text is not None:
self.crosshair_text.remove()
except:
pass
finally:
self.crosshair_v = None
self.crosshair_h = None
self.crosshair_point = None
self.crosshair_text = None
def updateCrosshair(self, x, y):
"""更新十字标线位置 - 安全版本"""
if not self.crosshair_visible:
return
# 确保十字标线已创建
self.createCrosshair()
# 如果十字标线组件不存在,直接返回
if (self.crosshair_v is None or self.crosshair_h is None or
self.crosshair_point is None or self.crosshair_text is None):
print("十字标线组件不存在,无法更新")
return
try:
# 获取当前坐标轴范围
xlim = self.axMain.get_xlim()
ylim = self.axMain.get_ylim()
# 确保坐标在有效范围内
if not (xlim[0] <= x <= xlim[1] and ylim[0] <= y <= ylim[1]):
return
# 可选:吸附到最近的数据点
snap_x, snap_y = self.snapToNearestDataPoint(x, y)
if snap_x is not None and snap_y is not None:
x, y = snap_x, snap_y
# 更新垂直线位置
self.crosshair_v.set_data([x, x], [ylim[0], ylim[1]])
self.crosshair_v.set_visible(True)
# 更新水平线位置
self.crosshair_h.set_data([xlim[0], xlim[1]], [y, y])
self.crosshair_h.set_visible(True)
# 更新交叉点圆点位置
self.crosshair_point.set_offsets([[x, y]])
self.crosshair_point.set_visible(True)
# 更新坐标文本
coord_text = self.formatCoordinateText(x, y)
self.crosshair_text.set_text(coord_text)
# 计算文本位置(避免超出图表边界)
text_x, text_y = self.calculateTextPosition(x, y, xlim, ylim)
self.crosshair_text.set_position((text_x, text_y))
self.crosshair_text.set_visible(True)
# 强制重绘以显示十字标线
self.canvas.draw_idle()
except Exception as e:
# 如果更新失败,静默处理
print(f"更新十字标线失败: {e}")
pass
def snapToNearestDataPoint(self, x, y):
"""吸附到最近的数据点(可选功能)"""
if not self.selectedVars:
return None, None
min_distance = float('inf')
snap_x, snap_y = None, None
# 遍历所有选中的变量,找到最近的数据点
for varName in self.selectedVars:
if varName in self.multiVarData:
data = self.multiVarData[varName]
if data['x'] and data['y']:
# 找到最接近的X坐标点
closest_idx = self.findClosestPoint(data['x'], x)
if closest_idx is not None and closest_idx < len(data['y']):
data_x = data['x'][closest_idx]
data_y = data['y'][closest_idx]
# 将datetime转换为matplotlib数值
if isinstance(data_x, datetime.datetime):
data_x_num = mdates.date2num(data_x)
else:
data_x_num = data_x
# 计算距离(在屏幕坐标系中)
distance = abs(data_x_num - x)
# 只有距离足够近才吸附(避免过度吸附)
if distance < min_distance and distance < 0.01: # 可调整吸附阈值
min_distance = distance
snap_x = data_x_num
snap_y = data_y
return snap_x, snap_y
def _delayed_crosshair_draw(self):
"""延迟绘制十字标线,避免频繁更新"""
try:
# 只绘制十字标线相关的元素
if (self.crosshair_v is not None and self.crosshair_v.get_visible()):
self.canvas.draw_idle()
except Exception as e:
# 静默处理异常
pass
def formatCoordinateText(self, x, y):
"""格式化坐标文本显示"""
try:
# 格式化时间X轴
if isinstance(x, (int, float)):
# 将matplotlib数值转换为datetime
time_obj = mdates.num2date(x)
time_str = time_obj.strftime('%H:%M:%S')
else:
time_str = str(x)
# 格式化数值Y轴
if isinstance(y, (int, float)):
if abs(y) >= 1000:
value_str = f"{y:.1f}"
elif abs(y) >= 100:
value_str = f"{y:.2f}"
else:
value_str = f"{y:.3f}"
else:
value_str = str(y)
return f"时间: {time_str}\n数值: {value_str}"
except Exception as e:
return f"X: {x:.3f}\nY: {y:.3f}"
def calculateTextPosition(self, x, y, xlim, ylim):
"""计算文本标签的最佳位置,避免超出边界"""
try:
# 计算相对位置
x_range = xlim[1] - xlim[0]
y_range = ylim[1] - ylim[0]
# 默认偏移量(相对于图表大小)
x_offset = x_range * 0.02 # X轴偏移2%
y_offset = y_range * 0.03 # Y轴偏移3%
# 计算初始位置
text_x = x + x_offset
text_y = y + y_offset
# 检查是否超出右边界
if text_x > xlim[1] - x_range * 0.15: # 预留15%空间给文本
text_x = x - x_offset * 3 # 移到左侧
# 检查是否超出上边界
if text_y > ylim[1] - y_range * 0.1: # 预留10%空间给文本
text_y = y - y_offset * 2 # 移到下方
# 确保不超出左边界和下边界
text_x = max(text_x, xlim[0] + x_range * 0.01)
text_y = max(text_y, ylim[0] + y_range * 0.01)
return text_x, text_y
except Exception as e:
# 如果计算失败,返回简单偏移
return x + 0.01, y + 0.01
def showCrosshair(self):
"""显示十字标线"""
self.crosshair_visible = True
self.createCrosshair()
def hideCrosshair(self):
"""隐藏十字标线"""
self.crosshair_visible = False
if self.crosshair_v is not None:
self.crosshair_v.set_visible(False)
if self.crosshair_h is not None:
self.crosshair_h.set_visible(False)
if self.crosshair_point is not None:
self.crosshair_point.set_visible(False)
if self.crosshair_text is not None:
self.crosshair_text.set_visible(False)
# 强制重绘以隐藏十字标线
self.canvas.draw_idle()
def onOverviewXlimChanged(self, eventAx):
"""当概览图范围变化时更新主图"""
if eventAx == self.axOverview:
@ -768,6 +1089,10 @@ class TrendWidgets(QWidget):
self.varColors = {}
self.axMain.clear()
self.axOverview.clear()
# 重置十字标线引用
self._cleanupCrosshair()
self.axMain.grid(True, alpha=0.3)
self.axOverview.grid(True, alpha=0.3)
self.canvas.draw()

@ -124,8 +124,26 @@ class HistoryDBManage:
# print(f"查询结果不是DataFrame: {type(df)}")
data, timeList = [], []
except Exception as e:
print(f"查询结果处理失败: {e}")
data, timeList = [], []
print(f"查询历史数据失败: {e}")
# 检查是否是Parquet文件问题
if "parquet" in str(e).lower() and "not found" in str(e).lower():
print(f"变量 '{varName}' 的历史数据文件损坏或丢失")
print("尝试查询最近的数据...")
# 尝试查询最近24小时的数据
try:
recent_sql = f'SELECT tz(time, \'Asia/Shanghai\') AS time, value FROM "{self.table}" WHERE {where} AND time >= now() - interval \'24 hours\' ORDER BY time DESC LIMIT 1000'
df = self.client.query(recent_sql, mode="pandas")
if isinstance(df, pd.DataFrame):
data = df["value"].tolist() if "value" in df.columns else []
timeList = df["time"].tolist() if "time" in df.columns else []
print(f"查询到最近 {len(data)} 个数据点")
else:
data, timeList = [], []
except Exception as e2:
print(f"查询最近数据也失败: {e2}")
data, timeList = [], []
else:
data, timeList = [], []
return data, timeList
def getAllVarNames(self):
@ -140,6 +158,14 @@ class HistoryDBManage:
return []
except Exception as e:
print(f"获取变量名失败: {e}")
# 如果是Parquet文件缺失错误尝试清理损坏的数据
if "parquet" in str(e).lower() and "not found" in str(e).lower():
print("检测到Parquet文件缺失可能需要清理InfluxDB数据")
print("建议解决方案:")
print("1. 重启InfluxDB服务")
print("2. 检查磁盘空间")
print("3. 清理损坏的数据文件")
print("4. 重建数据库索引")
return []
@classmethod

Loading…
Cancel
Save