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

"""
Module Documentation Here
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2021"
__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: Ed Brooksbank $"
__dateModified__ = "$dateModified: 2021-07-30 20:44:25 +0100 (Fri, July 30, 2021) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: CCPN $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================

import pyqtgraph as pg
from PyQt5 import QtCore, QtGui, QtWidgets
from pyqtgraph.Point import Point

from ccpn.ui.gui.lib.mouseEvents import \
    leftMouse, shiftLeftMouse, controlLeftMouse, controlShiftLeftMouse, \
    middleMouse, shiftMiddleMouse, controlMiddleMouse, controlShiftMiddleMouse, \
    rightMouse, shiftRightMouse, controlRightMouse, controlShiftRightMouse
from ccpn.core.NmrResidue import NmrResidue
from ccpn.ui.gui.widgets.CustomExportDialog import CustomExportDialog
from ccpn.ui.gui.widgets.Menu import Menu
from ccpn.util.Logging import getLogger
from ccpn.util.Common import percentage
from ccpn.core.Spectrum import Spectrum


current = []


#TODO:LUCA: this is most likely yours; update with documentation and check for ViewBox __init__ as it has changed

[docs]class BarGraph(pg.BarGraphItem): def __init__(self, application=None, viewBox=None, xValues=None, yValues=None, objects=None, brush=None, drawLabels=True, labelDistanceRatio=0.1, **kwds): super().__init__(**kwds) """ This class allows top draw bars with or without objects.It Needs only xValues and yValues. The bar width is by default set to 1. The objects are linked to the bars through the label annotations (with setData). """ # TODO: # setObjects in a more general way. Initially implemented only for NmrResidues objects. self.viewBox = viewBox self.callback = None self.trigger = QtCore.pyqtSignal() self.xValues = xValues if xValues is not None else [] self.yValues = yValues if yValues is not None else [] self.brush = brush self.clicked = None self.objects = objects or [] self.application = application self.opts = dict( # setting for BarGraphItem x=self.xValues, x0=self.xValues, x1=self.xValues, height=self.yValues, width=1, pen=self.brush, brush=self.brush, pens=None, brushes=None, ) self.opts.update(self.opts) self.allValues = {} self.getValueDict() self.labels = [] if drawLabels: self.drawLabels(labelDistanceRatio) if self.objects: self.setObjects(self.objects)
[docs] def setValues(self, xValues, yValues): opts = dict( # setting for BarGraphItem x=xValues, x0=xValues, x1=xValues, height=yValues, width=1, pen=self.brush, brush=self.brush, pens=None, brushes=None, ) self.opts.update(opts)
[docs] def setObjects(self, objects, objAttr='pid'): if len(self.labels) == len(objects): for label, obj in zip(self.labels, objects): if isinstance(obj, NmrResidue): nmrResidue = obj if hasattr(nmrResidue, 'sequenceCode'): if nmrResidue.residue: if nmrResidue.sequenceCode is not None: if str(nmrResidue.sequenceCode) == label.text(): label.setData(int(nmrResidue.sequenceCode), obj) else: label.setData(0, obj) if objAttr == 'pid': label.setText(obj.pid) else: label.setText(getattr(obj, objAttr, ''))
# for label in self.labels: # for object in objects: # if isinstance(object, NmrResidue): # nmrResidue = object # if hasattr(nmrResidue, 'sequenceCode'): # # if nmrResidue.residue: # if nmrResidue.sequenceCode is not None: # if str(nmrResidue.sequenceCode) == label.text(): # label.setData(int(nmrResidue.sequenceCode), object) # if isinstance(object, Spectrum): # label.setData(str(object.name), object) # # # # if nmrResidue.sequenceCode is not None: # # ind = nmrResidue.nmrChain.nmrResidues.index(nmrResidue) # # lbl = label.text() # # if str(ind) == lbl: # # label.setData(ind, object) # # # else: # # pass # # print('Impossible to set this object to its label. Function implemented only for NmrResidue')
[docs] def getValueDict(self): for x, y in zip(self.xValues, self.yValues): self.allValues.update({x: y})
[docs] def mouseClickEvent(self, event): position = event.pos().x() self.clicked = int(position) if event.button() == QtCore.Qt.LeftButton: for label in self.labels: if label.text() == str(self.clicked): label.setSelected(True) event.accept()
[docs] def mouseDoubleClickEvent(self, event): position = event.pos().x() self.doubleclicked = int(position) if event.button() == QtCore.Qt.LeftButton: for label in self.labels: if label.text() == str(self.doubleclicked): print(label.text(), label.data(self.doubleclicked)) event.accept()
[docs] def drawLabels(self, ratio=0.5): """ The label Text is the str of the x values and is used to find and set an object to it. NB, changing the text to any other str may not set the objects correctly! """ self.allLabelsShown = True for key, value in self.allValues.items(): label = CustomLabel(text=str(key)) self.viewBox.addItem(label) label.setPos(int(key), value + ratio) self.labels.append(label) label.setBrush(QtGui.QColor(self.brush))
[docs]class CustomLabel(QtWidgets.QGraphicsSimpleTextItem): """ A text annotation of a bar. """ def __init__(self, text, application=None): QtWidgets.QGraphicsSimpleTextItem.__init__(self) self.setText(text) font = self.font() font.setPointSize(15) self.setRotation(-75) self.setFont(font) self.setFlag(self.ItemIgnoresTransformations + self.ItemIsSelectable) self.setToolTip(text) self.isBelowThreshold = False self.customObject = self.data(int(self.text())) self.application = application
[docs] def setCustomObject(self, obj): self.customObject = obj self.customObject.customLabel = self
[docs] def getCustomObject(self): return self.customObject
[docs] def paint(self, painter, option, widget): self._selectCurrent() QtWidgets.QGraphicsSimpleTextItem.paint(self, painter, option, widget)
def _selectCurrent(self): if self.text().isdigit(): if self.data(int(self.text())) is not None: if self.application is not None: if self.data(int(self.text())) in self.application.current.nmrResidues: self.setSelected(True)
# add other option to use pids/
[docs]class CustomViewBox(pg.ViewBox): itemsSelected = QtCore.pyqtSignal(object) def __init__(self, application=None, *args, **kwds): pg.ViewBox.__init__(self, *args, **kwds) self.exportDialog = None self.addSelectionBox() self.application = application self.allLabelsShown = True self.showAboveThresholdOnly = False self.lastRange = self.viewRange() self.__xLine = pg.InfiniteLine(angle=0, movable=True, pen='b') self.addItem(self.xLine) self.contextMenu = None @property def xLine(self): return self.__xLine @xLine.getter def xLine(self): return self.__xLine
[docs] def addSelectionBox(self): self.selectionBox = QtWidgets.QGraphicsRectItem(0, 0, 1, 1) self.selectionBox.setPen(pg.functions.mkPen((255, 0, 255), width=1)) self.selectionBox.setBrush(pg.functions.mkBrush(255, 100, 255, 100)) self.selectionBox.setZValue(1e9) self.addItem(self.selectionBox, ignoreBounds=True) self.selectionBox.hide()
[docs] def wheelEvent(self, ev, axis=None): if (self.viewRange()[0][1] - self.viewRange()[0][0]) >= 10.001: self.lastRange = self.viewRange() super(CustomViewBox, self).wheelEvent(ev, axis) if (self.viewRange()[0][1] - self.viewRange()[0][0]) < 10: self.setRange(xRange=self.lastRange[0])
def _getLimits(self, p1: float, p2: float): r = QtCore.QRectF(p1, p2) r = self.childGroup.mapRectFromParent(r) self.selectionBox.setPos(r.topLeft()) self.selectionBox.resetTransform() self.selectionBox.scale(r.width(), r.height()) minX = r.topLeft().x() minY = r.topLeft().y() maxX = minX + r.width() maxY = minY + r.height() return minX, maxX, minY, maxY def _updateSelectionBox(self, ev, p1: float, p2: float): """ Updates drawing of selection box as mouse is moved. """ r = QtCore.QRectF(p1, p2) # print('PPP',dir(self.mapToParent(ev.buttonDownPos()))) r = self.mapRectFromParent(r) self.selectionBox.setPos(self.mapToParent(ev.buttonDownPos())) self.selectionBox.resetTransform() self.selectionBox.scale(r.width(), r.height()) self.selectionBox.show()
[docs] def mouseClickEvent(self, event): if event.button() == QtCore.Qt.RightButton: event.accept() self._raiseContextMenu(event) elif event.button() == QtCore.Qt.LeftButton: event.accept()
[docs] def mouseDragEvent(self, event): """ Re-implementation of PyQtGraph mouse drag event to allow custom actions off of different mouse drag events. Same as spectrum Display. Check SpectrumDisplay View Box for more documentation. """ selected = [] if leftMouse(event): # Left-drag: Panning of the view pg.ViewBox.mouseDragEvent(self, event) elif controlLeftMouse(event): self._updateSelectionBox(event, event.buttonDownPos(), event.pos()) event.accept() if not event.isFinish(): self._updateSelectionBox(event, event.buttonDownPos(), event.pos()) else: ## the event is finished. self._updateSelectionBox(event, event.buttonDownPos(), event.pos()) minX, maxX, minY, maxY = self._getLimits(event.buttonDownPos(), event.pos()) labels = [label for label in self.childGroup.childItems() if isinstance(label, CustomLabel)] # Control(Cmd)+left drag: selects label for label in labels: if int(label.pos().x()) in range(int(minX), int(maxX)): if self.inYRange(label.pos().y(), minY, maxY, ): if self.application is not None: obj = label.data(int(label.pos().x())) if obj is None: # new mode of setting object. obj = label.data(0) selected.append(obj) self._resetBoxes() else: self._resetBoxes() event.ignore() if len(selected) > 0: try: self.itemsSelected.emit(selected) if self.application.current: # replace this bit currentSelected = getattr(self.application.current, selected[0]._pluralLinkName) selected.extend(currentSelected) currentObjs = setattr(self.application.current, selected[0]._pluralLinkName, selected) self.updateSelectionFromCurrent() except Exception as e: getLogger().debug('Error in setting current objects. TODO: Replace with itemsSelected Notifier ' + str(e))
[docs] def inYRange(self, yValue, y1, y2): if round(y1, 3) <= round(yValue, 3) <= round(y2, 3): return True return False
[docs] def getLabels(self): return [label for label in self.childGroup.childItems() if isinstance(label, CustomLabel)]
def _selectLabelsByTexts(self, texts): if self.getLabels(): for label in self.getLabels(): for text in texts: if text == label.text(): label.setSelected(True)
[docs] def clearSelection(self): for label in self.getLabels(): label.setSelected(False)
[docs] def updateSelectionFromCurrent(self): if self.getLabels(): for label in self.getLabels(): for nmrResidue in self.application.current.nmrResidues: if nmrResidue.sequenceCode is not None: if label.data(int(nmrResidue.sequenceCode)): label.setSelected(True)
[docs] def upDateSelections(self, positions): if self.getLabels(): for label in self.getLabels(): for position in positions: print('Not impl')
def _resetBoxes(self): "Reset/Hide the boxes " self._successiveClicks = None self.selectionBox.hide() self.rbScaleBox.hide() def _getContextMenu(self): self.contextMenu = Menu('', None, isFloatWidget=True) self.contextMenu.addAction('Reset View', self.autoRange) ## ThresholdLine self.thresholdLineAction = QtGui.QAction("Threshold Line", self, triggered=self._toggleThresholdLine, checkable=True, ) self._checkThresholdAction() self.contextMenu.addAction(self.thresholdLineAction) ## Labels: Show All self.labelsAction = QtGui.QAction("Show Labels", self, triggered=self._toggleLabels, checkable=True, ) self.labelsAction.setChecked(self.allLabelsShown) self.contextMenu.addAction(self.labelsAction) ## Labels: Show Above Threshold self.showAboveThresholdAction = QtGui.QAction("Show Labels Above Threshold", self, triggered=self.showAboveThreshold) self.contextMenu.addAction(self.showAboveThresholdAction) ## Selection: Select Above Threshold self.selectAboveThresholdAction = QtGui.QAction("Select Items Above Threshold", self, triggered=self.selectAboveThreshold) self.contextMenu.addAction(self.selectAboveThresholdAction) self.contextMenu.addSeparator() self.contextMenu.addAction('Export', self.showExportDialog) return self.contextMenu def _raiseContextMenu(self, ev): self.contextMenu = self._getContextMenu() self.contextMenu.exec_(ev.screenPos().toPoint()) def _checkThresholdAction(self): tl = self.xLine if tl: if tl.isVisible(): self.thresholdLineAction.setChecked(True) else: self.thresholdLineAction.setChecked(False)
[docs] def addLabelMenu(self): self.labelMenu = Menu(parent=self.contextMenu, title='Label Menu') self.labelMenu.addItem('Show All', callback=self.showAllLabels, checked=False, checkable=True, ) self.labelMenu.addItem('Hide All', callback=self.hideAllLabels, checked=False, checkable=True, ) self.labelMenu.addItem('Show Above Threshold', callback=self.showAboveThreshold, checked=False, checkable=True, ) self.contextMenu._addQMenu(self.labelMenu)
def _toggleThresholdLine(self): tl = self.xLine if tl: tl.setVisible(not tl.isVisible()) def _toggleLabels(self): if self.allLabelsShown: self.hideAllLabels() else: self.showAllLabels()
[docs] def getThreshouldLine(self): if hasattr(self, 'xLine'): return self.xLine
[docs] def hideAllLabels(self): self.allLabelsShown = False self.showAboveThresholdOnly = False if self.getLabels(): for label in self.getLabels(): label.hide()
[docs] def showAllLabels(self): self.allLabelsShown = True self.showAboveThresholdOnly = False if self.getLabels(): for label in self.getLabels(): label.show()
[docs] def showAboveThreshold(self): self.allLabelsShown = False self.showAboveThresholdOnly = True if self.xLine: yTlPos = self.xLine.pos().y() if self.getLabels(): for label in self.getLabels(): if label.pos().y() >= yTlPos: label.show() else: label.hide() label.isBelowThreshold = True else: print('NOT FOUND')
[docs] def selectAboveThreshold(self): """Reimplement this in the module subclass""" pass
[docs] def showExportDialog(self): if self.exportDialog is None: ### parent() is the graphicsScene self.exportDialog = CustomExportDialog(self.scene(), titleName='Exporting') self.exportDialog.show(self)
###################################################################################################### ################################### Mock DATA ################################################ ###################################################################################################### # from collections import namedtuple # import random # # nmrResidues = [] # for i in range(30): # nmrResidue = namedtuple('nmrResidue', ['sequenceCode']) # nmrResidue.__new__.__defaults__ = (0,) # nmrResidue.sequenceCode = int(random.randint(1,300)) # nmrResidues.append(nmrResidue) # # x1 = [nmrResidue.sequenceCode for nmrResidue in nmrResidues] # y1 = [(i**random.random())/10 for i in range(len(x1))] # # # xLows = [] # yLows = [] # # xMids = [] # yMids = [] # # xHighs = [] # yHighs = [] # # # for x, y in zip(x1,y1): # if y <= 0.5: # xLows.append(x) # yLows.append(y) # if y > 0.5 and y <= 1: # xMids.append(x) # yMids.append(y) # if y > 1: # xHighs.append(x) # yHighs.append(y) # # ###################################################################################################### # # app = pg.mkQApp() # # customViewBox = CustomViewBox() # # # plotWidget = pg.PlotWidget(viewBox=customViewBox, background='w') # customViewBox.setParent(plotWidget) # # x=[6, # 8, # 10, # 12, # 14, # 16, # 18, # 20] # y = [ # 1.731, # 10.809, # 10.658, # 4.831, # 11.406, # 5.287, # 2.971, # 4.412, # ] # # xLow = BarGraph(viewBox=customViewBox, xValues=x, yValues=y, objects=[], brush='r', widht=1) # # xMid = BarGraph(viewBox=customViewBox, xValues=xMids, yValues=yMids, objects=[nmrResidues], brush='b',widht=1) # # xHigh = BarGraph(viewBox=customViewBox, xValues=xHighs, yValues=yHighs,objects=[nmrResidues], brush='g',widht=1) # # # customViewBox.addItem(xLow) # # customViewBox.addItem(xMid) # # customViewBox.addItem(xHigh) # # # xLine = pg.InfiniteLine(pos=max(yLows), angle=0, movable=True, pen='b') # # customViewBox.addItem(xLine) # # l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset) # l.setParentItem(customViewBox.graphicsItem()) # # c1 = plotWidget.plot(pen='r', name='low') # # c2 = plotWidget.plot(pen='b', name='mid') # # c3 = plotWidget.plot(pen='g', name='high') # # l.addItem(c1, 'low') # # l.addItem(c2, 'mid') # # l.addItem(c3, 'high') # # # customViewBox.setLimits(xMin=0, xMax=max(x1) + (max(x1) * 0.5), yMin=0, yMax=max(y1) + (max(y1) * 0.5)) # customViewBox.setRange(xRange=[10,200], yRange=[0.01,1000],) # customViewBox.setMenuEnabled(enableMenu=False) # print(customViewBox.xLine._viewBox) # plotWidget.show() # # # # # # # # Start Qt event # if __name__ == '__main__': # import sys # if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): # QtWidgets.QApplication.instance().exec_() # #