@ -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 } \n Y: { 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 ( )