"""
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-10-29 18:30:41 +0100 (Fri, October 29, 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 os
import shutil
import numpy as np
import pandas as pd
import pyqtgraph as pg
import ccpn.ui.gui.widgets as Widgets
from functools import partial
from collections import OrderedDict as od
import ccpn.ui.gui.guiSettings as gs
import ccpn.ui.gui.lib.mouseEvents as me
import ccpn.ui.gui.lib.GuiStripContextMenus as cm
from PyQt5 import QtCore, QtGui, QtWidgets
from ccpn.util.Colour import hexToRgb, hexToRgba, rgbaRatioToHex, darkDefaultSpectrumColours, hexToRgbaArray
from ccpn.ui.gui.widgets.Widget import Widget
from ccpn.ui.gui.widgets.Label import Label
from ccpn.ui.gui.lib.MenuActions import _openItemObject
from ccpn.ui.gui.widgets.CustomExportDialog import CustomExportDialog
from ccpn.ui.gui.widgets.Menu import Menu
from ccpn.ui.gui.widgets.Frame import Frame
# from ccpn.ui.gui.widgets.PulldownList import PulldownList
from ccpn.ui.gui.widgets.CompoundWidgets import PulldownListCompoundWidget
from ccpn.util.Logging import getLogger
from ccpn.core.lib.CallBack import CallBack
from ccpn.core.lib.Notifiers import Notifier
from ccpn.ui.gui.widgets.Font import setWidgetFont, getWidgetFontHeight
from ccpn.ui.gui.widgets.Font import Font, DEFAULTFONTNAME, DEFAULTFONTSIZE, getFontHeight, getFont
from ccpn.util.Common import _getObjectsByPids, splitDataFrameWithinRange
from ccpn.util.OrderedSet import OrderedSet
from ccpn.ui.gui.widgets.Icon import Icon, ICON_DIR
# colours
BackgroundColour = gs.getColours()[gs.CCPNGLWIDGET_HEXBACKGROUND]
OriginAxes = pg.functions.mkPen(hexToRgb(gs.getColours()[gs.GUISTRIP_PIVOT]), width=1, style=QtCore.Qt.DashLine)
SelectedPointPen = pg.functions.mkPen(rgbaRatioToHex(*gs.getColours()[gs.CCPNGLWIDGET_HIGHLIGHT]), width=4)
ROIline = rgbaRatioToHex(*gs.getColours()[gs.CCPNGLWIDGET_SELECTAREA])
ROIPen = pg.functions.mkPen(ROIline, width=3, style=QtCore.Qt.SolidLine)
HandlePen = pg.functions.mkPen(hexToRgb(gs.getColours()[gs.GUISTRIP_PIVOT]), width=5, style=QtCore.Qt.SolidLine)
DefaultRoiLimits = [[0, 0], [0, 0]] #
SelectedLabel = pg.functions.mkBrush(rgbaRatioToHex(*gs.getColours()[gs.CCPNGLWIDGET_HIGHLIGHT]), width=4)
c = rgbaRatioToHex(*gs.getColours()[gs.CCPNGLWIDGET_LABELLING])
GridPen = pg.functions.mkPen(c, width=1, style=QtCore.Qt.SolidLine)
GridFont = getFont()
DefaultPointSize = 10
DefaultPointColour = '#000000' # black
DefaultInnerPointColour = '#7080EE'
DefaultSymbol = 'o'
AllowedSymbols = ['o', 's', 't', 'd', '+']
_SELECTORNAME = 'selectorName'
_VALUEHEADER = 'headerValueName'
_PIDHEADER = 'headerPidName'
_TOOLTIP = 'tooltip'
_HEXCOLOURHEADER = 'headerHexColour'
_OBJCOLOURPROPERTY = 'objColourProperty'
_SYMBOL = 'symbol'
_CALLBACK = 'callback'
_POINTSIZE = 'pointSize'
_HEXCOLOUR = 'hexColour'
_INNERHEXCOLOUR = 'innerHexColour'
ScatterSymbolsDict = od([
['circle', {'symbol': 'o', 'icon': 'icons/scatter_o'}],
['square', {'symbol': 's', 'icon': 'icons/scatter_s'}],
['triangle', {'symbol': 't', 'icon': 'icons/scatter_t'}],
['diamond', {'symbol': 'd', 'icon': 'icons/scatter_d'}],
['plus', {'symbol': '+', 'icon': 'icons/scatter_+'}],
])
def _getPointsWithinLimits(points, limits):
xMin, xMax, yMin, yMax = limits
ptsPos = list(map(lambda s: (s.pos().x(), s.pos().y()), points))
if len(ptsPos) > 0:
x, y = zip(*ptsPos)
x, y = np.array(x), np.array(y)
i = np.where((x >= xMin) & (x <= xMax) & (y >= yMin) & (y <= yMax))
innerPoints = points[i]
return innerPoints
return []
class _ItemBC(object):
""" """
def __init__(self, headerValueName, **kwargs):
"""
Used to set the points/axis in the plot
:param kwargs:
"""
self.kwargs = {
_SELECTORNAME : headerValueName,
_VALUEHEADER : headerValueName,
_PIDHEADER : None,
_HEXCOLOURHEADER : None,
_OBJCOLOURPROPERTY: None, # the obj property where to grab the colour. E.g for spectrum: sliceColour
}
self.kwargs.update(kwargs)
for k, v in self.kwargs.items():
setattr(self, k, v)
[docs]class ScatterROI(pg.ROI):
"""
Re-implemetation of pg.ROI to allow customised functionalities
"""
def __init__(self, parentWidget, *args, **kwargs):
pg.ROI.__init__(self, *args, **kwargs)
self.parentWidget = parentWidget
self.handleSize = 8
self.translatable = False # keep False otherwise it doesn't allow normal pan/selection of the plotItems within the ROI region.
self._setROIhandles()
self.roiIsLinkedToSelection = True
# self._isEnabled = True
def _setROIhandles(self):
""" sets the handles,"""
## TranslateHandle -> moves the ROI without changing the shape
self.xMinHandle = self.addTranslateHandle((0, 0.5), name='xMinHandle')
self.xMaxHandle = self.addTranslateHandle((1, 0.5), name='xMaxHandle')
self.yMinHandle = self.addTranslateHandle((0.5, 0), name='yMinHandle')
self.yMaxHandle = self.addTranslateHandle((0.5, 1), name='yMaxHandle')
## ScaleHandle -> reshape the ROI without translating it
self.topRightHandle = self.addScaleHandle([1, 1], [0.5, 0.5], name='topRight')
self.topLeft = self.addScaleHandle([0, 1], [1, 0], name='topLeft')
self.bottomLeft = self.addScaleHandle([0, 0], [0.5, 0.5], name='bottomLeft')
self.bottomRight = self.addScaleHandle([1, 0], [0, 1], name='bottomRight'),
[docs] def getLimits(self):
"""
the values for the ROI
(getState returns a dict ['pos'] left bottom corner, ['size'] the size of RO1 and ['angle'] for this RectROI is 0)
:return: a list of rectangle coordinates in the format minX, maxX, minY, maxY
"""
state = self.getState()
pos = state['pos']
size = state['size']
xMin = pos[0]
xMax = pos[0] + size[0]
yMin = pos[1]
yMax = pos[1] + size[1]
return [xMin, xMax, yMin, yMax]
[docs] def setLimits(self, xMin, xMax, yMin, yMax):
"""
a conversion mechanism to the internal roiItem setState
:return: set the ROI box
"""
state = {'pos': [], 'size': [], 'angle': 0}
xMin = np.array([xMin]) # convert to arrays to deal with positives and negatives
xMax = np.array([xMax])
yMin = np.array([yMin])
yMax = np.array([yMax])
xSize = xMax - xMin
ySize = yMax - yMin
state['pos'] = [xMin[0], yMin[0]]
state['size'] = [xSize[0], ySize[0]]
self.setState(state)
[docs] def getInnerPoints(self):
"""
:return: the pointItems within the ROI limits (included extremes)
"""
return _getPointsWithinLimits(self.parentWidget.scatterPlot.points(), self.getLimits())
[docs] def getInnerData(self):
"""
:return: List of Pandas series. The linked data for points within the ROI limits.
"""
return list(map(lambda s: s.data(), self.getInnerPoints()))
def _getInnerIxNames(self):
"""
:return: List of Pandas series. The linked data for points within the ROI limits.
"""
return list(map(lambda s: s.data().name(), self.getInnerPoints()))
[docs] def getInnerDataFrame(self):
"""
:return: the inner data objs as a single Pandas dataframe
"""
innerSeries = self.getInnerData()
return pd.DataFrame(innerSeries)
[docs]class ScatterPlot(Widget):
dataSelectedSignal = QtCore.pyqtSignal(object)
def __init__(self,
application,
dataFrame,
axesDefinitions=None,
roiVisible=True,
roiEnabled=True,
mouseSelectionRegion=True,
pointSelectionCallback=None,
pointActionCallback=None,
pointSymbol=DefaultSymbol,
pointSize=DefaultPointSize,
hexPointColour=DefaultPointColour,
innerRoiPointColour=DefaultInnerPointColour,
**kwds):
super().__init__(setLayout=True, **kwds)
self.application = application
self.project = None
if self.application:
self.project = self.application.project
self._dataFrame = dataFrame
self._roiLimits = DefaultRoiLimits
self._roiDataFrame = None
self.axesDefinitions = axesDefinitions
self.setAxesDefinitions(self.axesDefinitions, updateWidgets=False)
self.pointSymbol = pointSymbol
self.pointSize = pointSize
self.hexPointColour = hexPointColour
self.innerRoiPointColour = innerRoiPointColour
self._scatterView = pg.GraphicsLayoutWidget()
self._scatterView.setBackground(BackgroundColour)
self._plotItem = self._scatterView.addPlot()
self._scatterViewbox = self._plotItem.vb
self._addScatterSelectionBox()
self._scatterViewbox.mouseClickEvent = self._scatterViewboxMouseClickEvent # click on the background canvas
self._scatterViewbox.mouseDragEvent = self._scatterMouseDragEvent
self._scatterViewbox.scene().mouseReleaseEvent = self._scatterMouseReleaseEvent
# self._scatterViewbox.hoverEvent = self._scatterHoverEvent
self._scatterViewbox.scene().sigMouseMoved.connect(self.mouseMoved) #use this if you need the mouse Posit
# self._scatterViewbox.setLimits(**{'xMin':0, 'xMax':1, 'yMin':0, 'yMax':1})
self._plotItem.setMenuEnabled(False)
self._exportDialog = None
self.scatterPlot = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 0, 0))
# self.scatterPlot.sigClicked.connect(self._plotClicked)
self.scatterPlot.mouseClickEvent = self._scatterMouseClickEvent
self.scatterPlot.mouseDoubleClickEvent = self._scatterMouseDoubleClickEvent
setWidgetFont(self)
## adjustable ROI box
self.roiIsLinkedToSelection = True
self.roiItem = ScatterROI(self, *DefaultRoiLimits, pen=ROIPen)
self.roiItem.sigRegionChangeFinished.connect(self._roiChangedCallback)
self._plotItem.addItem(self.roiItem)
self.showROI(roiVisible)
self.tipText = pg.TextItem(anchor=(-0.1, -0.6), angle=0, border='w', fill=(0, 0, 255, 100))
# self.tipText.hide()
self.tipText.setFont(GridFont)
self._plotItem.addItem(self.tipText)
self._plotItem.autoRange()
self.xOriginLine = pg.InfiniteLine(angle=90, pos=0, pen=OriginAxes)
self.yOriginLine = pg.InfiniteLine(angle=0, pos=0, pen=OriginAxes)
self.pointSelectionCallback = pointSelectionCallback # single click
self.pointActionCallback = pointActionCallback # double click
self._plotItem.addItem(self.scatterPlot)
self._plotItem.addItem(self.xOriginLine)
self._plotItem.addItem(self.yOriginLine)
autoBtnFile = os.path.join(ICON_DIR, 'icons/zoom-full.png')
self.autoBtn = self._plotItem.autoBtn = pg.ButtonItem(imageFile=autoBtnFile, width=30, parentItem=self._plotItem)
self._plotItem.updateButtons = lambda: None # just to remove the odd PyQtGraph default behaviour
self.autoBtn.clicked.connect(self._setZoomFull)
self.getLayout().addWidget(self._scatterView)
self.axisSelectionFrame = Frame(self, setLayout=True, grid=(1, 0))
self.axisSelectionFrame.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Minimum)
self._xSelCW = PulldownListCompoundWidget(self.axisSelectionFrame, labelText='Select X-axis',
callback=self._axisSelectionCallback, grid=(0, 0)) #, hAlign='l',)
self._ySelCW = PulldownListCompoundWidget(self.axisSelectionFrame, labelText='Select Y-axis',
callback=self._axisSelectionCallback, grid=(0, 1)) #, hAlign='l',)
self.xAxisSelector = self._xSelCW.pulldownList
self.yAxisSelector = self._ySelCW.pulldownList
# coordinates
self.coordinatesLabel = Label(self.axisSelectionFrame, text='', grid=(0, 2))
# context menu
self.contextMenu = Menu('', None, isFloatWidget=True)
self._setPlotItemFonts()
self.setAxesWidgets()
self._selectedData = []
self._tipTextIsEnabled = True
@property
def dataFrame(self):
return self._dataFrame
@dataFrame.setter
def dataFrame(self, dataFrame):
self._dataFrame = dataFrame
@property
def roiLimits(self) -> list:
"""
:return: a list of 4 elements: [xMin, xMax, yMin, yMax]
"""
self._roiLimits = self.roiItem.getLimits()
return self._roiLimits
@roiLimits.setter
def roiLimits(self, data):
"""
:param data: list of 4 elements: [xMin, xMax, yMin, yMax]
:return: None, set the roiLimits and update its plotItem.
"""
if len(data) != 4:
getLogger().warn('ROI Data must be a list of 4 elements: [xMin, xMax, yMin, yMax]')
return
self._roiLimits = data
self.roiItem.setLimits(*data)
@property
def roiDataFrame(self) -> list:
"""
:return: dataframe from data within the ROI
"""
self._roiDataFrame = self.roiItem.getInnerDataFrame()
return self._roiDataFrame
@property
def selectedData(self):
return self._selectedData
@selectedData.setter
def selectedData(self, data):
"""
:param data: list of series (retrieved from spotItem.data())
set the data as selected and draws the pattern around the spotItem
NB. it use selectedData rather item because an Item can get deleted while redrawing/changing axis.
By adding data, it fires a signal (dataSelectedSignal) that can be used for external callbacks.
"""
self._selectedData = data
# self._selectedData = list(OrderedSet(data))
self._setPointPens(self._getPointPens())
self.dataSelectedSignal.emit(data)
[docs] def setAxesDefinitions(self, defs: od, updateWidgets=True):
"""
:param defs: orderedDict key: visible label to appear in the pulldown, value: the dataframe column header name.
if None, they will be used the header names as they appear in the original dataframe.
"""
if isinstance(defs, dict):
self.axesDefinitions = defs
if defs is None:
self.axesDefinitions = od([k, _ItemBC(k)] for k in self.dataFrame.columns)
if updateWidgets:
self.setAxesWidgets()
[docs] def selectAxes(self, xHeader=None, yHeader=None):
"""
:param x: str, header as appears in the selection Pulldown
:param y: str, header as appears in the selection Pulldown
if None is given, it keeps the current value.
"""
if xHeader:
self.xAxisSelector.select(xHeader)
if yHeader:
self.yAxisSelector.select(yHeader)
[docs] def updatePlot(self):
"""
Redraws all points based on selected axes.
"""
if len(self.dataFrame) == 0: return
self.scatterPlot.clear()
self._setPlotItemLabels()
# brushes = self.getPointBrushes(self.axesDefinitions.get(self.xAxisSelector.getText()))
pens = self._getPointPens()
indices, series = zip(*self.dataFrame.iterrows())
xValues = self._getValuesFromDefition(self.xAxisSelector.getText(), _VALUEHEADER)
yValues = self._getValuesFromDefition(self.yAxisSelector.getText(), _VALUEHEADER)
if len(xValues) == len(yValues):
self.addPoints(x=xValues, y=yValues, size=self.pointSize, symbol=self.pointSymbol,
data=series, pen=pens)
self._updateBrushes() # update colours
self._plotItem.autoRange()
else:
Widgets.MessageDialog.showWarning('Error displaying data', 'Values length mismatch')
# self.scatterPlot.updatePoints()
def _setZoomFull(self, *args):
self._plotItem.autoRange()
[docs] def setEnabledROI(self, enable=True):
return
#todo
if not enable:
self.roiItem.hide()
self.roiItem.setLimits(0, 0, 0, 0)
self.roiItem.sigRegionChangeFinished.disconnect(self._roiChangedCallback)
self.roiItem._isEnabled = False
else:
self.roiItem.show()
self.roiItem.sigRegionChangeFinished.connect(self._roiChangedCallback)
self.roiItem._isEnabled = True
[docs] def setPointColour(self, hex, updatePlot=True, overrideItemDef=False):
self.hexPointColour = hex
if updatePlot:
brushes = self.getPointBrushes(overrideItemDef=overrideItemDef)
if len(brushes) == len(self.scatterPlot.points()):
self.scatterPlot.setBrush(brushes)
[docs] def setInnerPointColour(self, hex, updatePlot=True):
self.innerRoiPointColour = hex
if updatePlot:
brushes = self.getPointBrushes()
self.scatterPlot.setBrush(brushes)
[docs] def setPointSymbol(self, symbol=DefaultSymbol, updatePlot=True):
if symbol in AllowedSymbols:
self.pointSymbol = symbol
if updatePlot:
self.scatterPlot.setSymbol(self.pointSymbol)
[docs] def setPointSize(self, size=10, updatePlot=True):
self.pointSize = size
if updatePlot:
self.scatterPlot.setSize(self.pointSize)
def _setPointPens(self, pens):
if len(pens) == len(self.scatterPlot.points()):
self.scatterPlot.setPen(pens)
[docs] def addPoints(self, x=None, y=None, points=None, **kwargs):
"""
used to add points to the scatterPlot.
If only x,y is given, all the other parameters are set as default.
To set each property use points or define the kwargs (see below).
X,Y can also be define in points or kwargs constructs as "pos" (see below);
in that case is not necessary to set x,y as individual args, and can be left as None.
:param x: 1D arrays of x,y values
:param y: 1D arrays of x,y values
:param points: Optional list of dicts. Each dict specifies parameters for a single point:
{'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternative method
:param kwargs:
x,y 1D arrays of x,y values.
pos 2D structure of x,y pairs (such as Nx2 array or list of tuples)
symbol can be one (or a list) of:
* 'o' circle (default)
* 's' square
* 't' triangle
* 'd' diamond
* '+' plus
pen The pen (or list of pens) to use for drawing point outlines.
brush The brush (or list of brushes) to use for filling points.
size The size (or list of sizes) of points.
data a list of python objects used to uniquely identify each point.
name The name of this item. Names are used for automatically
generating LegendItem entries and by some exporters.
"""
self.scatterPlot.clear()
args = []
if points is not None:
args = [points]
if x is not None and y is not None:
args = [x, y]
if not kwargs.get(_SYMBOL) in AllowedSymbols:
getLogger().warning('Symbol not available. Used the default instead')
kwargs.update({_SYMBOL: 'o'})
self.scatterPlot.addPoints(*args, **kwargs)
self.setPlotLimits()
def _getValuesFromDefition(self, definitionName, definitionValueHeader):
"""
:param definitionName: the _ItemBC name
:param definitionValueHeader: the _ItemBC header values name.
:return:
"""
values = []
_item = self.axesDefinitions.get(definitionName)
dfColumnHeader = getattr(_item, definitionValueHeader)
filteredDf = self.dataFrame.get(dfColumnHeader)
if filteredDf is not None:
values = filteredDf.values
return values
[docs] def setPlotLimits(self, **kwargs):
from ccpn.util.Common import percentage
try:
addPercent = 200
xValues, yValues = self.scatterPlot.getData()
xMin, xMax = np.min(xValues), np.max(xValues)
yMin, yMax = np.min(yValues), np.max(yValues)
deltaX = xMax - xMin
deltaY = yMax - yMin
deltaX += percentage(addPercent, deltaX)
deltaY += percentage(addPercent, deltaY)
except Exception as e:
getLogger().warning("Error in setting scatter plot limits: %s" % e)
return
self._scatterViewbox.setLimits(
xMin=xMin - deltaX,
xMax=xMax + deltaX,
yMin=yMin - deltaY,
yMax=yMax + deltaY,
minXRange=0.01,
maxXRange=max(xValues) * 10,
minYRange=0.01,
maxYRange=max(yValues) * 10,
)
[docs] def getPointBrushes(self, itemDef=None, overrideItemDef=False):
"""
Two way of defining the point colours:
- Global: a unique colour for all point.
global colours are set on init or using the setters.
- Single: one colour for each point.
this option can to be defined in two ways:
* from defining the HEX colours in the input dataframe and
stating the coloumName in the _ItemBC definitions
_HEXCOLOURHEADER : 'theHeaderName',
* from the ccpnObjects property, e.g 'sliceColour'
in this case it is necessary to have a dataframe containing a column with pids,
and objs needs to exist in the project.
also define the object Property and the pidHeader name to use in the _ItemBC definitions:
_PIDHEADER : 'SpectraColumn',
_OBJCOLOURPROPERTY : 'sliceColour',
If points are outside the ROI, then they are a more transparent shade of colour.
:return: list of brushes for painting the scatterPlot points
"""
if itemDef is None:
if len(self.axesDefinitions.values()) > 0:
itemDef = list(self.axesDefinitions.values())[0]
innerData = self.roiItem.getInnerData()
if len(self.dataFrame) == 0: return []
ixs, series = zip(*self.dataFrame.iterrows())
hexs = [self.hexPointColour for i in ixs]
if not overrideItemDef:
if isinstance(itemDef, _ItemBC):
pidHeader = getattr(itemDef, _PIDHEADER)
objColourProperty = getattr(itemDef, _OBJCOLOURPROPERTY)
hexHeader = getattr(itemDef, _HEXCOLOURHEADER)
if objColourProperty is not None: # use the obj for getting the colour info (if defined)
pidDf = self.dataFrame.get(pidHeader)
if pidDf is not None and self.project:
ccpnObjs = _getObjectsByPids(self.project, pidDf.values)
hexs = [getattr(o, objColourProperty) for o in ccpnObjs]
if hexHeader is not None: # use the dedicated colour Header for getting the colour info (if defined)
hexDf = self.dataFrame.get(hexHeader)
if hexDf is not None:
hexDf.fillna(self.hexPointColour, inplace=True)
hexs = hexDf.values
innerNames = np.array([x.name for x in innerData])
_tempColours = hexToRgbaArray(hexs, 100)
innerIndices = np.where(np.in1d(ixs, innerNames))[0]
brushes = []
for i, colour in enumerate(list(_tempColours)):
if i in innerIndices:
colour = colour[:-1]
brushes.append(pg.functions.mkBrush(list(colour)))
return brushes
def _getPointPens(self):
"""
:return: list of pens. Used to draw a selection pattern. [Pen, None] None if no Pen.
"""
if len(self.dataFrame) == 0: return []
indices, series = zip(*self.dataFrame.iterrows())
pens = [SelectedPointPen if i in [j.name for j in self._selectedData] else None for i in indices]
return pens
def _axisSelectionCallback(self, *args):
"""
Callback from pulldown selectors. Adds Points to the plot based on the dataframe columns.
"""
self.updatePlot()
def _setPlotItemLabels(self):
self._plotItem.setLabel('bottom', self.xAxisSelector.getText())
self._plotItem.setLabel('left', self.yAxisSelector.getText())
def _setPlotItemFonts(self):
if self.application:
self._plotItem.getAxis('bottom').setPen(GridPen)
self._plotItem.getAxis('left').setPen(GridPen)
self._plotItem.getAxis('bottom').tickFont = GridFont
self._plotItem.getAxis('left').tickFont = GridFont
###### Context menu setups ######
def _getDefaultMenuItems(self):
"""
Creates default context menu items.
"""
items = [
cm._SCMitem(name='Reset Zoom',
typeItem=cm.ItemTypes.get(cm.ITEM), icon='icons/zoom-full',
toolTip='Reset the plot to default limits',
callback=self._plotItem.autoRange),
cm._SCMitem(name='ROI',
typeItem=cm.ItemTypes.get(cm.ITEM), icon='icons/roi',
toolTip='Toggle ROI',
checkable=True,
callback=self.toggleROI),
cm._SCMitem(name='Select within ROI',
typeItem=cm.ItemTypes.get(cm.ITEM), icon='icons/roi_selection',
toolTip='Select items locatate inside the ROI limits',
callback=self.selectFromROI),
cm._SCMitem(name='Toggle Mouse Text',
typeItem=cm.ItemTypes.get(cm.ITEM), icon=None,
toolTip='Show/hide the mouse coordinates from plot',
callback=self.toggleTipText),
cm._separator(),
]
items = [itm for itm in items if itm is not None]
return items
def _getExportMenuItems(self):
"""
Creates default Export context menu items.
"""
items = [
cm._SCMitem(name='Export image...',
typeItem=cm.ItemTypes.get(cm.ITEM), icon=None,
toolTip='Export image to file.',
callback=partial(self._showExportDialog, self._scatterViewbox)),
]
items = [itm for itm in items if itm is not None]
return items
def _addSubMenus(self, mainMenu):
"""
subclass this to add subMenus
"""
pass
def _createMenu(self, items):
"""
:param items: a list of _SCMitem obj :
:return: Creates and returns a context menu for the guiStrip from a list of items
"""
menu = self.contextMenu = Menu('', self, isFloatWidget=True) # generate new menu
for i in items:
try:
ff = getattr(menu, i.typeItem)
if ff:
action = ff(i.name, **vars(i))
setattr(self, i.stripMethodName, action)
except Exception as e:
getLogger().warning('Menu error: %s' % str(e))
return menu
def _raiseScatterContextMenu(self, ev):
""" Creates all the menu items for the scatter context menu. """
mainMenu = self._createMenu(self.setupContextMenu())
self._addSubMenus(mainMenu)
self.contextMenu.exec_(ev.screenPos().toPoint())
def _showExportDialog(self, viewBox):
"""
:param viewBox: the viewBox obj for the selected plot
:return:
"""
if self._exportDialog is None:
self._exportDialog = CustomExportDialog(viewBox.scene(), titleName='Exporting')
self._exportDialog.show(viewBox)
########### scatter Mouse Events ############
def _scatterViewboxMouseClickEvent(self, event):
""" click on scatter viewBox (the background canvas).
The parent of scatterPlot. Opens the context menu at any point. """
if event.button() == QtCore.Qt.RightButton:
event.accept()
self._raiseScatterContextMenu(event)
def _setCallbackData(self, items, trigger=None):
"""
:param items: list of pg.PointItem type
:param trigger: Any of CallBack _callbackwords:(CLICK, DOUBLECLICK, CURRENT)
:return: CallBack instance (ordered dict type)
"""
data = []
for item in items:
if item is not None:
data.append(CallBack(value=[item.pos().x(), item.pos().y()],
theObject=item,
object=item.data(),
targetName=None,
trigger=trigger,
))
return data
def _scatterMouseDoubleClickEvent(self, ev):
"""
re-implementation of scatter double click even
"""
plot = self.scatterPlot
pts = plot.pointsAt(ev.pos())
if len(pts) > 0:
callbackData = self._setCallbackData(pts, trigger=CallBack.DOUBLECLICK)
if self.pointActionCallback is not None:
self.pointActionCallback(callbackData)
ev.accept()
else:
# "no points, needs to clear selection"
ev.accept()
def _scatterMouseClickEvent(self, ev):
"""
Re-implementation of scatter mouse event to allow selections of a single point
"""
plot = self.scatterPlot
pts = plot.pointsAt(ev.pos())
_data = [pt.data() for pt in pts]
if len(pts) > 0:
callbackData = self._setCallbackData(pts, trigger=CallBack.CLICK)
if me.leftMouse(ev):
if self.pointSelectionCallback is not None:
self.pointSelectionCallback(callbackData)
self._clearScatterSelection()
self.selectedData = _data
# self._setPointPens(self._getPointPens())
# setPens = [pt.setPen(SelectedPointPen) for pt in pts]
ev.accept()
elif me.controlLeftMouse(ev):
# Control-left-click; add to selection
if self.pointSelectionCallback is not None:
self.pointSelectionCallback(callbackData)
self.selectedData += _data
# setPens = [pt.setPen(SelectedPointPen) for pt in pts]
ev.accept()
else:
ev.ignore()
else:
#need to clear selection
self.selectedData = []
# self.scatterPlot.setPen([None]*len(self.dataFrame.index))
ev.accept()
[docs] def mouseMoved(self, event):
"""
use this if you need for example display the mouse coords on display
:param event:
:return:
"""
position = event
if self._tipTextIsEnabled:
if self._scatterViewbox.sceneBoundingRect().contains(position):
mousePoint = self._scatterViewbox.mapSceneToView(position)
x = mousePoint.x()
y = mousePoint.y()
self._showTipTextForPosition(x, y)
else:
self.tipText.hide()
# def _scatterHoverEvent(self, event):
# position = event.pos()
# if self._tipTextIsEnabled:
# if self._scatterViewbox.sceneBoundingRect().contains(position):
# mousePoint = self._scatterViewbox.mapToView(event.pos())
# x = mousePoint.x()
# y = mousePoint.y()
# print(x, y, 'Mouse moved')
# self._showTipTextForPosition(x,y)
# else:
# self.tipText.hide()
def _showTipTextForPosition(self, x, y):
labelPos = "x=%0.2f, y=%0.2f" % (x, y)
pts = self.scatterPlot.pointsAt(pg.Point(x, y))
if len(pts) > 0:
pids = self.getPidsFromPoints(pts)
if any(pids):
pidsLabels = '\n'.join(map(str, pids))
labelPos += '\n'
labelPos += pidsLabels
self.tipText.setText(labelPos)
self.tipText.setPos(x, y)
self.tipText.show()
# def hoverEvent(self, event):
# pass #not needed
def _getDataForPoints(self, points):
return list(map(lambda s: s.data(), points))
def _scatterMouseReleaseEvent(self, event):
"""
re-implementation to allow proper cleaning up of the selection box when releasing the Cmd/Ctrl
button before the mouse button.
"""
self._resetSelectionBox()
pg.GraphicsScene.mouseReleaseEvent(self._scatterViewbox.scene(), event)
def _scatterMouseDragEvent(self, event, *args):
"""
Re-implementation of PyQtGraph mouse drag event to allow custom actions off of different mouse
drag events. Same as spectrum Display. Check Spectrum Display View Box for more documentation.
Known bug: left drag on the axis, raises a pyqtgraph exception
"""
if me.leftMouse(event):
pg.ViewBox.mouseDragEvent(self._scatterViewbox, event)
elif me.controlLeftMouse(event):
self._updateScatterSelectionBox(event.buttonDownPos(), event.pos())
event.accept()
if not event.isFinish():
self._updateScatterSelectionBox(event.buttonDownPos(), event.pos())
else: ## the event is finished.
limits = self._updateScatterSelectionBox(event.buttonDownPos(), event.pos())
selectedData = self._getDataForPoints(_getPointsWithinLimits(self.scatterPlot.points(), limits))
self.selectedData += selectedData
self._resetSelectionBox()
else:
self._resetSelectionBox()
event.ignore()
[docs] def toggleTipText(self):
self._tipTextIsEnabled = not self.tipText.isVisible()
self.tipText.setVisible(not self.tipText.isVisible())
[docs] def toggleROI(self):
"""
show/hide ROI from the plot
"""
self.roiItem.setVisible(not self.roiItem.isVisible())
[docs] def showROI(self, value):
"""
show/hide ROI from the plot
"""
self.roiItem.setVisible(value)
[docs] def selectFromROI(self):
self.selectedData = self.roiItem.getInnerData()
def _addScatterSelectionBox(self):
self._scatterSelectionBox = QtWidgets.QGraphicsRectItem(0, 0, 1, 1)
self._scatterSelectionBox.setPen(pg.functions.mkPen((255, 0, 255), width=1))
self._scatterSelectionBox.setBrush(pg.functions.mkBrush(255, 100, 255, 100))
self._scatterSelectionBox.setZValue(1e9)
self._scatterViewbox.addItem(self._scatterSelectionBox, ignoreBounds=True)
self._scatterSelectionBox.hide()
def _roiChangedCallback(self):
self._updateBrushes()
def _updateBrushes(self):
brushes = self.getPointBrushes()
if len(brushes) == len(self.scatterPlot.points()):
self.scatterPlot.setBrush(brushes)
[docs] def presetROI(self, func=np.std, factor=3):
"""
Apply the function (default np.mean) to the currently displayed plot data
to get the x,y values for setting the ROI box.
:param func: a function applicable to the x,y data
:return: set the ROI on the scatter plot
"""
return
#todo
x, y = self.scatterPlot.getData()
if not len(x) > 0 and not len(y) > 0:
return
xR = func(x)
yR = func(y)
xRange = np.max(x) - np.min(x)
yRange = np.max(y) - np.min(y)
xMin = xR - xperc
yMin = yR - yperc
xMax = xR + xperc
yMax = yR + yperc
self.setROI(xMin, xMax, yMin, yMax)
def _updateScatterSelectionBox(self, p1: float, p2: float):
"""
Updates drawing of selection box as mouse is moved.
"""
vb = self._scatterViewbox
r = QtCore.QRectF(p1, p2)
r = vb.childGroup.mapRectFromParent(r)
self._scatterSelectionBox.setPos(r.topLeft())
self._scatterSelectionBox.resetTransform()
self._scatterSelectionBox.scale(r.width(), r.height())
self._scatterSelectionBox.show()
minX = r.topLeft().x()
minY = r.topLeft().y()
maxX = minX + r.width()
maxY = minY + r.height()
limits = [minX, maxX, minY, maxY]
if self.roiIsLinkedToSelection:
self.roiItem.setLimits(*limits)
return limits
def _resetSelectionBox(self):
"Reset/Hide the boxes "
self._successiveClicks = None
self._scatterSelectionBox.hide()
self._scatterViewbox.rbScaleBox.hide()
def _clearScatterSelection(self):
self._selectedData = []
# self._setPointPens(self._getPointPens())
def _selectScatterPointsFromCurrent(self):
pass
#todo
def _invertScatterSelection(self):
pass
#todo
[docs] def selectByPids(self, pids):
if len(self.axesDefinitions) > 0:
dataToSelect = []
itemDef = list(self.axesDefinitions.values())[0]
pidHeader = getattr(itemDef, _PIDHEADER)
for pid in pids:
for point in self.scatterPlot.points():
pidPoint = point.data().get(pidHeader)
if pid == pidPoint:
dataToSelect.append(point.data())
self.blockSignals(True)
self.selectedData = dataToSelect
self.blockSignals(False)
[docs] def getPidsFromPoints(self, points):
pids = []
if len(self.axesDefinitions) > 0:
itemDef = list(self.axesDefinitions.values())[0]
pidHeader = getattr(itemDef, _PIDHEADER)
for point in points:
pid = point.data().get(pidHeader)
pids.append(pid)
return pids
[docs] def constrainPlot(self, abool):
if not abool:
self._scatterViewbox.setLimits(
xMin=None,
xMax=None,
yMin=None,
yMax=None,
minXRange=None,
maxXRange=None,
minYRange=None,
maxYRange=None,
)
else:
self.setPlotLimits()
if __name__ == '__main__':
from PyQt5 import QtGui, QtWidgets
from ccpn.ui.gui.widgets.Application import TestApplication
from ccpn.ui.gui.widgets.CcpnModuleArea import CcpnModuleArea
from ccpn.ui.gui.modules.CcpnModule import CcpnModule
n = 50
data = pd.DataFrame({
'entryValues' : np.arange(1, n + 1),
'lengthValues' : np.random.rand(n) * 33,
'diameterValues' : np.random.rand(n) / 10,
'heightValues' : np.random.rand(n) * 7.5,
'buggingScoreValues': np.random.rand(n),
# 'SpectraPidsValues': np.array(['SP:LadyBug', 'SP:Lice', 'SP:BedBug', 'SP:Flea', 'SP:Mite']),
# 'HexColoursValues': np.array([list(darkDefaultSpectrumColours.keys())[10]]*n),
})
defs = od([
('#', _ItemBC(
selectorName='#',
headerValueName='entryValues',
headerPidName='SpectraPidsValues',
headerHexColour='HexColoursValues',
)),
('Length', _ItemBC(
selectorName='Length',
headerValueName='lengthValues',
headerPidName='SpectraPidsValues',
headerHexColour='HexColoursValues',
)),
('Score', _ItemBC(
selectorName='Score',
headerValueName='buggingScoreValues',
headerPidName='SpectraPidsValues',
headerHexColour='HexColoursValues',
)),
])
app = TestApplication()
win = QtWidgets.QMainWindow()
moduleArea = CcpnModuleArea(mainWindow=None)
module = CcpnModule(mainWindow=None, name='Testing Module')
moduleArea.addModule(module)
scatterPlot = ScatterPlot(parent=module.mainWidget, application=None,
dataFrame=data, axesDefinitions=defs, roiEnabled=True, grid=(0, 0))
# scatterPlot.setAxesDefinitions(defs)
scatterPlot.selectAxes(xHeader='#', yHeader='Length')
scatterPlot.roiLimits = [0, 1, 0, 1]
scatterPlot.setInnerPointColour('#008000') # green
scatterPlot.setPointSymbol(AllowedSymbols[4])
win.setCentralWidget(moduleArea)
win.resize(1000, 500)
win.setWindowTitle('Testing %s' % module.moduleName)
win.show()
app.start()
win.close()
false = False
if false: # this should never be called from here ### only run on ipythonConsole
from collections import OrderedDict as od
from ccpn.ui.gui.modules.CcpnModule import CcpnModule
from ccpn.ui.gui.widgets.ScatterPlotWidget import ScatterPlot, _ItemBC
import numpy as np
import pandas as pd
n = 5
data = pd.DataFrame({
'Values0' : np.arange(1, n + 1),
'Values1' : np.random.rand(n),
'Values2' : np.random.rand(n),
'SpectraPids': [sp.pid for sp in project.spectra[:5]]
})
defs = od([
('#', _ItemBC(
selectorName='#',
headerValueName='Values0',
headerPidName='SpectraPids',
objColourProperty='positiveContourColour',
)),
('Length', _ItemBC(
selectorName='Length',
headerValueName='Values1',
headerPidName='SpectraPids',
objColourProperty='positiveContourColour',
)),
('Score', _ItemBC(
selectorName='Score',
headerValueName='Values2',
headerPidName='SpectraPids',
objColourProperty='positiveContourColour',
)),
])
module = CcpnModule(mainWindow=mainWindow, name='Testing Module')
scatterPlot = ScatterPlot(parent=module.mainWidget, application=application, dataFrame=data, grid=(0, 0))
scatterPlot.setAxesDefinitions(defs)
scatterPlot.selectAxes(xHeader='#', yHeader='Length')
mainWindow.moduleArea.addModule(module)