P2: true multi-axis overlay + gauge view
gui/widgets.py: - MultiAxisPlot -- overlay with one Y axis PER UNIT (psi/V/rpm/C/%), linked ViewBoxes on X, so mixed-scale signals are readable at true values (base left axis + up to 4 right axes). - SinglePlot -- one shared axis for the Normalize (% of range) mode. - ArcGauge -- 270deg arc gauge with peak tick + numeric readout, own dark bg. - GaugeGrid -- scrollable grid of gauges. gui/main.py: - Graph page is now a multi-axis/single-axis sub-stack; Normalize toggles between true multi-axis (raw) and single-axis (%). curves map key->color; plot ops route to the active graph widget. - Gauge View menu enabled (3rd center page); gauges update on tick with peak. - Theme applies to both plot widgets; profile switch clears graphs/gauges. Fix: ArcGauge QPen built via setCapStyle (the QPen(...cap=...) kwarg segfaults PySide6). Validated headless: driving preset -> 6 unit groups across 5 axes, gauge view renders, normalize round-trips, profile-switch clears cleanly. obdcore + diagnostics tests still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
This commit is contained in:
+67
-31
@@ -18,6 +18,7 @@ import pyqtgraph as pg
|
||||
from obdcore import (list_profiles, profiles_dir, save_profile, load_profile,
|
||||
export_csv, replay_csv, TimeSeriesStore)
|
||||
from .controller import Controller
|
||||
from .widgets import MultiAxisPlot, SinglePlot, GaugeGrid
|
||||
|
||||
PLOT_WINDOW_S = 60.0
|
||||
REFRESH_MS = 100
|
||||
@@ -86,8 +87,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.view_table = self._act(viewm, "Table View", lambda: self._set_view(1),
|
||||
checkable=True)
|
||||
self.view_graph.setChecked(True)
|
||||
gauges = self._act(viewm, "Gauge View (P2)", lambda: None, checkable=True)
|
||||
gauges.setEnabled(False)
|
||||
self.view_gauge = self._act(viewm, "Gauge View", lambda: self._set_view(2),
|
||||
checkable=True)
|
||||
viewm.addSeparator()
|
||||
self.show_pids = self._act(viewm, "Show PID Panel", self._toggle_pid_dock,
|
||||
checkable=True)
|
||||
@@ -352,32 +353,43 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
def _build_center(self):
|
||||
self.stack = QtWidgets.QStackedWidget()
|
||||
|
||||
# graph page
|
||||
# graph page: a sub-stack of multi-axis (raw) and single-axis (normalized)
|
||||
gpage = QtWidgets.QWidget(); gl = QtWidgets.QVBoxLayout(gpage)
|
||||
gl.setContentsMargins(4, 4, 4, 4)
|
||||
bar = QtWidgets.QHBoxLayout()
|
||||
self.norm_chk = QtWidgets.QCheckBox("Normalize (% of range)")
|
||||
self.norm_chk.toggled.connect(lambda v: self.norm_act.setChecked(v))
|
||||
bar.addWidget(self.norm_chk); bar.addStretch(1)
|
||||
self.norm_chk.toggled.connect(self._set_normalize)
|
||||
bar.addWidget(self.norm_chk)
|
||||
bar.addWidget(QtWidgets.QLabel(" (off = true multi-axis, one Y scale per unit)"))
|
||||
bar.addStretch(1)
|
||||
bar.addWidget(QtWidgets.QLabel(f"window: {int(PLOT_WINDOW_S)}s"))
|
||||
gl.addLayout(bar)
|
||||
self.plot = pg.PlotWidget()
|
||||
self.plot.addLegend(offset=(10, 10))
|
||||
self.plot.showGrid(x=True, y=True, alpha=0.25)
|
||||
self.plot.setLabel("bottom", "time", units="s")
|
||||
gl.addWidget(self.plot)
|
||||
self.stack.addWidget(gpage)
|
||||
self.multi = MultiAxisPlot()
|
||||
self.single = SinglePlot()
|
||||
self.graph_stack = QtWidgets.QStackedWidget()
|
||||
self.graph_stack.addWidget(self.multi) # index 0 = multi-axis
|
||||
self.graph_stack.addWidget(self.single) # index 1 = normalized
|
||||
gl.addWidget(self.graph_stack)
|
||||
self.stack.addWidget(gpage) # center index 0 = graph
|
||||
|
||||
# table page
|
||||
# table page (center index 1)
|
||||
self.table = QtWidgets.QTableWidget(0, 6)
|
||||
self.table.setHorizontalHeaderLabels(["Signal", "Value", "Unit", "Min", "Max", "Conf"])
|
||||
self.table.horizontalHeader().setStretchLastSection(True)
|
||||
self.table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.stack.addWidget(self.table)
|
||||
|
||||
# gauge page (center index 2)
|
||||
self.gauges = GaugeGrid()
|
||||
self.stack.addWidget(self.gauges)
|
||||
|
||||
self.setCentralWidget(self.stack)
|
||||
self._apply_theme()
|
||||
|
||||
def _graph(self):
|
||||
"""The active graph widget (multi-axis unless Normalize is on)."""
|
||||
return self.single if self.norm_chk.isChecked() else self.multi
|
||||
|
||||
def _build_statusbar(self):
|
||||
self.status = self.statusBar()
|
||||
self.status.showMessage("Not connected. Pick a port (or Mock) and Connect.")
|
||||
@@ -404,6 +416,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
QtWidgets.QMessageBox.critical(self, "Profile load failed", str(e))
|
||||
return
|
||||
self.curves.clear(); self._color_i = 0
|
||||
self.multi.clear(); self.single.clear(); self.gauges.rebuild([])
|
||||
self._rebuild_for_profile()
|
||||
self._rebuild_profile_menu()
|
||||
self.status.showMessage(f"Loaded profile: {self.ctl.profile.name}")
|
||||
@@ -538,25 +551,42 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
def _add_curve(self, key):
|
||||
p = self.ctl.reg.get(key)
|
||||
color = CURVE_COLORS[self._color_i % len(CURVE_COLORS)]; self._color_i += 1
|
||||
self.curves[key] = self.plot.plot([], [], name=f"{p.name} ({p.unit})",
|
||||
pen=pg.mkPen(color=color, width=2))
|
||||
self.curves[key] = color # curves maps key -> color
|
||||
self._graph().add_curve(key, f"{p.name} ({p.unit})", p.unit, color)
|
||||
self._refresh_gauges()
|
||||
|
||||
def _remove_curve(self, key):
|
||||
c = self.curves.pop(key, None)
|
||||
if c is not None:
|
||||
self.plot.removeItem(c)
|
||||
leg = self.plot.plotItem.legend
|
||||
if leg:
|
||||
try:
|
||||
leg.removeItem(c)
|
||||
except Exception:
|
||||
pass
|
||||
if self.curves.pop(key, None) is None:
|
||||
return
|
||||
self.multi.remove_curve(key)
|
||||
self.single.remove_curve(key)
|
||||
self._refresh_gauges()
|
||||
|
||||
def _set_normalize(self, on):
|
||||
"""Swap between true multi-axis (raw) and single-axis (% of range)."""
|
||||
self.norm_act.setChecked(on)
|
||||
self.multi.clear(); self.single.clear()
|
||||
self.graph_stack.setCurrentIndex(1 if on else 0)
|
||||
active = self._graph()
|
||||
for key, color in self.curves.items():
|
||||
p = self.ctl.reg.get(key)
|
||||
active.add_curve(key, f"{p.name} ({p.unit})", p.unit, color)
|
||||
self._apply_theme()
|
||||
self._redraw_curves(static=not self.ctl.connected)
|
||||
|
||||
def _refresh_gauges(self):
|
||||
specs = []
|
||||
for key, color in self.curves.items():
|
||||
p = self.ctl.reg.get(key)
|
||||
specs.append((key, p.name, p.unit, p.vmin, p.vmax, color))
|
||||
self.gauges.rebuild(specs)
|
||||
|
||||
# ---------- view ----------
|
||||
def _set_view(self, idx):
|
||||
self.stack.setCurrentIndex(idx)
|
||||
self.view_graph.setChecked(idx == 0)
|
||||
self.view_table.setChecked(idx == 1)
|
||||
self.view_gauge.setChecked(idx == 2)
|
||||
|
||||
def _toggle_pid_dock(self):
|
||||
self.pid_dock.setVisible(self.show_pids.isChecked())
|
||||
@@ -569,10 +599,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self._apply_theme()
|
||||
|
||||
def _apply_theme(self):
|
||||
bg, fg = THEMES[self._theme]
|
||||
self.plot.setBackground(bg)
|
||||
self.plot.getAxis("bottom").setPen(fg); self.plot.getAxis("left").setPen(fg)
|
||||
self.plot.getAxis("bottom").setTextPen(fg); self.plot.getAxis("left").setTextPen(fg)
|
||||
bg, _fg = THEMES[self._theme]
|
||||
self.multi.set_background(bg)
|
||||
self.single.set_background(bg)
|
||||
|
||||
def _populate_table_rows(self):
|
||||
pids = self.ctl.reg.all()
|
||||
@@ -661,7 +690,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
else:
|
||||
since = (self.ctl.t0 or 0) + max(0.0, self.ctl.now() - PLOT_WINDOW_S)
|
||||
normalize = self.norm_chk.isChecked()
|
||||
for key, curve in self.curves.items():
|
||||
active = self._graph()
|
||||
for key in self.curves:
|
||||
p = self.ctl.reg.get(key)
|
||||
xs, ys = [], []
|
||||
for t, v in self.ctl.store.channel(key).series(since=since):
|
||||
@@ -672,8 +702,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
ys.append((v - p.vmin) / (p.vmax - p.vmin) * 100.0)
|
||||
else:
|
||||
ys.append(v)
|
||||
curve.setData(xs, ys)
|
||||
self.plot.setLabel("left", "% of range" if normalize else "value")
|
||||
active.set_data(key, xs, ys)
|
||||
if normalize:
|
||||
self.single.set_y_label("% of range")
|
||||
|
||||
def _tick(self):
|
||||
if not self.ctl.connected:
|
||||
@@ -691,7 +722,12 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.table.item(r, 3).setText("--" if lo is None else f"{lo:g}")
|
||||
self.table.item(r, 4).setText("--" if hi is None else f"{hi:g}")
|
||||
self.tree.blockSignals(False)
|
||||
self._redraw_curves()
|
||||
if self.stack.currentIndex() == 2: # gauge view
|
||||
for key in self.curves:
|
||||
lo, hi = self.ctl.store.minmax(key)
|
||||
self.gauges.set_value(key, self.ctl.store.latest(key), peak=hi)
|
||||
else:
|
||||
self._redraw_curves()
|
||||
|
||||
def closeEvent(self, ev):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user