plot tab: fix y-axis scaling, add Visible/Used Sats modes, right-click menu
- Fix _nice_ticks: use floor for first tick and ceil for last tick so the tick range always fully brackets the data; data was previously clipped when the maximum fell between two nice tick values - Add 5% headroom padding so data does not sit flush against plot edges - Add 'Visible Sats' and 'Used Sats' y-axis modes (global, like pDOP) - Right-click context menu on plot canvas: Save as PNG and Copy to clipboard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+34
-7
@@ -34,7 +34,9 @@ from PyQt5.QtCore import Qt, QPointF, QRectF, QThread, pyqtSignal
|
||||
|
||||
from data_model import ReceiverManager
|
||||
|
||||
_Y_OPTIONS = ['C/N₀', 'Azimuth', 'Elevation', 'pDOP']
|
||||
_Y_OPTIONS = ['C/N₀', 'Azimuth', 'Elevation', 'pDOP', 'Visible Sats', 'Used Sats']
|
||||
|
||||
_GLOBAL_MODES = {'pDOP', 'Visible Sats', 'Used Sats'}
|
||||
_COLORS = ['#2196F3', '#F44336', '#4CAF50', '#FF9800',
|
||||
'#9C27B0', '#00BCD4', '#FFEB3B', '#795548']
|
||||
|
||||
@@ -61,9 +63,10 @@ def _nice_ticks(lo: float, hi: float, n: int = 5) -> list:
|
||||
if base * m >= raw:
|
||||
step = base * m
|
||||
break
|
||||
first = math.ceil(lo / step) * step
|
||||
first = math.floor(lo / step) * step # ≤ lo — data never clipped at bottom
|
||||
last = math.ceil(hi / step) * step # ≥ hi — data never clipped at top
|
||||
ticks, v = [], first
|
||||
while v <= hi + step * 0.01:
|
||||
while v <= last + step * 0.01:
|
||||
ticks.append(v)
|
||||
v += step
|
||||
return ticks
|
||||
@@ -148,7 +151,9 @@ class PlotRenderThread(QThread):
|
||||
if ts_max - ts_min < 1:
|
||||
ts_min = ts_max - 1.0
|
||||
|
||||
v_ticks = _nice_ticks(v_min, v_max)
|
||||
# Add 5 % headroom so data never sits right on the plot edge
|
||||
pad = (v_max - v_min) * 0.05 if v_max > v_min else 0.5
|
||||
v_ticks = _nice_ticks(v_min - pad, v_max + pad)
|
||||
v_min, v_max = v_ticks[0], v_ticks[-1]
|
||||
if v_max - v_min < 1e-9:
|
||||
v_max = v_min + 1.0
|
||||
@@ -268,6 +273,22 @@ class _Canvas(QWidget):
|
||||
super().resizeEvent(event)
|
||||
self.resized.emit(self.width(), self.height())
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
from PyQt5.QtWidgets import QMenu, QFileDialog
|
||||
if not self._image or self._image.isNull():
|
||||
return
|
||||
menu = QMenu(self)
|
||||
act_save = menu.addAction("Save as PNG…")
|
||||
act_copy = menu.addAction("Copy to clipboard")
|
||||
action = menu.exec_(self.mapToGlobal(event.pos()))
|
||||
if action == act_save:
|
||||
path, _ = QFileDialog.getSaveFileName(
|
||||
self, "Save plot", "", "PNG (*.png)")
|
||||
if path:
|
||||
self._image.save(path)
|
||||
elif action == act_copy:
|
||||
QApplication.clipboard().setImage(self._image)
|
||||
|
||||
|
||||
# ── Single configurable plot ──────────────────────────────────────────────────
|
||||
|
||||
@@ -360,7 +381,7 @@ class SinglePlotWidget(QWidget):
|
||||
self._y_cb.blockSignals(True)
|
||||
self._y_cb.setCurrentText(s['y_axis'])
|
||||
self._y_cb.blockSignals(False)
|
||||
self._val_cb.setEnabled(s['y_axis'] != 'pDOP')
|
||||
self._val_cb.setEnabled(s['y_axis'] not in _GLOBAL_MODES)
|
||||
if 'history' in s:
|
||||
self._hist_spin.blockSignals(True)
|
||||
self._hist_spin.setValue(s['history'])
|
||||
@@ -424,8 +445,7 @@ class SinglePlotWidget(QWidget):
|
||||
self._submit_render()
|
||||
|
||||
def _on_y_changed(self, _text: str):
|
||||
is_pdop = (_text == 'pDOP')
|
||||
self._val_cb.setEnabled(not is_pdop)
|
||||
self._val_cb.setEnabled(_text not in _GLOBAL_MODES)
|
||||
for rid in list(self._data):
|
||||
n = self._hist_spin.value()
|
||||
self._data[rid] = [deque(maxlen=n), deque(maxlen=n)]
|
||||
@@ -470,6 +490,13 @@ class SinglePlotWidget(QWidget):
|
||||
if rid not in selected:
|
||||
return
|
||||
y_axis = self._y_cb.currentText()
|
||||
|
||||
if y_axis == 'Visible Sats':
|
||||
self._push_point(rid, time.time(), len(sats))
|
||||
return
|
||||
if y_axis == 'Used Sats':
|
||||
self._push_point(rid, time.time(), sum(1 for s in sats if s.used_in_fix))
|
||||
return
|
||||
if y_axis == 'pDOP':
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user