# A simple display for numbers with optional trend_data line, histogram, min/max, and rolling average. from PySide6.QtCore import Qt, Slot, QSize from PySide6.QtGui import QAction, QFontDatabase from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QLabel, QSizePolicy, QGridLayout ) import numpy as np import pyqtgraph as pg from typing import Optional, List class _StatsDisplay(QWidget): """Helper Widget for the stats display.""" def __init__(self, parent=None): super().__init__(parent) # create grid array, minimum size vertically. layout = QGridLayout(self) self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) @Slot(float, float, float) def update_values(self, new_min: float, new_avg: float, new_max: float): class SmartDisplay(QWidget): """A simple numeric display with optional statistics, trends, and histogram""" value: float = 0.0 min: float = -float("inf") max: float = float("inf") avg: float = 0.0 trend_data: List[float] = [] histogram_data: List[float] = [] # TODO: settable sample count for histogram/trend in right click menu def __init__(self, parent=None, title: str = None, initial_value: float = None, unit_suffix=None, show_histogram=False, show_trendline: bool = False, show_stats=False, histogram_samples=100, trend_samples=30): super().__init__(parent) self.trend_samples = trend_samples self.histogram_samples = histogram_samples layout = QVBoxLayout(self) if title is not None: self.title = title # create the title label self.title_widget = QLabel(title, self) self.title_widget.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.title_widget.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) layout.addWidget(self.title_widget) number_font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) number_font.setPointSize(18) self.value = initial_value self.suffix = unit_suffix or "" self.value_widget = QLabel(f"{self.value}{self.suffix}", self) self.value_widget.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.value_widget.setFont(number_font) layout.addWidget(self.value_widget) # histogram widget self.histogram_widget = pg.PlotWidget(self, title="Histogram") self.histogram_widget.enableAutoRange() self.histogram_widget.setVisible(False) self.histogram_graph = pg.PlotDataItem() self.histogram_widget.addItem(self.histogram_graph) layout.addWidget(self.histogram_widget) # stats display # trendline display self.trendline_widget = pg.PlotWidget(self, title="Trend") self.trendline_widget.enableAutoRange() self.trendline_widget.setVisible(False) self.trendline_data = pg.PlotDataItem() self.trendline_widget.addItem(self.trendline_data) layout.addWidget(self.trendline_widget) toggle_histogram = QAction("Show Histogram", self, checkable=True) toggle_histogram.toggled.connect(self._toggle_histogram) self.addAction(toggle_histogram) toggle_trendline = QAction("Show Trendline", self, checkable=True) toggle_trendline.toggled.connect(self._toggle_trendline) self.addAction(toggle_trendline) reset_stats = QAction("Reset Data", self) reset_stats.triggered.connect(self.reset_data) self.addAction(reset_stats) # use the QWidget Actions list as the right click context menu. This is inherited by children. self.setContextMenuPolicy(Qt.ActionsContextMenu) def _toggle_histogram(self): self.histogram_widget.setVisible(not self.histogram_widget.isVisible()) def _toggle_trendline(self): self.trendline_widget.setVisible(not self.trendline_widget.isVisible()) def _update_view(self): self.trendline_data.setData(self.trend_data) self.value_widget.setText(f"{self.value:4g}{self.suffix}") if self.histogram_widget.isVisible(): hist, bins = np.histogram(self.histogram_data) self.histogram_graph.setData(bins, hist, stepMode="center") @Slot(float) def update_value(self, value: float): """Update the value displayed and associated stats.""" self.value = value # update stats. if self.value > self.max: self.max = self.value if self.value < self.min: self.min = self.value # update trend_data data. self.trend_data.append(value) if len(self.trend_data) > self.trend_samples: self.trend_data.pop(0) # update histogram self.histogram_data.append(value) if len(self.histogram_data) > self.histogram_samples: self.histogram_data.pop(0) # update average # noinspection PyTypeChecker self.avg = np.cumsum(self.trend_data) / len(self.trend_data) # re-render data. self._update_view() @Slot() def reset_data(self): """Resets the existing data (trendline, stats, histogram)""" self.max = float("inf") self.min = -float("inf") self.trend_data = [] self.histogram_data = []