Source code for visiannot.tools.ToolsPyqtgraph

# -*- coding: utf-8 -*-
#
# Copyright Université Rennes 1 / INSERM
# Contributor: Raphael Weber
#
# Under CeCILL license
# http://www.cecill.info

"""
Module with sub-classes and functions for scientific graphics with pyqtgraph

See https://pyqtgraph.readthedocs.io/en/latest
"""


import pyqtgraph as pg
from PyQt5 import QtCore
from PyQt5.QtGui import QFont, QSizePolicy
from .ToolsPyQt import createWindow, addWidgetToLayout, initializeDisplay
import numpy as np


# *************************************************************************** #
# *************************************************************************** #
# ************************** Pyqtgraph subclass ***************************** #
# *************************************************************************** #
# *************************************************************************** #

[docs]class PlotItemCustom(pg.graphicsItems.PlotItem.PlotItem): """ Subclass of **pyqtgraph.graphicsItems.PlotItem.PlotItem** so that the effect of "auto-range" button is applied only on Y axis The method autoBtnClicked is re-implemented. See https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/plotitem.html for details. """
[docs] def autoBtnClicked(self): """ Re-implemented """ self.enableAutoRange(axis='y', enable=True)
[docs]class SignalWidget(pg.PlotWidget): def __init__(self, parent=None, background='default', **kargs): """ Subclass of **pyqtgraph.PlotWidget** so that he effect of "auto-range" button is applied only on Y axis The constructor is re-implemented so that a PlotItemCustom instance is used as the central item of the widget. See https://pyqtgraph.readthedocs.io/en/latest/widgets/plotwidget.html for details about parent class. """ pg.GraphicsView.__init__(self, parent, background) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.enableMouse(False) self.plotItem = PlotItemCustom(**kargs) self.setCentralItem(self.plotItem) for m in [ 'addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'setLimits', 'register', 'unregister', 'viewRect' ]: setattr(self, m, getattr(self.plotItem, m)) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)
[docs]class ProgressWidget(pg.PlotWidget): def __init__( self, nframes, parent=None, progress_style={'symbol': 'o', 'brush': '#F00', 'size': 7}, bg_progress_style={'pen': {'color': 'b', 'width': 2}}, line_style={'color': (0, 0, 0), 'width': 2}, title=None, title_style={'color': '#000', 'size': '9pt'}, ticks_color="#000", ticks_size=9, ticks_offset=0 ): """ Subclass of **pyqtgraph.PlotWidget** that defines the widget used as a progression bar for video/signal navigation in :class:`.ViSiAnnoT` window See https://pyqtgraph.readthedocs.io/en/latest/widgets/plotwidget.html for details about parent class. The constructor is re-implemented. It calls the constructor of PlotWidget and adds new attributes. NB: attributes have the prefix _, so one should use the get methods to access them. :param nframes: number of frames in the progress bar :type nframes: int :param parent: see https://pyqtgraph.readthedocs.io/en/latest/widgets/plotwidget.html :param progress_style: plot style of the sliding progression point :type progress_style: dict :param bg_progress_style: plot style of the background progression bar :type bg_progress_style: dict :param line_style: plot style of the current temporal range bounds :type line_style: dict :param title: progress bar title :type title: str :param title_style: title style :type title_style: dict :param ticks_color: color of the ticks text in HEX string or RGB format :type ticks_color: str or tuple :param ticks_size: font size of the ticks text in pt :type ticks_size: float or int :param ticks_offset: ticks text offset :type ticks_offset: int """ # PlotWidget initialization pg.PlotWidget.__init__(self, parent) # input attributes self._nframes = nframes self._dragged = False # add background bar self._progress_curve = pg.PlotCurveItem( [0, self._nframes], [0, 0], **bg_progress_style ) self.addItem(self._progress_curve) # add sliding progress point self._progress_plot = pg.ScatterPlotItem([0], [0], **progress_style) self.addItem(self._progress_plot) # add infinite lines for first and last frames # (position not initialized) self._first_line = pg.InfiniteLine(pen=line_style) self.addItem(self._first_line) self._last_line = pg.InfiniteLine(pen=line_style) self.addItem(self._last_line) # disable default mouse interaction self.setMouseEnabled(x=False, y=False) self.hideButtons() self.setMenuEnabled(False) # no Y axis self.showAxis('left', show=False) # set X axis ticks style setTicksTextStyle(self.getAxis('bottom'), color=ticks_color, size=ticks_size, offset=ticks_offset) # set title self.setTitle(title, **title_style)
[docs] def getNFrames(self): """ Get method for attribute nframes :returns: number of frames :rtype: int """ return self._nframes
[docs] def getProgressCurve(self): """ Get method for attribute progress_curve :returns: background progression bar :rtype: pyqtgraph.PlotCurveItem """ return self._progress_curve
[docs] def getDragged(self): """ Get method for attribute dragged :returns: specify if the sliding progress point is dragged :rtype: bool """ return self._dragged
[docs] def getProgressPlot(self): """ Get method for attribute progress_plot :returns: sliding progress point :rtype: pyqtgraph.ScatterPlotItem """ return self._progress_plot
[docs] def getFirstLine(self): """ Get method for attribute first_line :returns: start boundary of the current temporal range in the associated :class:`.ViSiAnnoT` window :rtype: pyqtgraph.InfiniteLine """ return self._first_line
[docs] def getLastLine(self): """ Get method for attribute first_line :returns: end boundary of the current temporal range in the associated :class:`.ViSiAnnoT` window :rtype: pyqtgraph.InfiniteLine """ return self._last_line
[docs] def getMouseXPosition(self, ev): """ Computes the position of the mouse on the X axis in the progress plot coordinates :param ev: emitted when the mouse is clicked/moved :type ev: QtGui.QMouseEvent :returns: position of the mouse :rtype: int """ return self.getViewBox().mapToView(ev.pos()).x()
[docs] def updateNFrames(self, nframes): """ Sets a new value for the number of frames in the progress bar :param nframes: new number of frames in the progress bar :type nframes: int """ self._nframes = nframes self._progress_curve.setData([0, self._nframes], [0, 0])
[docs] def mousePressEvent(self, ev): """ Re-implemented in order to set the new position of the sliding progression point and launch the mouse dragging :param ev: emitted when the mouse is clicked/moved :type ev: QtGui.QMouseEvent """ # check if left button is clicked and dragging not launched if ev.button() == QtCore.Qt.LeftButton and not self._dragged: # get mouse position on the X axis position = self.getMouseXPosition(ev) # check boundaries if position >= 0 and position < self._nframes: # set new position of the sliding progress point self._progress_plot.setData([position], [0]) # launch dragging self._dragged = True
[docs] def mouseMoveEvent(self, ev): """ Re-implemented in order to set the new position of the sliding progression point while dragging :param ev: emitted when the mouse is clicked/moved :type ev: QtGui.QMouseEvent """ # check if dragging is launched if self._dragged: # get mouse position on the X axis position = self.getMouseXPosition(ev) # check boundaries if position >= 0 and position < self._nframes: # set new position of the sliding progress point self._progress_plot.setData([position], [0])
[docs] def mouseReleaseEvent(self, ev): """ Re-implemented in order to set the new position of the sliding progression point and terminate the mouse dragging :param ev: emitted when the mouse is clicked/moved :type ev: QtGui.QMouseEvent """ # check if left button release and if dragging is launched if ev.button() == QtCore.Qt.LeftButton and self._dragged: # get mouse position on the X axis position = self.getMouseXPosition(ev) # check boundaries if position >= 0 and position < self._nframes: # set new position of the sliding progress point self._progress_plot.setData([position], [0]) # terminate dragging self._dragged = False
# *************************************************************************** # # *************************************************************************** # # ***************************** Functions *********************************** # # *************************************************************************** # # *************************************************************************** #
[docs]def setBackgroundColor(color=(255, 255, 255)): """ Sets background color of pyqtgraph widgets (independent of PyQt5 widgets) :param color: background color as a string or RGB (see https://pyqtgraph.readthedocs.io/en/latest/style.html for details) :type color: str or tuple """ pg.setConfigOption('background', color)
[docs]def initializeDisplayAndBgColor(color=(255, 255, 255)): """ Creates a Qt application for display and sets background color of the pyqtgraph widgets It calls the functions :func:`.ToolsPyQt.initializeDisplay` and :func:`.Toolspyqtgraph.setBackgroundColor`. :param color: background color as a string or RGB (see https://pyqtgraph.readthedocs.io/en/latest/style.html for details) :type color: str or tuple :returns: instance of QtCore.QCoreApplication or QtWidgets.QApplication """ app = initializeDisplay() setBackgroundColor(color=color) return app
[docs]def create2DWidget( lay, widget_position, widget_size=(0, 0), widget_title=None, title_style={'color': '#0000', 'size': '9pt'}, x_range=[], y_range=[], axes_label_dict={"left": None, "bottom": None}, flag_invert_x=False, flag_invert_y=False, flag_aspect_locked=False, flag_plot_menu=True ): """ Creates a 2D widget and adds it to a grid layout The widget content is empty. :param lay: parent layout where the widget is added :type lay: QtWidgets.QGridLayout :param widget_position: position of the widget in the parent layout, length 2 ``(row, col)`` or 4 ``(row, col, rowspan, colspan)`` :type widget_position: tuple :param widget_size: maximum size of the widget, length 2 ``(width, height)``, set a value to 0 in order to have an automatic sizing in the corresponding axis :type widget_size: tuple :param widget_title: widget title, basic HTML allowed :type widget_title: str :param title_style: title style :type title_style: dict :param x_range: range values on the X axis, length 2 ``(x_min, x_max)`` :type x_range: tuple :param y_range: range values on the Y axis, length 2 ``(y_min, y_max)`` :type y_range: tuple :param axes_label_dict: axes to show and their respective label - Key is the axis identifier (``"left"``, ``"right"``, ``"bottom"`` or ``"top"``) - Value is the associated label as a list of length 2: - axis label (*str*) - label style (*dict*), e.g. ``{'color': '#000', 'font-size': '10pt'}`` - In case no label is associated to the axis, set the corresponding value to ``None`` :type axes_label_dict: dict :param flag_invert_x: specify if the X axis must be inverted :type flag_invert_x: bool :param flag_invert_y: specify if the Y axis must be inverted :type flag_invert_y: bool :param flag_aspect_locked: specify if the aspect ratio must be locked :type flag_aspect_locked: bool :param flag_plot_menu: specify if the plot menu is enabled :type flag_plot_menu: bool :returns: widget :rtype: pyqtgraph.PlotWidget """ # create the widget widget = pg.PlotWidget() # axes settings widget.showAxis('left', show=False) widget.showAxis('bottom', show=False) for axis, label_info in axes_label_dict.items(): widget.showAxis(axis, show=True) if label_info is not None: if label_info[1] is None: widget.getAxis(axis).setLabel(text=label_info[0]) else: widget.getAxis(axis).setLabel( text=label_info[0], **label_info[1] ) # invert axis if isinstance(flag_invert_x, bool): widget.invertX(flag_invert_x) if isinstance(flag_invert_y, bool): widget.invertY(flag_invert_y) # set title widget.setTitle(widget_title, **title_style) # set the widget size if widget_size[0] == 0 and widget_size[1] != 0: widget.setMaximumHeight(widget_size[1]) elif widget_size[0] != 0 and widget_size[1] == 0: widget.setMaximumWidth(widget_size[0]) elif widget_size[0] != 0 and widget_size[1] != 0: widget.setMaximumSize(widget_size[0], widget_size[1]) # set aspect locked or not widget.setAspectLocked(flag_aspect_locked) # set plot menu widget.setMenuEnabled(flag_plot_menu) # set the range if (isinstance(x_range, tuple) or isinstance(x_range, list)) and \ len(x_range) == 2: widget.setXRange(x_range[0], x_range[1]) if (isinstance(y_range, tuple) or isinstance(y_range, list)) and \ len(y_range) == 2: widget.setYRange(y_range[0], y_range[1]) # add the widget to the layout addWidgetToLayout(lay, widget, widget_position) return widget
[docs]def createWidgetImage(lay, widget_position, im=None, title=None, title_style={'color': '#000', 'size': '9pt'}): """ Creates a widget containing an image and adds it to a grid layout :param lay: parent layout where the widget is added :type lay: QtWidgets.QGridLayout :param widget_position: position of the widget in the parent layout, length 2 ``(row, col)`` or 4 ``(row, col, rowspan, colspan)`` :type widget_position: tuple :param im: RGB image array of shape :math:`(width, height, 3)` :type im: numpy array :param title: widget title :type title: str :param title_style: widget title style :type title_style: dict :returns: - **widget** (:class:`.ToolsPyqtgraph.PlotWidget`) -- image container - **img** (*pyqtgraph.ImageItem*) -- image item """ # create the widget widget = create2DWidget( lay, widget_position, widget_title=title, title_style=title_style, axes_label_dict={}, flag_invert_y=True, flag_aspect_locked=True, flag_plot_menu=False ) # create image item and add it to the widget img = pg.ImageItem(im) widget.addItem(img) return widget, img
[docs]def createWidgetSignal( lay, widget_position, y_range=[], left_label='', left_label_style={'color': '#000', 'font-size': '10pt'}, ticks_color="#000", ticks_size=9, ticks_offset=0 ): """ Creates a widget for plotting signals (see :class:`.ToolsPyqtgraph.SignalWidget) and adds it to a grid layout (used in :class:`.ViSiAnnoT`) For details about color, see https://pyqtgraph.readthedocs.io/en/latest/functions.html#color-pen-and-brush-functions :param lay: parent layout where the widget is added :type lay: QtWidgets.QGridLayout instance of QtWidgets.QGridLayout where the widget is added :param widget_position: position of the widget in the parent layout, length 2 ``(row, col)`` or 4 ``(row, col, rowspan, colspan)`` :type widget_position: tuple :param y_range: visible Y range, length 2 ``(y_min, y_max)``, set to ``[]`` for auto range :type y_range: tuple :param left_label: label for Y axis :type left_label: str :param left_label_style: axis label title style :type left_label_style: dict :param ticks_color: color of the ticks text in HEX string or RGB format :type ticks_color: str or tuple :param ticks_size: font size of the ticks text in pt :type ticks_size: float or int :param ticks_offset: ticks text offset :type ticks_offset: int :returns: widget containing the signals plots :rtype: ToolsPyQt.SignalWidget """ # create the widget widget = SignalWidget() # add the widget to the layout addWidgetToLayout(lay, widget, widget_position) # disable default mouse interaction widget.setMouseEnabled(x=False) # disable plot menu widget.setMenuEnabled(False) # set axes font setTicksTextStyle( widget.getAxis('left'), color=ticks_color, size=ticks_size, offset=ticks_offset ) setTicksTextStyle( widget.getAxis('bottom'), color=ticks_color, size=ticks_size, offset=ticks_offset ) # set Y axis label widget.getAxis('left').setLabel(text=left_label, **left_label_style) # disable auto-range on X axis widget.enableAutoRange(axis='x', enable=False) # check if Y range is to be set if len(y_range) == 2 and y_range[0] < y_range[1]: # set Y range widget.setYRange(y_range[0], y_range[1]) # disable default mouse interaction widget.setMouseEnabled(y=False) # hide auto range button widget.hideButtons() else: # enable auto-range on Y axis widget.enableAutoRange(axis='y', enable=True) return widget
[docs]def setTicksTextStyle(axis_item, color="#000", size=9, offset=0): """ Sets ticks text style of an axis item See https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/axisitem.html for details. :param axis_item: axis for which the style of ticks text must be set :type axis_item: pyqtgraph.AxisItem :param color: color of the text in HEX string or RGB format :type color: str or tuple :param size: font size of the text in pt :type size: float or int :param offset: text offset :type offset: int """ # set ticks color axis_item.setPen(color=color) axis_item.setTextPen(color=color) # create font with desired size font = QFont() font.setPointSize(size) # set ticks size axis_item.setTickFont(font) # set offset axis_item.setStyle(tickTextOffset=offset)
[docs]def deleteNaNForPlot(data): """ Deletes NaNs from an array so that they are ignored for plotting :param data: array where to delete NaNs, shape :math:`(n,)` or :math:`(n, 2)` (where first column contains timestamp, second column contains data value), list and tuple are also supported :type data: numpy array :returns: array without NaNs, shape :math:`(m, 2)`, with :math:`m \leq n` ; if the input array is 1D (shape :math:`(n,)`), then the first column of the output array contains the index of each point in the input array :rtype: numpy array """ if isinstance(data, list) or isinstance(data, tuple): data = np.array(data) if data.ndim == 1: data = np.vstack(( np.arange(data.shape[0]), data )).T data = np.delete(data, np.where(np.isnan(data[:, 1])), axis=0) return data
[docs]def addPlotTo2DWidget( widget, data, flag_clear=False, flag_nan_void=True, plot_style={'pen': {'color': 'b', 'width': 1}} ): """ Adds a plot item to a 2D widget For details about plot style, see https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/plotdataitem.html For details about color, see https://pyqtgraph.readthedocs.io/en/latest/functions.html#color-pen-and-brush-functions :param widget: widget where the plot must be displayed :type widget: pyqtgraph.PlotWidget :param data: data to plot, two possibilites: - shape :math:`(n, 2)`, first column contains X values, second column contains Y values, if only one point to plot make sure to have shape :math:`(1, 2)` - shape :math:`(n,)`, only Y values, then X values are the array indexes :type data: numpy array :param flag_clear: specify if content currently displayed must be cleared before plotting the data :type flag_clear: bool :param flag_nan_void: specify how to handle NaN values - ``False``: default behaviour of pyqtgraph - ``True``: there is no plotting where NaN NB: when calling the method ``plot.setData(data_array)``, the behaviour is back to default. If NaNs must be ignored, the function :func:`.ToolsPyqtgraph.deleteNaNForPlot` must be called before. :type flag_nan_void: bool :param plot_style: plot style, keys are keyword arguments of the constructor of pyqtgraph.PlotDataItem, see link above :type plot_style: dict :returns: plot item (or a list of pyqtgraph.PlotDataItem in case ``nan_void`` is true) :rtype: pyqtgraph.PlotDataItem """ if flag_clear: widget.clear() if flag_nan_void: data = deleteNaNForPlot(data) plot = pg.PlotDataItem(data, **plot_style) widget.addItem(plot) return plot
[docs]def basic2DPlot( data, opts_win_dict={}, opts_wid_dict={}, plot_style={'pen': {'color': 'b', 'width': 1}} ): """ Creates a window with a 2D plot For details about plot style, see https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/plotdataitem.html For details about color, see https://pyqtgraph.readthedocs.io/en/latest/functions.html#color-pen-and-brush-functions :param data: data to plot, two possibilites: - shape :math:`(n, 2)`, first column contains X values, second column contains Y values, if only one point to plot make sure to have shape :math:`(1, 2)` - shape :math:`(n,)`, only Y values, then X values are the array indexes :type data: numpy array :param opts_win_dict: keyword arguments of the function :func:`.ToolsPyQt.createWindow` :type opts_win_dict: dict :param opts_wid_dict: keyword arguments of the function :func:`.ToolsPyqtgraph.create2DWidget` :type opts_wid_dict: dict :param plot_style: plot style, keys are keyword arguments of the constructor of pyqtgraph.PlotDataItem, see link above :type plot_style: dict :returns: - **win** (*QWidgets.QWidget*) -- window container - **lay** (*QWidgets.QGridLayout*) -- layout - **widget** (*pyqtgraph.PlotWidget*) -- 2D widget filling the whole layout - **plot** (*pyqtgraph.PlotDataItem*) -- plot item """ win, lay = createWindow(**opts_win_dict) if "bg_color" in opts_win_dict.keys(): setBackgroundColor(opts_win_dict["bg_color"]) widget = create2DWidget(lay, (0, 0), **opts_wid_dict) plot = addPlotTo2DWidget(widget, data, plot_style=plot_style) return win, lay, widget, plot
[docs]def basicImagePlot(im, **kwargs): """ Creates a window with an image :param im: RGB image array of shape :math:`(width, height, 3)` :type im: numpy array :param kwargs: keyword arguments of the function :func:`.ToolsPyQt.createWindow`, if background color of the window is specified, then it is also applied to the image widget :returns: - **win** (*QWidgets.QWidget*) -- window container - **lay** (*QWidgets.QGridLayout*) -- layout - **widget** (*pyqtgraph.PlotWidget*) -- 2D widget filling the whole layout - **img** (*pyqtgraph.ImageItem*) -- image item """ win, lay = createWindow(**kwargs) if "bg_color" in kwargs.keys(): setBackgroundColor(kwargs["bg_color"]) widget, img = createWidgetImage(lay, (0, 0), im=im) return win, lay, widget, img
[docs]def addLegendTo2DWidget( widget, item_dict, size=None, offset=(0, 0), position='inside', legend_wid_size=(0, 0) ): """ Adds a legend to a 2D widget If specified, this function creates a widget dedicated to the legend, instead of having a legend item inside the 2D widget. :param widget: widget containing the plot items to legend :type widget: pyqtgraph.PlotWidget :param item_dict: plot items to legend, key is the legend label and value is the associated plot item :type item_dict: dict :param size: size of the legend item, length 2 ``(width, height)`` :type size: tuple :param offset: offset position in pixels of the legend item in the widget :type offset: tuple :param position: legend mode - ``"inside"``: legend item inside the widget - ``"right"``: legend item is in a new widget created at the right side of the widget - ``"bottom"``: legend is in a new widget created at the bottom side of the 2D widget :type position: str :param legend_wid_size: size of the created legend widget, length 2 ``(width, height)`` in case ``position`` is ``"right"`` or ``"bottom"`` :type legend_wid_size: tuple :returns: - **legend** (*-pyqtgraph.LegendItem*) -- legend item - **legend_widget** (*pyqtgraph.PlotWidget*) -- widget containing the legend item if it has been created (otherwise ``None``) """ if position == 'inside': # create legend legend = pg.LegendItem(size=size, offset=offset) legend.setParentItem(widget.graphicsItem()) legend_widget = None elif position == 'right' or position == 'bottom': # get the list of the window children, first element is the layout # the others are the widgets win_children_list = widget.parent().children() # get the layout layout = win_children_list[0] # get the widget index in the layout wid_ind = layout.indexOf(widget) # get the widget position in the layout wid_pos = layout.getItemPosition(wid_ind) # get the legend widget position in the layout if position == 'right': legend_wid_pos = (wid_pos[0] + (wid_pos[2] - 1), wid_pos[1] + wid_pos[3], wid_pos[2], 1) elif position == 'bottom': legend_wid_pos = (wid_pos[0] + wid_pos[2], wid_pos[1] + (wid_pos[3] - 1), 1, wid_pos[3]) # check if the legend widget has already been created if layout.itemAtPosition(legend_wid_pos[0], legend_wid_pos[1]) == None: # create the legend widget legend_widget = create2DWidget( layout, legend_wid_pos, axes_label_dict={} ) # set the size of the legend widget if legend_wid_size[0] == 0 and legend_wid_size[1] != 0: legend_widget.setMaximumHeight(legend_wid_size[1]) elif legend_wid_size[0] != 0 and legend_wid_size[1] == 0: legend_widget.setMaximumWidth(100) elif legend_wid_size[0] != 0 and legend_wid_size[1] != 0: legend_widget.setMaximumSize( legend_wid_size[0], legend_wid_size[1] ) else: # get the widget that contains the legend # assuming that it is the element right after the widget legend_widget = \ win_children_list[win_children_list.index(widget) + 1] # create legend legend = pg.LegendItem(size=size, offset=offset) legend.setParentItem(legend_widget.graphicsItem()) # set the legend item to the corresponding item and label for item_label, item in item_dict.items(): legend.addItem(item, item_label) return legend, legend_widget
[docs]def addTextItemTo2DWidget( widget, pos, text='', color="#000000", html=None, anchor=(0.5, 0.5), border=None, fill=None, angle=0, flag_arrow=False, arrow_angle=0, arrow_style={ 'headLen': 15, 'tipAngle': 20, 'pen': '#000000', 'brush': '#000000' } ): """ Adds a text item to a 2D widget For details about color, see https://pyqtgraph.readthedocs.io/en/latest/functions.html#color-pen-and-brush-functions :param widget: widget where the text item must be displayed :type widget: pyqtgraph.PlotWidget :param pos: position of the text item in the widget, length 2 ``(x, y)`` :type pos: tuple :param text: text to display :type text: str :param color: text color, may be a tuple with RGB or a HEX string :type color: str or tuple :param html: html text to display, it overwrites ``text`` and ``color`` :type html: str :param anchor: coordinate of the text item that is anchored to its position ``pos`` (``(0, 0)`` => top left, ``(1, 0)`` => top right, ``(1, 0)`` => bottom left, ``(1, 1)`` => bottom right :type anchor: tuple :param border: color of the border, may be a tuple with RGB or a HEX string :type border: str or tuple :param fill: color used when filling within the border, may be a tuple with RGB or a HEX :type fill: str or tuple :param angle: angle in degrees to rotate the text item :type angle: float :param flag_arrow: specify if an arrow must be added :type flag_arrow: bool :param arrow_angle: angle in degrees to rotate the arrow (arrow pointing to the left) :type arrow_angle: float :param arrow_style: see https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/arrowitem.html#pyqtgraph.ArrowItem.setStyle :type arrow_style: dict :returns: - **text_item** (*pyqtgraph.TextItem*) - **arrow_item** (*pyqtgraph.ArrowItem*) -- may be ``None`` if arrow not specified """ # create text item text_item = pg.TextItem( text=text, color=color, html=html, anchor=anchor, border=border, fill=fill, angle=angle ) # set the text item position text_item.setPos(pos[0], pos[1]) # add the text item to the widget widget.addItem(text_item) # create an arrow item if specified and add it to the widget if flag_arrow: arrow_item = pg.ArrowItem(pos=pos, angle=arrow_angle) arrow_item.setStyle(**arrow_style) widget.addItem(arrow_item) else: arrow_item = None return text_item, arrow_item
[docs]def createColorMap(values, colors, lut_dim=256): """ Creates a color map See https://pyqtgraph.readthedocs.io/en/latest/colormap.html for details. :param values: values spanning the color map, at least two values are required, it may be a 1D numpy array :type values: list or tuple :param colors: colors corresponding to the values, must be the same length as ``values``, each element is a (RGB) color :type colors: list or numpy array :param lut_dim: dimension of the look up table :type lut_dim: int :returns: - **color_map** (*pyqtgraph.ColorMap*) - **lut** (*numpy array*) -- color of the look up table, shape ``(lut_dim, 4)`` """ color_map = pg.ColorMap(values, colors) lut = color_map.getLookupTable(values[0], values[-1], lut_dim) return color_map, lut
[docs]def createWidgetColorBar( lay, widget_position, color_map, lut, ticks_values, widget_width=80, ticks_val_formatting="%s", img_width=5 ): """ Creates a widget with a color bar (useful for the legend of a heat map) :param lay: parent layout where the widget is added :type lay: QtWidgets.QGridLayout :param widget_position: position of the widget in the parent layout, length 2 ``(row, col)`` or 4 ``(row, col, rowspan, colspan)`` :type widget_position: tuple :param color_map: color map to define the color bar :type color_map: pyqtgraph.ColorMap :param lut: look up table to define the color bar :type lut: numpy array :param ticks_values: values of the ticks to display :type ticks_values: list or tuple :param widget_width: width of the widget in pixels :type widget_width: int :param ticks_val_formatting: formatting pattern of the ticks text :type ticks_val_formatting: str :param img_width: width of the color bar in pixels :type img_width: int :returns: - **wid_bar** (:class:`.ToolsPyqtgraph.PlotWidget`) -- widget containing the color bar - **bar_img_item** (*pyqtgraph.ImageItem*) -- image item of the color bar """ # legend color bar image lut_img = np.expand_dims(lut, axis=0) lut_img = np.tile(lut_img, (img_width, 1, 1)) # create legend widget wid_bar, bar_img_item = createWidgetImage(lay, widget_position, im=lut_img) wid_bar.invertY(False) wid_bar.setMaximumWidth(widget_width) wid_bar.showAxis("right") # loop on ticks values bar_ticks = [] for val in ticks_values: # color corresponding to value val_color = color_map.map(val)[:3] # get similarity between the LUT and the value color lut_val_similarity = np.sum(lut == val_color, axis=1) # get color index in LUT color_ind = np.where(lut_val_similarity == 3)[0] if color_ind.shape[0] > 0: color_ind = color_ind[0] else: # if value color not in LUT, look for the closest color_inds = np.where(lut_val_similarity == 2)[0] diff_sum_prev = -np.inf for ite, color_ind_tmp in enumerate(color_inds): diff_sum = np.abs( np.sum(lut[color_ind_tmp].astype(int) - val_color) ) if diff_sum >= diff_sum_prev and ite > 0: color_ind = color_inds[ite - 1] break diff_sum_prev = diff_sum # update ticks list bar_ticks.append((color_ind, ticks_val_formatting % val)) # set Y ticks right_axis = wid_bar.getAxis("right") right_axis.setTicks([bar_ticks]) right_axis.setStyle(stopAxisAtTick=(True, True)) return wid_bar, bar_img_item
[docs]def addMeanStdPlotTo2DWidget( wid, data_mean, data_std, data_X=None, pen_std_style={'color': 'k'}, n_population_list=[], **kwargs ): """ Adds a plot item to a 2D widget, a vertical line is associated to each point of the plot item, particularly useful to display a mean/median curve with the standard deviation The method raises an exception if ``data_mean`` and ``data_std`` or ``data_mean`` and ``data_X`` do not have the same length. :param wid: widget where to add the plot items :type wid: pyqtgraph.PlotWidget :param data_mean: mean/median values (1D array) :type data_mean: numpy array :param data_std: standard deviation values (1D array), same length as ``data_mean`` :type data_std: numpy array :param data_X: coordinates of the mean/std values on the X axis (1D array), must have the same length as ``data_mean`` :type data_X: numpy array :param pen_std_style: style of the standard deviation plot item :type pen_std_style: dict :param n_population_list: number of samples at each point (same length than ``data_mean``), a text item is then added near each point :type n_population_list: list :param kwargs: keyword arguments of the function :func:`.ToolsPyqtgraph.addPlotTo2DWidget`, used for the plot item of the mean/median values :returns: - **mean_plot** (*pyqtgraph.PlotDataItem*) -- plot item of the mean/median values - **std_plot** (*pyqtgraph.PlotDataItem*) -- plot item of the standard deviation values - **text_item_list** (*list*) -- text items (pyqtgraph.TextItem) of the number of samples at each point, empty list if ``n_population_list`` is empty """ # check shape if data_mean.shape[0] != data_std.shape[0]: raise Exception( "Mean and std do not have the same shape: %d vs %d" % (data_mean.shape[0], data_std.shape[0]) ) elif data_X is not None: if data_mean.shape[0] != data_X.shape[0]: raise Exception( "Mean and X array do not have the same shape: %d vs %d" % (data_mean.shape[0], data_X.shape[0]) ) # format mean array if X data is provided if data_X is None: data_mean_tmp = data_mean else: data_mean_tmp = np.vstack((data_X, data_mean)).T # format list of samples number if len(n_population_list) != data_mean.shape[0]: n_population_list = [None for i in range(data_mean.shape[0])] # plot mean data mean_plot = addPlotTo2DWidget(wid, data_mean_tmp, **kwargs) # create X data if necessary if data_X is None: data_X = np.arange(data_mean.shape[0]) # get X axis step if data_X.shape[0] > 1: x_step = data_X[1] - data_X[0] else: x_step = 1 # initialize std curve array data_std_tmp = np.empty((0, 2)) # loop on median values text_item_list = [] for x, med, std, nb_samples in zip( data_X, data_mean, data_std, n_population_list ): # check if not NaN if ~np.isnan(med): # create std curve sub-array data = np.array([[x, med - std], [x, med + std], [x - x_step / 4, med - std], [x + x_step / 4, med - std], [x - x_step / 4, med + std], [x + x_step / 4, med + std]]) # concatenate std curve array data_std_tmp = np.concatenate((data_std_tmp, data)) # add text item with number of data if nb_samples is not None: text_item, _ = addTextItemTo2DWidget( wid, (x + x_step / 8, med + std / 2), text="%d" % nb_samples ) text_item_list.append(text_item) # plot std curve std_plot = pg.PlotCurveItem( data_std_tmp[:, 0], data_std_tmp[:, 1], pen=pen_std_style, connect="pairs" ) wid.addItem(std_plot) return mean_plot, std_plot, text_item_list