146 lines
5.2 KiB
Python
146 lines
5.2 KiB
Python
|
# 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 = []
|