diff --git a/gui_client.py b/gui_client.py index c84cde1..0ae349a 100644 --- a/gui_client.py +++ b/gui_client.py @@ -390,6 +390,43 @@ class TimeAxisItem(pg.AxisItem): return [datetime.fromtimestamp(v).strftime("%H:%M:%S") for v in values] +class PlotTooltip(QWidget): + """Floating value readout that follows the crosshair cursor.""" + def __init__(self, parent: QWidget): + super().__init__(parent) + self.setAttribute(Qt.WA_TransparentForMouseEvents) + self.setStyleSheet( + "QWidget { background: rgba(18,18,30,215); border: 1px solid #444466;" + " border-radius: 3px; }" + "QLabel { color: #ddddee; font-size: 10px; border: none; }" + ) + lay = QVBoxLayout(self) + lay.setContentsMargins(6, 4, 6, 4) + lay.setSpacing(1) + self._labels: list[QLabel] = [] + self.hide() + + def set_values(self, entries: list): + if not entries: + self.hide() + return + while len(self._labels) < len(entries): + lbl = QLabel() + lbl.setTextFormat(Qt.RichText) + self.layout().addWidget(lbl) + self._labels.append(lbl) + for j, lbl in enumerate(self._labels): + if j < len(entries): + color, name, val = entries[j] + lbl.setText(f' {name}: {val}') + lbl.show() + else: + lbl.hide() + self.adjustSize() + self.show() + self.raise_() + + class PlotTab(QWidget): NUM_PLOTS = 8 MAX_TRACES = 4 @@ -428,6 +465,7 @@ class PlotTab(QWidget): self._add_combos: list[QComboBox] = [] self._chip_bars: list[QHBoxLayout] = [] self._date_labels: list[QLabel] = [] + self._tooltips: list[PlotTooltip] = [] for i in range(self.NUM_PLOTS): cell = QWidget() @@ -506,6 +544,7 @@ class PlotTab(QWidget): cell_vbox.addWidget(pw) self._plot_widgets.append(pw) + self._tooltips.append(PlotTooltip(pw)) plot_grid.addWidget(cell, i // 2, i % 2) scroll = QScrollArea() @@ -563,6 +602,8 @@ class PlotTab(QWidget): self._redraw_timer.stop() self._series.clear() self._units.clear() + for tt in self._tooltips: + tt.hide() self._redraw() def update(self, snapshot: dict): @@ -689,19 +730,45 @@ class PlotTab(QWidget): # ── cursor tracing ──────────────────────────────────────────────────────── + def _y_at(self, key: str, x: float): + series = self._series.get(key) + if not series: + return None + return min(series, key=lambda p: abs(p[0] - x))[1] + def _on_mouse_move(self, evt, plot_idx: int): pos = evt[0] pw = self._plot_widgets[plot_idx] vl = self._vlines[plot_idx] + tt = self._tooltips[plot_idx] if not pw.sceneBoundingRect().contains(pos): vl.setVisible(False) + tt.hide() return x = pw.getPlotItem().vb.mapSceneToView(pos).x() vl.setPos(x) vl.setVisible(True) + entries = [] + for j, key in enumerate(self._traces[plot_idx]): + y = self._y_at(key, x) + if y is None: + continue + unit = self._units.get(key, "") + val_str = (f"{y:g} {unit}").strip() + entries.append((self.COLORS[j], _trace_label(key), val_str)) + + tt.set_values(entries) + if entries: + wpos = pw.mapFromScene(pos) + tx = int(wpos.x()) + 14 + ty = int(wpos.y()) - tt.height() // 2 + tx = min(tx, pw.width() - tt.width() - 4) + ty = max(4, min(ty, pw.height() - tt.height() - 4)) + tt.move(tx, ty) + # ── Main window ───────────────────────────────────────────────────────────────