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:
@@ -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};">●</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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user