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 pyqtgraph import mkPen

from pymodaq_utils.config import GlobalConfig
from pymodaq_utils.logger import set_logger, get_module_name

import pyqtgraph as pg

from pymodaq_gui.utils.widgets import SpinBox
from pymodaq_utils import utils

from pymodaq_data import data as data_mod
from pymodaq_data.plotting.utils import PlotColors

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
from pymodaq_gui import foreground_color
from pymodaq_gui.utils.dock import Dock

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()]
config = GlobalConfig()


[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=None): super().__init__() if plot_colors is None: plot_colors = PLOT_COLORS self._plotitem = plotitem self.colors = plot_colors self._do_scatter = False self._do_xy = False self._plotitem.addLegend() self._plot_items: Dict[str, pg.PlotDataItem] = {} self._min_lines: Dict[str, pg.InfiniteLine] = {} self._max_lines: Dict[str, pg.InfiniteLine] = {} self._data = Data0DWithHistory() self.use_timestamps = False 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] width = color.pop('width', self.linewidth) plot_item = pyqtgraph.PlotDataItem(pen=mkPen(width=width, **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
[docs] def set_use_timestamps(self, use_timestamps: bool = False): self.use_timestamps = use_timestamps axis = self._plotitem.getAxis('bottom') if use_timestamps: axis.setLabel(text='Timestamps', units='s') else: axis.setLabel(text='Samples', units='S') self.update_plots()
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) @property def linewidth(self) -> int: return config('data', 'plotting', 'linewidth')
[docs] def update_colors(self, colors: List[dict]): self.colors[0:len(colors)] = colors symbol_size = 5 symbol = 'o' for label, color_idx in self._color_indices.items(): color = self.colors[color_idx] width = color.pop('width', self.linewidth) if self._do_scatter: pen = None symbol_type = symbol brush = color['color'] else: pen = mkPen(width=width, **color) symbol_type = None brush = None self._plot_items[label].setPen(pen) self._plot_items[label].setSymbolBrush(brush) self._plot_items[label].setSymbol(symbol_type) self._plot_items[label].setSymbolSize(symbol_size) dash_pen = pg.mkPen(color=color['color'], style=Qt.PenStyle.DashLine) self._max_lines[label].setPen(dash_pen) self._min_lines[label].setPen(dash_pen) self.update_plots()
[docs] def update_scatter(self, do_scatter=False): self._do_scatter = do_scatter self.update_colors(self.colors)
[docs] def update_xyplot(self, do_xy=True): self._do_xy = do_xy self.update_plots() labels = list(self._data.data.keys()) plot_items = [self._plot_items[label] for label in labels] xaxis = self._plotitem.getAxis('bottom') yaxis = self._plotitem.getAxis('left') if do_xy and len(labels) >= 2: plot_items[0].setVisible(False) xaxis.setLabel(text=labels[0], units='') else: plot_items[0].setVisible(True) self.set_use_timestamps(self.use_timestamps)
@property def legend(self) -> pg.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): if self.use_timestamps: return self._data.timestamps else: 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_data(data) self.update_plots()
[docs] def update_plots(self): if self._do_xy and len(self._data.data) >= 2: labels = list(self._data.data.keys()) plot_items = [self._plot_items[label] for label in labels] data_list = [self._data.data[label] for label in labels] for ind in range(1, len(data_list)): plot_items[ind].setData(data_list[0], data_list[ind]) else: for label, plot_item in self._plot_items.items(): if label in self._data.data: plot_item.setData(self.axis, self._data.data[label]) for label, values in self._data.data.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, title=''): QObject.__init__(self) ActionManager.__init__(self, toolbar=QtWidgets.QToolBar()) self._title = title 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]) @property def title(self) -> str: return self._title @title.setter def title(self, value: str): self._title = value
[docs] def setup_actions(self): self.add_action('clear', 'Clear plot', 'ink_eraser', 'Clear the current plots') self.add_widget('Nhistory', SpinBox, tip='Set the history length of the plot', setters=dict(setMaximumWidth=100)) self.add_action('show_data_as_list', 'Show numbers', 'pin', 'If triggered, will display last data as numbers' 'in a side panel', checkable=True) self.add_action('show_min_max', 'Show Min/Max lines', 'contrast_square', 'If triggered, will display horizontal dashed lines for min/max of data', checkable=True) self.add_action('use_timestamps', 'Use Timestamps', 'timer_off', 'Use timestamps as axis', checkable=True, icon_checked='timer') self.add_action('scatter', 'Scatter', 'scatter_plot', 'Switch between line or scatter plots', checkable=True) self.add_action('xyplot', 'XYPlotting', 'function', 'Switch between normal or XY representation (valid for 2 channels)', checkable=True, visible=False) 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) self.connect_action('use_timestamps', self.data_displayer.set_use_timestamps) self.connect_action('use_timestamps', self.set_x_axis_type) self.connect_action('scatter', self.data_displayer.update_scatter) self.connect_action('xyplot', self.data_displayer.update_xyplot) self.connect_action('xyplot', self.set_x_axis_type)
[docs] def set_x_axis_type(self): if self.is_action_checked('use_timestamps') and not self.is_action_checked('xyplot'): self.plot_widget.plotItem.setAxisItems({'bottom': pg.DateAxisItem()}) else: self.plot_widget.plotItem.setAxisItems({'bottom': pg.AxisItem('bottom')})
def _prepare_ui(self): """add here everything needed at startup""" self.values_list.setVisible(False) self.get_action('Nhistory').setValue(config('gui', 'viewer', 'viewer0D', 'Nhistory')) for action_name in ('show_data_as_list', 'show_min_max'): if config('gui', 'viewer', 'viewer0D', action_name): self.get_action(action_name).trigger() if not config('gui', 'viewer', 'viewer0D', 'sync_x_axis'): self.get_action('sync_x_axis').trigger()
[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): self.set_action_visible('xyplot', len(data) >= 2) 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, rois_dock: Dock = None): super().__init__(parent, title) self.view = View0D(self.parent, show_toolbar=show_toolbar, no_margins=no_margins, title=title) 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() prog.get_action('use_timestamps').trigger() for ind, data in enumerate(y1): prog.show_data(data_mod.DataRaw('mydata', data=[np.array([data]), np.array([y2[ind]]), -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()