"""Debounced viewport statistics shared by desktop charts.""" import time from dataclasses import replace try: from PySide6.QtCore import QObject, QTimer, QThreadPool, Qt except ImportError: from PySide2.QtCore import QObject, QTimer, QThreadPool, Qt from .plot_workers import NavigationSignals as _NavigationSignals, NavigationJob as _NavigationJob from set_devices.oscilloscope import time_label def number(value): return f"{value:g}" class ViewportNavigation(QObject): def __init__(self, chart): super().__init__(chart) self.chart = chart self._navigation_revision = 0 self._navigation_busy = False self._navigation_emitter = _NavigationSignals(self) self._navigation_emitter.ready.connect(self._navigation_ready, Qt.ConnectionType.QueuedConnection) self._navigation_timer = QTimer(self) self._navigation_timer.setSingleShot(True) self._navigation_timer.setInterval(180) self._navigation_timer.timeout.connect(self._navigation_finished) def _navigation_changed(self): self._navigation_revision += 1 if max((len(s.values) for s in self.chart.plot._series), default=0) <= 20000: self.chart._scope_measurements() self.chart._refresh_processing() self.chart._refresh_legend() return self._navigation_timer.start() def _navigation_finished(self): if False: return if max((len(s.values) for s in self.chart.plot._series), default=0) <= 20000: self.chart._scope_measurements() self.chart._refresh_processing() return if self._navigation_busy: self._navigation_timer.start() return self._navigation_busy = True bounds = self.chart.plot.visible_bounds panel = self.chart.processing offset = self.chart._plot_time_offset if not self.chart.fft.isChecked() else 0. # Navigation changes the next calculation's domain; retained curves keep # their original domain. Avoid preparing millions of inputs on the UI. panel._snapshot = replace(panel._snapshot, x_range=(bounds.left + offset, bounds.right + offset)) series = [(s.key, s.timestamps, s.values) for s in self.chart.plot._series if not s.discrete] QThreadPool.globalInstance().start(_NavigationJob(self._navigation_revision, series, bounds, self._navigation_emitter)) def _navigation_ready(self, revision, stats, bounds): self._navigation_busy = False if False or revision != self._navigation_revision: return for row in range(self.chart.legend.rowCount()): item = self.chart.legend.item(row, 0) if item is None: continue result, _ = stats.get(item.data(Qt.ItemDataRole.UserRole), (None, 0)) for column, key in ((4, "rms"), (5, "maximum"), (6, "minimum")): cell = self.chart.legend.item(row, column) if cell is not None: cell.setText("—" if result is None else "%g" % result[key]) scope = self.chart.scope result, count = stats.get(scope.channel.currentData(), (None, 0)) if result is None: scope.readings.setText("Измерения: —") return text = " · ".join(f"{title} {number(result[key])}" for title, key in ( ("Min", "minimum"), ("Max", "maximum"), ("Размах", "peak_to_peak"), ("Среднее", "mean"), ("RMS", "rms"))) if not self.chart.fft.isChecked(): frequency = result["frequency"] text += " · f ≈ " + (f"{number(frequency)} Гц" if frequency is not None else "—") text += f" · {time_label((bounds.right - bounds.left) / 10)}/дел" scope.readings.setText(text + f" · N={count}")