gotelem/py/pytelem/widgets/smart_display.py

146 lines
5.2 KiB
Python
Raw Normal View History

2023-09-19 19:17:22 +00:00
# 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 = []