from typing import List, Union, Dict
from numbers import Real
from qtpy import QtWidgets, QtGui
from qtpy.QtCore import QObject, Slot, Signal, Qt
import sys
import pyqtgraph
import pymodaq.utils.daq_utils as utils
from pymodaq.utils import data as data_mod
from pymodaq.utils.logger import set_logger, get_module_name
from pymodaq.utils.plotting.data_viewers.viewer import ViewerBase
from pymodaq.utils.managers.action_manager import ActionManager
from pymodaq.utils.plotting.widgets import PlotWidget
from pymodaq.utils.plotting.utils.plot_utils import Data0DWithHistory
import numpy as np
from collections import OrderedDict
import datetime
logger = set_logger(get_module_name(__file__))
PLOT_COLORS = [dict(color=color) for color in utils.plot_colors]
class DataDisplayer(QObject):
"""
This Object deals with the display of 0D data on a plotitem
"""
updated_item = Signal(list)
labels_changed = Signal(list)
def __init__(self, plotitem: pyqtgraph.PlotItem, plot_colors=PLOT_COLORS):
super().__init__()
self._plotitem = plotitem
self.colors = plot_colors
self._plotitem.addLegend()
self._plot_items: List[pyqtgraph.PlotDataItem] = []
self._min_lines: List[pyqtgraph.InfiniteLine] = []
self._max_lines: List[pyqtgraph.InfiniteLine] = []
self._data = Data0DWithHistory()
self._mins: List = []
self._maxs: List = []
self._show_lines: bool = False
axis = self._plotitem.getAxis('bottom')
axis.setLabel(text='Samples', units='S')
def update_colors(self, colors: List[QtGui.QPen]):
self.colors[0:len(colors)] = colors
self.update_data(self._data.last_data, force_update=True)
@property
def legend(self) -> pyqtgraph.LegendItem:
return self._plotitem.legend
@property
def legend_names(self) -> List[str]:
return [item[1].text for item in self.legend.items]
@property
def axis(self):
return self._data.xaxis
def clear_data(self):
self._data.clear_data()
self._mins = []
self._maxs = []
def update_axis(self, history_length: int):
self._data.length = history_length
@property
def Ndata(self):
return len(self._data.last_data) if self._data.last_data is not None else 0
def update_data(self, data: data_mod.DataWithAxes, force_update=False):
if data is not None:
if len(data) != len(self._plot_items) or force_update or data.labels != self.legend_names:
self.update_display_items(data)
self._data.add_datas(data)
for ind, data_str in enumerate(self._data.datas):
self._plot_items[ind].setData(self._data.xaxis, self._data.datas[data_str])
if len(self._mins) != len(self._data.datas):
self._mins = []
self._maxs = []
for ind, label in enumerate(self._data.datas):
if len(self._mins) != len(self._data.datas):
self._mins.append(float(np.min(self._data.datas[label])))
self._maxs.append(float(np.max(self._data.datas[label])))
else:
self._mins[ind] = min(self._mins[ind], float(np.min(self._data.datas[label])))
self._maxs[ind] = max(self._maxs[ind], float(np.max(self._data.datas[label])))
self._min_lines[ind].setValue(self._mins[ind])
self._max_lines[ind].setValue(self._maxs[ind])
def update_display_items(self, data: data_mod.DataWithAxes = None):
while len(self._plot_items) > 0:
plot_item = self._plotitem.removeItem(self._plot_items.pop(0))
self.legend.removeItem(plot_item)
self._plotitem.removeItem(self._max_lines.pop(0))
self._plotitem.removeItem(self._min_lines.pop(0))
if data is not None:
for ind in range(len(data)):
self._plot_items.append(pyqtgraph.PlotDataItem(pen=self.colors[ind]))
self._plotitem.addItem(self._plot_items[-1])
self.legend.addItem(self._plot_items[-1], data.labels[ind])
max_line = pyqtgraph.InfiniteLine(angle=0,
pen=pyqtgraph.mkPen(color=self.colors[ind]['color'],
style=Qt.DashLine))
min_line = pyqtgraph.InfiniteLine(angle=0,
pen=pyqtgraph.mkPen(color=self.colors[ind]['color'],
style=Qt.DashLine))
self._max_lines.append(max_line)
self._min_lines.append(min_line)
max_line.setVisible(self._show_lines)
min_line.setVisible(self._show_lines)
self._plotitem.addItem(self._max_lines[-1])
self._plotitem.addItem(self._min_lines[-1])
self.updated_item.emit(self._plot_items)
self.labels_changed.emit(data.labels)
def show_min_max(self, show=True):
self._show_lines = show
for line in self._max_lines:
line.setVisible(show)
for line in self._min_lines:
line.setVisible(show)
class View0D(ActionManager, QObject):
def __init__(self, parent_widget: QtWidgets.QWidget = None, show_toolbar=True,
no_margins=False):
QObject.__init__(self)
ActionManager.__init__(self, toolbar=QtWidgets.QToolBar())
self.no_margins = no_margins
self.data_displayer: DataDisplayer = None
self.other_data_displayers: Dict[str, DataDisplayer] = {}
self.plot_widget: PlotWidget = PlotWidget()
self.values_list = QtWidgets.QListWidget()
self.setup_actions()
self.parent_widget = parent_widget
if self.parent_widget is None:
self.parent_widget = QtWidgets.QWidget()
self.parent_widget.show()
self.data_displayer = DataDisplayer(self.plotitem)
self._setup_widgets()
self._connect_things()
self._prepare_ui()
if not show_toolbar:
self.splitter.setSizes([0,1])
self.get_action('Nhistory').setValue(200) #default history length
def setup_actions(self):
self.add_action('clear', 'Clear plot', 'clear2', 'Clear the current plots')
self.add_widget('Nhistory', pyqtgraph.SpinBox, tip='Set the history length of the plot',
setters=dict(setMaximumWidth=100))
self.add_action('show_data_as_list', 'Show numbers', 'ChnNum', 'If triggered, will display last data as numbers'
'in a side panel', checkable=True)
self.add_action('show_min_max', 'Show Min/Max lines', 'Statistics',
'If triggered, will display horizontal dashed lines for min/max of data', checkable=True)
def _setup_widgets(self):
self.splitter = QtWidgets.QSplitter(Qt.Vertical)
self.parent_widget.setLayout(QtWidgets.QVBoxLayout())
if self.no_margins:
self.parent_widget.layout().setContentsMargins(0, 0, 0, 0)
self.parent_widget.layout().addWidget(self.splitter)
self.splitter.addWidget(self.toolbar)
self.splitter.setStretchFactor(0, 0)
splitter_hor = QtWidgets.QSplitter(Qt.Horizontal)
self.splitter.addWidget(splitter_hor)
splitter_hor.addWidget(self.plot_widget)
splitter_hor.addWidget(self.values_list)
font = QtGui.QFont()
font.setPointSize(20)
self.values_list.setFont(font)
def _connect_things(self):
self.connect_action('clear', self.data_displayer.clear_data)
self.connect_action('show_data_as_list', self.show_data_list)
self.connect_action('Nhistory', self.data_displayer.update_axis, signal_name='valueChanged')
self.connect_action('show_min_max', self.data_displayer.show_min_max)
def _prepare_ui(self):
"""add here everything needed at startup"""
self.values_list.setVisible(False)
def get_double_clicked(self):
return self.plot_widget.view.sig_double_clicked
@property
def plotitem(self):
return self.plot_widget.plotItem
def display_data(self, data: data_mod.DataWithAxes, displayer: str = None, **kwargs):
if displayer is None:
self.data_displayer.update_data(data)
elif displayer in self.other_data_displayers:
self.other_data_displayers[displayer].update_data(data)
if self.is_action_checked('show_data_as_list'):
self.values_list.clear()
self.values_list.addItems(['{:.03e}'.format(dat[0]) for dat in data])
QtWidgets.QApplication.processEvents()
def show_data_list(self, state=None):
if state is None:
state = self.is_action_checked('show_data_as_list')
self.values_list.setVisible(state)
def add_data_displayer(self, displayer_name: str, plot_colors=PLOT_COLORS):
self.other_data_displayers[displayer_name] = DataDisplayer(self.plotitem, plot_colors)
self.connect_action('clear', self.other_data_displayers[displayer_name].clear_data)
def remove_data_displayer(self, displayer_name: str):
displayer = self.other_data_displayers.pop(displayer_name, None)
if displayer is not None:
displayer.update_display_items()
[docs]class Viewer0D(ViewerBase):
"""this plots 0D data on a plotwidget with history. Display as numbers in a table is possible.
Datas and measurements are then exported with the signal data_to_export_signal
"""
def __init__(self, parent=None, title='', show_toolbar=True, no_margins=False):
super().__init__(parent, title)
self.view = View0D(self.parent, show_toolbar=show_toolbar, no_margins=no_margins)
self._labels = []
def update_colors(self, colors: list, displayer=None):
if displayer is None:
self.view.data_displayer.update_colors(colors)
elif displayer in self.view.other_data_displayers:
self.view.other_data_displayers[displayer].update_colors(colors)
@property
def labels(self):
return self._labels
@labels.setter
def labels(self, labels):
if labels != self._labels:
self._labels = labels
@Slot(list)
def _show_data(self, data: data_mod.DataRaw):
self.labels = data.labels
self.view.display_data(data)
self.data_to_export_signal.emit(self.data_to_export)
def main_view():
app = QtWidgets.QApplication(sys.argv)
widget = QtWidgets.QWidget()
prog = View0D(widget)
widget.show()
sys.exit(app.exec_())
def main():
app = QtWidgets.QApplication(sys.argv)
widget = QtWidgets.QWidget()
prog = Viewer0D(widget, show_toolbar=False)
from pymodaq.utils.daq_utils import gauss1D
x = np.linspace(0, 200, 201)
y1 = gauss1D(x, 75, 25) + 0.1*np.random.rand(len(x))
y2 = 0.7 * gauss1D(x, 120, 50, 2) + 0.2*np.random.rand(len(x))
widget.show()
prog.get_action('show_data_as_list').trigger()
for ind, data in enumerate(y1):
prog.show_data(data_mod.DataRaw('mydata', data=[np.array([data]), np.array([y2[ind]])],
labels=['lab1', 'lab2']))
QtWidgets.QApplication.processEvents()
sys.exit(app.exec_())
if __name__ == '__main__': # pragma: no cover
#main_view()
main()