Plot tab: floating tooltip showing trace values at crosshair position

Add PlotTooltip — a frameless child widget that appears next to the
mouse cursor whenever it moves over a plot. It lists each active trace
with a colored bullet, short name, and the nearest y-value (unit
appended). The tooltip is repositioned on every mouse-move event and
clamped to stay within the plot widget bounds. Hidden on mouse-exit and
on clear_data (reconnect).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 16:42:32 +02:00
co-authored by Claude Sonnet 4.6
parent 5e2aa3f17d
commit 849473acc0
+67
View File
@@ -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'<span style="color:{color};">&#9679;</span> {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 ───────────────────────────────────────────────────────────────