Source code for twinpy.ui.custom_widgets
"""Qt widgets that are not directly children of TcWidget.
Use this for more general custom widgets.
"""
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QWidget,
QLabel,
QVBoxLayout,
QScrollArea,
)
from pyqtgraph import PlotWidget, PlotDataItem, mkPen, intColor
import numpy as np
import time
from typing import List, Optional
class ScrollLabel(QScrollArea):
"""Scrollable label."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWidgetResizable(True)
content = QWidget(self)
self.setWidget(content)
layout = QVBoxLayout(content)
self.label = QLabel("NaN")
self.label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.label.setWordWrap(True)
self.label.setTextFormat(Qt.RichText) # Allow HTML-ish formatting
self.label.setTextInteractionFlags(Qt.TextSelectableByMouse)
layout.addWidget(self.label)
def setText(self, text): # noqa: N802 # pylint: disable=invalid-name
self.label.setText(text)
[docs]class GraphWidget(QWidget):
"""Class to make an rolling plot of some data.
When data comes in faster than FS, the plot won't be refreshed. The data will
still be stored and show up when it's time.
:cvar FPS: Maximum refresh rate (Hz), faster samples are buffered but not yet
plotted.
"""
FPS = 30.0 # Maximum refresh rate
def __init__(
self,
labels: List[str],
units: Optional[List[str]] = None,
buffer_size: int = 100,
values_in_legend: bool = True,
*args, **kwargs,
):
"""
:param labels: List of strings corresponding to plotted variable names
:param units: List of display units for each variable (default: None)
:param buffer_size: Number of points to keep in graph
:param values_in_legend: When `True` (default), put the last values in the
legend
"""
super().__init__(*args, **kwargs)
self.labels = labels
self.buffer_size = buffer_size
self.units = units
self.values_in_legend = values_in_legend
self.num_signals = len(self.labels)
if self.units is not None:
if len(self.units) > self.num_signals:
raise ValueError("Number of units does not match the number of signals")
if len(self.units) < self.num_signals:
self.units.extend([""] * (self.num_signals - len(self.units)))
# All buffered data (column 0 is x-axis data) (row per sample)
self.data = np.empty([self.buffer_size, self.num_signals + 1]) * np.nan
# Create uninitialized matrix and place NaN values
self.start_time: float = time.time()
self.last_update: float = 0.0 # Keep track of last screen refresh
# Make plot stuff:
self.plot_widget = PlotWidget()
layout = QVBoxLayout(self)
layout.addWidget(self.plot_widget)
self.plot_item = self.plot_widget.getPlotItem()
self.legend = self.plot_item.addLegend()
self.legend.setBrush('k')
self.curves: List[PlotDataItem] = []
for i, name in enumerate(self.labels):
pen = mkPen(color=intColor(i), width=2)
curve = self.plot_item.plot(name=name, pen=pen)
self.curves.append(curve)
for i, label in enumerate(self.labels):
self.legend.items[i][1].setText(self.labels[i])
[docs] def add_data(self, y_list: List[float], x: Optional[float] = None):
"""Add new datapoint to the rolling graph.
:param y_list: List of new y-values
:param x: New x-value (default: use system time instead)
"""
if len(y_list) != self.num_signals:
raise ValueError("Size of `y_list` does not match the number of signals!")
if x is None:
x = time.time() - self.start_time
# Roll data matrix
self.data = np.roll(self.data, -1, axis=0) # Roll all rows up by one
self.data[-1, :] = [x] + y_list
if time.time() - self.last_update > 1.0 / self.FPS:
self.update_plot()
[docs] def update_plot(self):
"""Refresh the plot based on `self.data`."""
for i, curve in enumerate(self.curves):
curve.setData(x=self.data[:, 0], y=self.data[:, 1 + i])
if self.values_in_legend:
value = self.data[-1, 1 + i]
entry = "{}: {:.3f}".format(self.labels[i], value)
if self.units is not None:
entry += " [{}]".format(self.units[i])
self.legend.items[i][1].setText(entry)
self.last_update = time.time()