Source code for ccpn.ui.gui.widgets.PlotWidget

"""Module Documentation here

"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2022"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
               "Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
                 "CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
                 "J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Geerten Vuister $"
__dateModified__ = "$dateModified: 2022-02-01 15:30:09 +0000 (Tue, February 01, 2022) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: Geerten Vuister"
__date__ = "$Date: 2018-12-20 15:44:35 +0000 (Thu, December 20, 2018) $"
__date__ = "$Date: 2020-12-03 18:45:05 +0000 (Thu, December 03, 2020) $"
#=========================================================================================
# Start of code
#=========================================================================================

from typing import Sequence

import pyqtgraph as pg
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets, QtOpenGL

from ccpn.ui.gui.widgets.ViewBox import ViewBox
from ccpn.ui.gui.widgets.ViewBox import CrossHair
from ccpn.ui.gui.widgets.CcpnGridItem import CcpnGridItem
from ccpn.ui.gui.lib.mouseEvents import rightMouse
from ccpn.util.Constants import MOUSEDICTSTRIP
from ccpn.util.Colour import Colour

from ccpnmodel.ccpncore.api.ccpnmr.gui.Task import Ruler as ApiRuler
import pyqtgraph.opengl as gl


#TODO:WAYNE: This class should contain all the nitty gritty of the displaying; including the axis labels and the like
# as it is only there and is just a small wrapper arount a pyqtgraph class
# goes together with AxisTextItem (probably can be reduced to a function and included here.
#TODO:WAYNE: should this inherit from Base??


[docs]class PlotWidget(pg.PlotWidget): def __init__(self, strip, useOpenGL=False): #def __init__(self, strip, useOpenGL=False, showDoubleCrosshair=False): # Be sure to use explicit arguments to ViewBox as the call order is different in the __init__ self.viewBox = ViewBox(strip) pg.PlotWidget.__init__(self, parent=strip, viewBox=self.viewBox, axes=None, enableMenu=True) self.strip = strip self.plotItem.setAcceptHoverEvents(True) self.setInteractive(True) self.plotItem.setAcceptDrops(True) self.plotItem.setMenuEnabled(enableMenu=True, enableViewBoxMenu=False) self.rulerLineDict = {} # ruler --> line for that ruler self.rulerLabelDict = {} # ruler --> label for that ruler self.xAxisAtomLabels = [] self.yAxisAtomLabels = [] self.xAxisTextItem = None self.yAxisTextItem = None self.hideButtons() if useOpenGL and QtOpenGL.QGLFormat.hasOpenGL(): # TODO:ED The OpenGL needs to be optimised self.setViewport(QtWidgets.QOpenGLWidget()) # need FullViewportUpdate below, otherwise ND windows do not update when you pan/zoom # (BoundingRectViewportUpdate might work if you can implement boundingRect suitably) # (NoViewportUpdate might work if you could explicitly get the view to repaint when needed) self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate) # self.setOptimizationFlags(QtWidgets.QGraphicsView.DontSavePainterState) # self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) # not sure if these change anything # strip.spectrumDisplay.mainWindow._mouseMovedSignal.connect(self._mousePositionChanged) #TODO:GEERTEN: Fix with proper stylesheet # Also used in AxisTextItem # NOTE: self.highlightColour is also being used in GuiPeakListView for selected peaks if strip.spectrumDisplay.mainWindow.application._colourScheme == 'light': self.background = '#f7ffff' self.foreground = '#080000' self.gridColour = '#080000' self.highlightColour = '#3333ff' self._labellingColour = (10, 10, 10) else: self.background = '#080000' self.foreground = '#f7ffff' self.gridColour = '#f7ffff' self.highlightColour = '#00ff00' self._labellingColour = (255, 255, 255) self.setBackground(self.background) #self.setForeground(self.foreground) # does not seem to have this (or typo?) # axes self.plotItem.axes['left']['item'].hide() self.plotItem.axes['right']['item'].show() for orientation in ('left', 'top'): axisItem = self.plotItem.axes[orientation]['item'] axisItem.hide() for orientation in ('right', 'bottom'): axisItem = self.plotItem.axes[orientation]['item'] axisItem.setPen(color=self.foreground) # axisItem = self.plotItem.axes[orientation]['item'] # axisItem.hide() # add grid self.grid = CcpnGridItem(gridColour=self.gridColour) self.addItem(self.grid, ignoreBounds=False) # Add two crosshairs self.crossHair1 = CrossHair(self, show=True, colour=self.foreground) self.crossHair2 = CrossHair(self, show=False, colour=self.foreground) # add label to show mouse coordinates at the position of the cursor self.mouseLabel = pg.TextItem(text='', color=self._labellingColour, anchor=(0, 1)) self.mouseLabel.hide() self.addItem(self.mouseLabel) self.mouseLabel.setZValue(1.0) # brings the item to the top (I assume everything else is 0) # add label to show stripID in the top corner self.stripIDLabel = pg.TextItem(text='BOX LABEL', color=self._labellingColour) self.stripIDLabel.show() self.addItem(self.stripIDLabel) self.stripIDLabel.setZValue(1.0)
[docs] def highlightAxes(self, state=False): "Highlight the axes on/of" if state: for orientation in ('right', 'bottom'): axisItem = self.plotItem.axes[orientation]['item'] axisItem.setPen(color=self.highlightColour) self.stripIDLabel.setColor(color=self.highlightColour) else: for orientation in ('right', 'bottom'): axisItem = self.plotItem.axes[orientation]['item'] axisItem.setPen(color=self.foreground) self.stripIDLabel.setColor(color=self.foreground)
[docs] def toggleGrid(self): "Toggle grid state" newState = not self.grid.isVisible() self.grid.setVisible(not self.grid.isVisible())
# def cycleSymbolLabelling(self): # "Toggle grid state" # self.symbolLabelling = not self.symbolLabelling # TODO:ED update peaks here def __getattr__(self, attr): """ Wrap pyqtgraph PlotWidget __getattr__, which raises wrong error and so makes hasattr fail. """ try: return super().__getattr__(attr) except NameError: raise AttributeError(attr)
[docs] def addItem(self, item: QtWidgets.QGraphicsObject): """ Adds specified graphics object to the Graphics Scene of the PlotWidget. """ self.scene().addItem(item)
# copied from GuiStripNd! def _mouseDragEvent(self, event): """ Re-implemented mouse event to enable smooth panning. """ if rightMouse(event): pass else: self.viewBox.mouseDragEvent(self, event) def _crosshairCode(self, axisCode): # determines what axisCodes are compatible as far as drawing crosshair is concerned # TBD: the naive approach below should be improved return axisCode # if axisCode[0].isupper() else axisCode @QtCore.pyqtSlot(dict) def _mousePositionChanged(self, mouseMovedDict): """ This is called when the mouse position is changed in some strip It means the crosshair(s) position should be updated :param mouseMovedDict: 'strip'->strip and axisCode->position for each axisCode in strip :return: None """ strip = self.strip if strip.isDeleted: return axes = strip.orderedAxes # TODO:ED sometimes set to None if not axes[0] or not axes[1]: return xPos = mouseMovedDict.get(self._crosshairCode(axes[0].code)) yPos = mouseMovedDict.get(self._crosshairCode(axes[1].code)) #print('>>', strip, xPos, yPos) self.crossHair1.setPosition(xPos, yPos) strip.axisPositionDict[axes[0].code] = xPos strip.axisPositionDict[axes[1].code] = yPos #TODO:SOLIDS This is clearly not correct; it should take the offset as defined for spectrum #xPos = mouseMovedDict.get(self._crosshairCode(axes[1].code)) #yPos = mouseMovedDict.get(self._crosshairCode(axes[0].code)) #self.crossHair2.setPosition(xPos, yPos) if strip.spectra: spectrumView = strip.spectrumViews[0] # use the first spectrum spectrum = spectrumView.spectrum if spectrum.showDoubleCrosshair: #if strip.spectrumDisplay.mainWindow.application.preferences.general.doubleCrossHair: offsets = spectrum.doubleCrosshairOffsets displayIndices = spectrumView.dimensionIndices xOffset = offsets[displayIndices[0]] yOffset = offsets[displayIndices[1]] if xPos is None or xOffset == 0: self.crossHair2.vLine.hide() else: # TBD: below assumes that axis is in ppm xOffset /= spectrum.spectrometerFrequencies[displayIndices[0]] # convert from Hz to ppm self.crossHair2.setVline(xPos + xOffset) self.crossHair2.vLine.show() if yPos is None or yOffset == 0: self.crossHair2.hLine.hide() else: # TBD: below assumes that axis is in ppm yOffset /= spectrum.spectrometerFrequencies[displayIndices[1]] # convert from Hz to ppm self.crossHair2.setHline(yPos + yOffset) self.crossHair2.hLine.show() if self.strip != mouseMovedDict[MOUSEDICTSTRIP]: # hide the mouse label if the event comes form a different window self.mouseLabel.hide() # NBNB TODO code uses API object. REFACTOR def _addRulerLine(self, apiRuler: ApiRuler): """CCPN internal Called from GuiStrip when a ruler is created This adds a line into the PlotWidget""" axisCode = apiRuler.axisCode # TODO: use label and unit position = apiRuler.position label = apiRuler.label if apiRuler.mark.colour[0] == '#': # TODO: why this restriction??? colour = Colour(apiRuler.mark.colour) # TODO: this is a CCPN object, does it work to set pen=colour below else: colour = self.foreground strip = self.strip axisOrder = strip.axisOrder # TODO: is the below correct (so the correct axes)? if axisCode == axisOrder[0]: angle = 90 y = self.plotItem.vb.mapSceneToView(strip.viewBox.boundingRect().bottomLeft()).y() textPosition = (position, y) textAnchor = 1 labels = self.xAxisAtomLabels elif axisCode == axisOrder[1]: angle = 0 x = strip.plotWidget.plotItem.vb.mapSceneToView(strip.viewBox.boundingRect().bottomLeft()).x() textPosition = (x, position) textAnchor = 0 labels = self.yAxisAtomLabels else: return line = pg.InfiniteLine(angle=angle, movable=False, pen=colour) line.setPos(position) self.addItem(line, ignoreBounds=True) self.rulerLineDict[apiRuler] = line if label: textItem = pg.TextItem(label, color=colour) textItem.anchor = pg.Point(0, textAnchor) textItem.setPos(*textPosition) self.addItem(textItem) labels.append(textItem) self.rulerLabelDict[apiRuler] = textItem def _removeRulerLine(self, apiRuler: ApiRuler): """CCPN internal Called from GuiStrip when a ruler is deleted This removes a line into the PlotWidget""" if apiRuler in self.rulerLineDict: line = self.rulerLineDict.pop(apiRuler) self.removeItem(line) if apiRuler in self.rulerLabelDict: label = self.rulerLabelDict.pop(apiRuler) self.removeItem(label) # TODO:WAYNE: Make this part of PlotWidget [done], pass axes label strings on init [??] def _moveAxisCodeLabels(self): """CCPN internal Called from a notifier in GuiStrip Puts axis code labels in the correct place on the PlotWidget """ return self.xAxisTextItem.setPos(self.viewBox.boundingRect().bottomLeft()) self.yAxisTextItem.setPos(self.viewBox.boundingRect().topRight()) for item in self.xAxisAtomLabels: y = self.plotItem.vb.mapSceneToView(self.strip.viewBox.boundingRect().bottomLeft()).y() x = item.pos().x() item.setPos(x, y) for item in self.yAxisAtomLabels: x = self.plotItem.vb.mapSceneToView(self.strip.viewBox.boundingRect().bottomLeft()).x() y = item.pos().y() item.setPos(x, y) # ejb - move the stripIDLabel to be fixed in the top-left corner if the plotWidget k = self.strip.viewBox.boundingRect().topLeft() self.stripIDLabel.setPos(self.plotItem.vb.mapSceneToView(k).x(), self.plotItem.vb.mapSceneToView(k).y()) def _initTextItems(self): """CCPN internal Called from GuiStrip when axes are ready """ axisOrder = self.strip.axisOrder self.xAxisTextItem = AxisTextItem(self, orientation='top', axisCode=axisOrder[0]) self.yAxisTextItem = AxisTextItem(self, orientation='left', axisCode=axisOrder[1])
# TODO:ED this does override but acnnot change the zoom centre # def wheelEvent(self, ev, axis=None): # mask = np.array(self.state['mouseEnabled'], dtype=np.float) # if axis is not None and axis >= 0 and axis < len(mask): # mv = mask[axis] # mask[:] = 0 # mask[axis] = mv # s = ((mask * 0.02) + 1) ** ( # ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor # # center = None # Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) # # center = ev.pos() # # self._resetTarget() # self.scaleBy(s, center) # self.sigRangeChangedManually.emit(self.state['mouseEnabled']) # ev.accept()
[docs]class AxisTextItem(pg.TextItem): def __init__(self, plotWidget, orientation, axisCode=None, units=None, mappedDim=None): self.plotWidget = plotWidget self.orientation = orientation self.axisCode = axisCode self.units = units self.mappedDim = mappedDim pg.TextItem.__init__(self, text=axisCode, color=plotWidget.gridColour) if orientation == 'top': self.setPos(plotWidget.plotItem.vb.boundingRect().bottomLeft()) self.anchor = pg.Point(0, 1) else: self.setPos(plotWidget.plotItem.vb.boundingRect().topRight()) self.anchor = pg.Point(1, 0) plotWidget.scene().addItem(self) def _setUnits(self, units): self.units = units def _setAxisCode(self, axisCode): self.axisCode = str(axisCode)