From ec61dcbf67ee205e3dfd642efc2da5136a328ba8 Mon Sep 17 00:00:00 2001 From: zcwBit Date: Fri, 8 Aug 2025 19:24:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8E=86=E5=8F=B2=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=85=89=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UI/TrendManage/TrendWidget.py | 327 +++++++++++++++++++++++- model/HistoryDBModel/HistoryDBManage.py | 30 ++- 2 files changed, 354 insertions(+), 3 deletions(-) diff --git a/UI/TrendManage/TrendWidget.py b/UI/TrendManage/TrendWidget.py index 504cb78..2f79e64 100644 --- a/UI/TrendManage/TrendWidget.py +++ b/UI/TrendManage/TrendWidget.py @@ -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() diff --git a/model/HistoryDBModel/HistoryDBManage.py b/model/HistoryDBModel/HistoryDBManage.py index c1c3b8b..f99b12d 100644 --- a/model/HistoryDBModel/HistoryDBManage.py +++ b/model/HistoryDBModel/HistoryDBManage.py @@ -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