Source code for pymodaq_gui.plotting.data_viewers.viewer0D

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

from pymodaq_utils import utils
from pymodaq_data import data as data_mod
from pymodaq_data.plotting.utils import PlotColors
from pymodaq_utils.logger import set_logger, get_module_name
from pymodaq_gui.plotting.data_viewers.viewer import ViewerBase
from pymodaq_gui.managers.action_manager import ActionManager
from pymodaq_gui.plotting.widgets import PlotWidget
from pymodaq_gui.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 PlotColors()]


[docs] 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: Dict[str, pyqtgraph.PlotDataItem] = {} self._min_lines: Dict[str, pyqtgraph.InfiniteLine] = {} self._max_lines: Dict[str, pyqtgraph.InfiniteLine] = {} self._data = Data0DWithHistory() self._mins: Dict[str, float] = {} self._maxs: Dict[str, float] = {} self._color_indices: Dict[str, int] = {} self._show_lines: bool = False axis = self._plotitem.getAxis('bottom') axis.setLabel(text='Samples', units='S') def _next_color_index(self) -> int: """Return the lowest color index not currently in use.""" used = set(self._color_indices.values()) for i in range(len(self.colors)): if i not in used: return i return len(self._plot_items) % len(self.colors) def _add_label(self, label: str, units: str): color_idx = self._next_color_index() self._color_indices[label] = color_idx color = self.colors[color_idx] plot_item = pyqtgraph.PlotDataItem(pen=color) self._plot_items[label] = plot_item self._plotitem.addItem(plot_item) self.legend.addItem(plot_item, f"{label} ({units})") dash_pen = pyqtgraph.mkPen(color=color['color'], style=Qt.PenStyle.DashLine) max_line = pyqtgraph.InfiniteLine(angle=0, pen=dash_pen) min_line = pyqtgraph.InfiniteLine(angle=0, pen=dash_pen) self._max_lines[label] = max_line self._min_lines[label] = min_line max_line.setVisible(self._show_lines) min_line.setVisible(self._show_lines) self._plotitem.addItem(max_line) self._plotitem.addItem(min_line)
[docs] def set_sync_x_axis(self, sync: bool): """When True (default), adding a new channel resets all histories so all curves start from the same x-index. When False, existing channels keep their history and the new channel is NaN-padded from the left.""" self._data.sync_x_axis = sync
def _remove_label(self, label: str): if label in self._plot_items: plot_item = self._plot_items.pop(label) self._plotitem.removeItem(plot_item) self.legend.removeItem(plot_item) if label in self._max_lines: self._plotitem.removeItem(self._max_lines.pop(label)) if label in self._min_lines: self._plotitem.removeItem(self._min_lines.pop(label)) self._color_indices.pop(label, None) self._mins.pop(label, None) self._maxs.pop(label, None)
[docs] def update_colors(self, colors: List[QtGui.QPen]): self.colors[0:len(colors)] = colors for label, color_idx in self._color_indices.items(): color = self.colors[color_idx] self._plot_items[label].setPen(color) dash_pen = pyqtgraph.mkPen(color=color['color'], style=Qt.PenStyle.DashLine) self._max_lines[label].setPen(dash_pen) self._min_lines[label].setPen(dash_pen)
@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
[docs] def clear_data(self): self._data.clear_data() self._mins = {} self._maxs = {}
[docs] 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
[docs] def update_data(self, data: data_mod.DataWithAxes, force_update=False): if data is not None: if set(data.labels) != set(self._plot_items.keys()) or force_update: self.update_display_items(data) self._data.add_datas(data) for label, plot_item in self._plot_items.items(): if label in self._data.datas: plot_item.setData(self._data.xaxis, self._data.datas[label]) for label, values in self._data.datas.items(): if label not in self._mins: self._mins[label] = float(np.nanmin(values)) self._maxs[label] = float(np.nanmax(values)) else: self._mins[label] = min(self._mins[label], float(np.nanmin(values))) self._maxs[label] = max(self._maxs[label], float(np.nanmax(values))) if label in self._min_lines: self._min_lines[label].setValue(self._mins[label]) self._max_lines[label].setValue(self._maxs[label])
[docs] def update_display_items(self, data: data_mod.DataWithAxes = None): new_labels = set(data.labels) if data is not None else set() current_labels = set(self._plot_items.keys()) for label in current_labels - new_labels: self._remove_label(label) if data is not None: for label in data.labels: if label not in self._plot_items: self._add_label(label, data.units) if new_labels != current_labels: self.updated_item.emit(list(self._plot_items.values())) self.labels_changed.emit(data.labels if data is not None else [])
[docs] def show_min_max(self, show=True): self._show_lines = show for line in self._max_lines.values(): line.setVisible(show) for line in self._min_lines.values(): line.setVisible(show)
[docs] 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
[docs] 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) self.add_action('sync_x_axis', 'Sync X axis', 'sync_disabled', 'If checked, adding a new channel resets all histories so curves ' 'share the same x-axis origin', checkable=True, checked=True, icon_checked='sync_lock', icon_color='#F9A825', icon_checked_color='#607D8B')
def _setup_widgets(self): self.splitter = QtWidgets.QSplitter(Qt.Orientation.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.Orientation.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) self.connect_action('sync_x_axis', self.data_displayer.set_sync_x_axis) def _prepare_ui(self): """add here everything needed at startup""" self.values_list.setVisible(False) self.get_action('sync_x_axis').setChecked(True)
[docs] def get_double_clicked(self): return self.plot_widget.view.sig_double_clicked
@property def plotitem(self): return self.plot_widget.plotItem
[docs] 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()
[docs] 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)
[docs] 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)
[docs] 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 = []
[docs] 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)
[docs] def main_view(): app = QtWidgets.QApplication(sys.argv) widget = QtWidgets.QWidget() prog = View0D(widget) widget.show() sys.exit(app.exec())
[docs] def main(): app = QtWidgets.QApplication(sys.argv) widget = QtWidgets.QWidget() prog = Viewer0D(widget, show_toolbar=False) from pymodaq_utils.math_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'], units="V")) QtWidgets.QApplication.processEvents() sys.exit(app.exec())
if __name__ == '__main__': # pragma: no cover #main_view() main()