Fixes for PlotCurveItem, PlotDataItem, ScatterPlotItem.

Made APIs more complete and consistent.
This commit is contained in:
Luke Campagnola 2012-03-18 14:57:36 -04:00
parent 66dd6f974e
commit 59ed9397a3
4 changed files with 601 additions and 301 deletions

View File

@ -56,7 +56,7 @@ def update():
global curve, data, ptr, p6 global curve, data, ptr, p6
curve.setData(data[ptr%10]) curve.setData(data[ptr%10])
if ptr == 0: if ptr == 0:
p6.enableAutoRange('xy', False) p6.enableAutoRange('xy', False) ## stop auto-scaling after the first data set is plotted
ptr += 1 ptr += 1
timer = QtCore.QTimer() timer = QtCore.QTimer()
timer.timeout.connect(update) timer.timeout.connect(update)

View File

@ -22,41 +22,34 @@ class PlotCurveItem(GraphicsObject):
sigPlotChanged = QtCore.Signal(object) sigPlotChanged = QtCore.Signal(object)
sigClicked = QtCore.Signal(object) sigClicked = QtCore.Signal(object)
def __init__(self, y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, color=None, clickable=False): def __init__(self, y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, clickable=False):
GraphicsObject.__init__(self, parent) GraphicsObject.__init__(self, parent)
self.clear() self.clear()
self.path = None self.path = None
self.fillPath = None self.fillPath = None
if pen is None:
if color is None:
self.setPen((200,200,200))
else:
self.setPen(color)
else:
self.setPen(pen)
self.setShadowPen(shadowPen)
if y is not None: if y is not None:
self.updateData(y, x, copy) self.updateData(y, x)
## this is disastrous for performance. ## this is disastrous for performance.
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
self.fillLevel = fillLevel
self.brush = brush
self.metaData = {} self.metaData = {}
self.opts = { self.opts = {
'spectrumMode': False, #'spectrumMode': False,
'logMode': [False, False], #'logMode': [False, False],
'pointMode': False, #'downsample': False,
'pointStyle': None, #'alphaHint': 1.0,
'downsample': False, #'alphaMode': False,
'alphaHint': 1.0, 'pen': 'w',
'alphaMode': False 'shadowPen': None,
'fillLevel': fillLevel,
'brush': brush,
} }
self.setPen(pen)
self.setShadowPen(shadowPen)
self.setFillLevel(fillLevel)
self.setBrush(brush)
self.setClickable(clickable) self.setClickable(clickable)
#self.fps = None #self.fps = None
@ -71,35 +64,36 @@ class PlotCurveItem(GraphicsObject):
def getData(self): def getData(self):
if self.xData is None: return self.xData, self.yData
return (None, None) #if self.xData is None:
if self.xDisp is None: #return (None, None)
nanMask = np.isnan(self.xData) | np.isnan(self.yData) #if self.xDisp is None:
if any(nanMask): #nanMask = np.isnan(self.xData) | np.isnan(self.yData)
x = self.xData[~nanMask] #if any(nanMask):
y = self.yData[~nanMask] #x = self.xData[~nanMask]
else: #y = self.yData[~nanMask]
x = self.xData #else:
y = self.yData #x = self.xData
ds = self.opts['downsample'] #y = self.yData
if ds > 1: #ds = self.opts['downsample']
x = x[::ds] #if ds > 1:
#y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing #x = x[::ds]
y = y[::ds] ##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing
if self.opts['spectrumMode']: #y = y[::ds]
f = fft(y) / len(y) #if self.opts['spectrumMode']:
y = abs(f[1:len(f)/2]) #f = fft(y) / len(y)
dt = x[-1] - x[0] #y = abs(f[1:len(f)/2])
x = np.linspace(0, 0.5*len(x)/dt, len(y)) #dt = x[-1] - x[0]
if self.opts['logMode'][0]: #x = np.linspace(0, 0.5*len(x)/dt, len(y))
x = np.log10(x) #if self.opts['logMode'][0]:
if self.opts['logMode'][1]: #x = np.log10(x)
y = np.log10(y) #if self.opts['logMode'][1]:
self.xDisp = x #y = np.log10(y)
self.yDisp = y #self.xDisp = x
#print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() #self.yDisp = y
#print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() ##print self.yDisp.shape, self.yDisp.min(), self.yDisp.max()
return self.xDisp, self.yDisp ##print self.xDisp.shape, self.xDisp.min(), self.xDisp.max()
#return self.xDisp, self.yDisp
#def generateSpecData(self): #def generateSpecData(self):
#f = fft(self.yData) / len(self.yData) #f = fft(self.yData) / len(self.yData)
@ -124,120 +118,121 @@ class PlotCurveItem(GraphicsObject):
else: else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
def setMeta(self, data): #def setMeta(self, data):
self.metaData = data #self.metaData = data
def meta(self): #def meta(self):
return self.metaData #return self.metaData
def setPen(self, pen): def setPen(self, *args, **kargs):
self.pen = fn.mkPen(pen) self.opts['pen'] = fn.mkPen(*args, **kargs)
self.update() self.update()
def setColor(self, color): def setShadowPen(self, *args, **kargs):
self.pen.setColor(color) self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
self.update()
def setAlpha(self, alpha, auto):
self.opts['alphaHint'] = alpha
self.opts['alphaMode'] = auto
self.update()
def setSpectrumMode(self, mode):
self.opts['spectrumMode'] = mode
self.xDisp = self.yDisp = None
self.path = None
self.update()
def setLogMode(self, mode):
self.opts['logMode'] = mode
self.xDisp = self.yDisp = None
self.path = None
self.update()
def setPointMode(self, mode):
self.opts['pointMode'] = mode
self.update()
def setShadowPen(self, pen):
self.shadowPen = fn.mkPen(pen)
self.update() self.update()
def setDownsampling(self, ds): def setBrush(self, *args, **kargs):
if self.opts['downsample'] != ds: self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.opts['downsample'] = ds self.update()
self.xDisp = self.yDisp = None
self.path = None
self.update()
def setData(self, x, y, copy=False):
"""For Qwt compatibility"""
self.updateData(y, x, copy)
def updateData(self, data, x=None, copy=False): def setFillLevel(self, level):
self.opts['fillLevel'] = level
self.fillPath = None
self.update()
#def setColor(self, color):
#self.pen.setColor(color)
#self.update()
#def setAlpha(self, alpha, auto):
#self.opts['alphaHint'] = alpha
#self.opts['alphaMode'] = auto
#self.update()
#def setSpectrumMode(self, mode):
#self.opts['spectrumMode'] = mode
#self.xDisp = self.yDisp = None
#self.path = None
#self.update()
#def setLogMode(self, mode):
#self.opts['logMode'] = mode
#self.xDisp = self.yDisp = None
#self.path = None
#self.update()
#def setPointMode(self, mode):
#self.opts['pointMode'] = mode
#self.update()
#def setDownsampling(self, ds):
#if self.opts['downsample'] != ds:
#self.opts['downsample'] = ds
#self.xDisp = self.yDisp = None
#self.path = None
#self.update()
def setData(self, *args, **kargs):
"""Same as updateData()"""
self.updateData(*args, **kargs)
def updateData(self, *args, **kargs):
prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) prof = debug.Profiler('PlotCurveItem.updateData', disabled=True)
if isinstance(data, list):
data = np.array(data) if len(args) == 1:
if isinstance(x, list): kargs['y'] = args[0]
x = np.array(x) elif len(args) == 2:
if not isinstance(data, np.ndarray) or data.ndim > 2: kargs['x'] = args[0]
raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape)) kargs['y'] = args[1]
if x == None:
if 'y' not in kargs or kargs['y'] is None:
kargs['y'] = np.array([])
if 'x' not in kargs or kargs['x'] is None:
kargs['x'] = np.arange(len(kargs['y']))
for k in ['x', 'y']:
data = kargs[k]
if isinstance(data, list):
kargs['k'] = np.array(data)
if not isinstance(data, np.ndarray) or data.ndim > 1:
raise Exception("Plot data must be 1D ndarray.")
if 'complex' in str(data.dtype): if 'complex' in str(data.dtype):
raise Exception("Can not plot complex data types.") raise Exception("Can not plot complex data types.")
else:
if 'complex' in str(data.dtype)+str(x.dtype):
raise Exception("Can not plot complex data types.")
if data.ndim == 2: ### If data is 2D array, then assume x and y values are in first two columns or rows.
if x is not None:
raise Exception("Plot data may be 2D only if no x argument is supplied.")
ax = 0
if data.shape[0] > 2 and data.shape[1] == 2:
ax = 1
ind = [slice(None), slice(None)]
ind[ax] = 0
y = data[tuple(ind)]
ind[ax] = 1
x = data[tuple(ind)]
elif data.ndim == 1:
y = data
prof.mark("data checks") prof.mark("data checks")
self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly
## Test this bug with test_PlotWidget and zoom in on the animated plot ## Test this bug with test_PlotWidget and zoom in on the animated plot
self.prepareGeometryChange() self.prepareGeometryChange()
if copy: self.yData = kargs['y'].view(np.ndarray)
self.yData = y.view(np.ndarray).copy() self.xData = kargs['x'].view(np.ndarray)
else:
self.yData = y.view(np.ndarray)
if x is None:
self.xData = np.arange(0, self.yData.shape[0])
else:
if copy:
self.xData = x.view(np.ndarray).copy()
else:
self.xData = x.view(np.ndarray)
prof.mark('copy') prof.mark('copy')
if self.xData.shape != self.yData.shape: if self.xData.shape != self.yData.shape:
raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape)))
self.path = None self.path = None
self.xDisp = self.yDisp = None self.fillPath = None
#self.xDisp = self.yDisp = None
if 'pen' in kargs:
self.setPen(kargs['pen'])
if 'shadowPen' in kargs:
self.setShadowPen(kargs['shadowPen'])
if 'fillLevel' in kargs:
self.setFillLevel(kargs['fillLevel'])
if 'brush' in kargs:
self.setBrush(kargs['brush'])
prof.mark('set') prof.mark('set')
self.update() self.update()
prof.mark('update') prof.mark('update')
#self.emit(QtCore.SIGNAL('plotChanged'), self)
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
prof.mark('emit') prof.mark('emit')
#prof.finish()
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
prof.mark('set cache mode')
prof.finish() prof.finish()
def generatePath(self, x, y): def generatePath(self, x, y):
@ -303,10 +298,10 @@ class PlotCurveItem(GraphicsObject):
return QtCore.QRectF() return QtCore.QRectF()
if self.shadowPen is not None: if self.opts['shadowPen'] is not None:
lineWidth = (max(self.pen.width(), self.shadowPen.width()) + 1) lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
else: else:
lineWidth = (self.pen.width()+1) lineWidth = (self.opts['pen'].width()+1)
pixels = self.pixelVectors() pixels = self.pixelVectors()
@ -343,34 +338,32 @@ class PlotCurveItem(GraphicsObject):
path = self.path path = self.path
prof.mark('generate path') prof.mark('generate path')
if self.brush is not None: if self.opts['brush'] is not None and self.opts['fillLevel'] is not None:
if self.fillPath is None: if self.fillPath is None:
if x is None: if x is None:
x,y = self.getData() x,y = self.getData()
p2 = QtGui.QPainterPath(self.path) p2 = QtGui.QPainterPath(self.path)
p2.lineTo(x[-1], self.fillLevel) p2.lineTo(x[-1], self.opts['fillLevel'])
p2.lineTo(x[0], self.fillLevel) p2.lineTo(x[0], self.opts['fillLevel'])
p2.lineTo(x[0], y[0])
p2.closeSubpath() p2.closeSubpath()
self.fillPath = p2 self.fillPath = p2
p.fillPath(self.fillPath, fn.mkBrush(self.brush)) p.fillPath(self.fillPath, self.opts['brush'])
if self.shadowPen is not None:
sp = QtGui.QPen(self.shadowPen)
else:
sp = None
## Copy pens and apply alpha adjustment ## Copy pens and apply alpha adjustment
cp = QtGui.QPen(self.pen) sp = QtGui.QPen(self.opts['shadowPen'])
for pen in [sp, cp]: cp = QtGui.QPen(self.opts['pen'])
if pen is None: #for pen in [sp, cp]:
continue #if pen is None:
c = pen.color() #continue
c.setAlpha(c.alpha() * self.opts['alphaHint']) #c = pen.color()
pen.setColor(c) #c.setAlpha(c.alpha() * self.opts['alphaHint'])
#pen.setCosmetic(True) #pen.setColor(c)
##pen.setCosmetic(True)
if self.shadowPen is not None: if sp is not None:
p.setPen(sp) p.setPen(sp)
p.drawPath(path) p.drawPath(path)
p.setPen(cp) p.setPen(cp)

View File

@ -78,9 +78,16 @@ class PlotDataItem(GraphicsObject):
self.setFlag(self.ItemHasNoContents) self.setFlag(self.ItemHasNoContents)
self.xData = None self.xData = None
self.yData = None self.yData = None
self.curves = [] self.xDisp = None
self.scatters = [] self.yDisp = None
self.clear() #self.curves = []
#self.scatters = []
self.curve = PlotCurveItem()
self.scatter = ScatterPlotItem()
self.curve.setParentItem(self)
self.scatter.setParentItem(self)
#self.clear()
self.opts = { self.opts = {
'fftMode': False, 'fftMode': False,
'logMode': [False, False], 'logMode': [False, False],
@ -130,17 +137,20 @@ class PlotDataItem(GraphicsObject):
self.opts['pointMode'] = mode self.opts['pointMode'] = mode
self.update() self.update()
def setPen(self, pen): def setPen(self, *args, **kargs):
""" """
| Sets the pen used to draw lines between points. | Sets the pen used to draw lines between points.
| *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() <pyqtgraph.mkPen>` | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() <pyqtgraph.mkPen>`
""" """
self.opts['pen'] = fn.mkPen(pen) pen = fn.mkPen(*args, **kargs)
for c in self.curves: self.opts['pen'] = pen
c.setPen(pen) #self.curve.setPen(pen)
self.update() #for c in self.curves:
#c.setPen(pen)
#self.update()
self.updateItems()
def setShadowPen(self, pen): def setShadowPen(self, *args, **kargs):
""" """
| Sets the shadow pen used to draw lines between points (this is for enhancing contrast or | Sets the shadow pen used to draw lines between points (this is for enhancing contrast or
emphacizing data). emphacizing data).
@ -148,10 +158,46 @@ class PlotDataItem(GraphicsObject):
and should generally be assigned greater width than the primary pen. and should generally be assigned greater width than the primary pen.
| *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() <pyqtgraph.mkPen>` | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() <pyqtgraph.mkPen>`
""" """
pen = fn.mkPen(*args, **kargs)
self.opts['shadowPen'] = pen self.opts['shadowPen'] = pen
for c in self.curves: #for c in self.curves:
c.setPen(pen) #c.setPen(pen)
self.update() #self.update()
self.updateItems()
def setBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs)
self.opts['brush'] = brush
self.updateItems()
def setFillLevel(self, level):
self.opts['fillLevel'] = level
self.updateItems()
def setSymbol(self, symbol):
self.opts['symbol'] = symbol
#self.scatter.setSymbol(symbol)
self.updateItems()
def setSymbolPen(self, *args, **kargs):
pen = fn.mkPen(*args, **kargs)
self.opts['symbolPen'] = pen
#self.scatter.setSymbolPen(pen)
self.updateItems()
def setSymbolBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs)
self.opts['symbolBrush'] = brush
#self.scatter.setSymbolBrush(brush)
self.updateItems()
def setSymbolSize(self, size):
self.opts['symbolSize'] = size
#self.scatter.setSymbolSize(symbolSize)
self.updateItems()
def setDownsampling(self, ds): def setDownsampling(self, ds):
if self.opts['downsample'] != ds: if self.opts['downsample'] != ds:
@ -165,7 +211,7 @@ class PlotDataItem(GraphicsObject):
See :func:`__init__() <pyqtgraph.PlotDataItem.__init__>` for details; it accepts the same arguments. See :func:`__init__() <pyqtgraph.PlotDataItem.__init__>` for details; it accepts the same arguments.
""" """
self.clear() #self.clear()
y = None y = None
x = None x = None
@ -219,7 +265,7 @@ class PlotDataItem(GraphicsObject):
## if symbol pen/brush are given with no symbol, then assume symbol is 'o' ## if symbol pen/brush are given with no symbol, then assume symbol is 'o'
if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs): if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs or 'symbolSize' in kargs):
kargs['symbol'] = 'o' kargs['symbol'] = 'o'
for k in self.opts.keys(): for k in self.opts.keys():
@ -251,6 +297,8 @@ class PlotDataItem(GraphicsObject):
self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by
self.yData = y.view(np.ndarray) self.yData = y.view(np.ndarray)
self.xDisp = None
self.yDisp = None
self.updateItems() self.updateItems()
view = self.getViewBox() view = self.getViewBox()
@ -260,29 +308,37 @@ class PlotDataItem(GraphicsObject):
def updateItems(self): def updateItems(self):
for c in self.curves+self.scatters: #for c in self.curves+self.scatters:
if c.scene() is not None: #if c.scene() is not None:
c.scene().removeItem(c) #c.scene().removeItem(c)
curveArgs = {} curveArgs = {}
for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: for k in ['pen', 'shadowPen', 'fillLevel', 'brush']:
curveArgs[k] = self.opts[k] curveArgs[k] = self.opts[k]
scatterArgs = {} scatterArgs = {}
for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol')]: for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size')]:
scatterArgs[v] = self.opts[k] scatterArgs[v] = self.opts[k]
x,y = self.getData() x,y = self.getData()
self.curve.setData(x=x, y=y, **curveArgs)
if curveArgs['pen'] is not None or curveArgs['brush'] is not None: if curveArgs['pen'] is not None or curveArgs['brush'] is not None:
curve = PlotCurveItem(x=x, y=y, **curveArgs) self.curve.show()
curve.setParentItem(self) else:
self.curves.append(curve) self.curve.hide()
#curve = PlotCurveItem(x=x, y=y, **curveArgs)
#curve.setParentItem(self)
#self.curves.append(curve)
self.scatter.setData(x=x, y=y, **scatterArgs)
if scatterArgs['symbol'] is not None: if scatterArgs['symbol'] is not None:
sp = ScatterPlotItem(x=x, y=y, **scatterArgs) self.scatter.show()
sp.setParentItem(self) else:
self.scatters.append(sp) self.scatter.hide()
#sp = ScatterPlotItem(x=x, y=y, **scatterArgs)
#sp.setParentItem(self)
#self.scatters.append(sp)
def getData(self): def getData(self):
@ -335,15 +391,17 @@ class PlotDataItem(GraphicsObject):
def clear(self): def clear(self):
for i in self.curves+self.scatters: #for i in self.curves+self.scatters:
if i.scene() is not None: #if i.scene() is not None:
i.scene().removeItem(i) #i.scene().removeItem(i)
self.curves = [] #self.curves = []
self.scatters = [] #self.scatters = []
self.xData = None self.xData = None
self.yData = None self.yData = None
self.xDisp = None self.xDisp = None
self.yDisp = None self.yDisp = None
self.curve.setData([])
self.scatter.setData([])
def appendData(self, *args, **kargs): def appendData(self, *args, **kargs):
pass pass

View File

@ -2,6 +2,8 @@ from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point from pyqtgraph.Point import Point
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
from GraphicsObject import GraphicsObject from GraphicsObject import GraphicsObject
import numpy as np
import scipy.stats
__all__ = ['ScatterPlotItem', 'SpotItem'] __all__ = ['ScatterPlotItem', 'SpotItem']
class ScatterPlotItem(GraphicsObject): class ScatterPlotItem(GraphicsObject):
@ -10,74 +12,265 @@ class ScatterPlotItem(GraphicsObject):
sigClicked = QtCore.Signal(object, object) ## self, points sigClicked = QtCore.Signal(object, object) ## self, points
sigPlotChanged = QtCore.Signal(object) sigPlotChanged = QtCore.Signal(object)
def __init__(self, spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=7, def __init__(self, *args, **kargs):
symbol=None, identical=False, data=None):
""" """
Arguments: Accepts the same arguments as setData()
spots: list of dicts. Each dict specifies parameters for a single spot: """
{'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}
x,y: array of x,y values. Alternatively, specify spots['pos'] = (x,y) GraphicsObject.__init__(self)
pxMode: If True, spots are always the same size regardless of scaling, and size is given in px. self.data = None
self.spots = []
self.bounds = [None, None]
self.opts = {}
self.spotsValid = False
self._spotPixmap = None
self.setPen(200,200,200)
self.setBrush(100,100,150)
self.setSymbol('o')
self.setSize(7)
self.setPxMode(True)
self.setIdentical(False)
self.setData(*args, **kargs)
def setData(self, *args, **kargs):
"""
Ordered Arguments:
If there is only one unnamed argument, it will be interpreted like the 'spots' argument.
If there are two unnamed arguments, they will be interpreted as sequences of x and y values.
Keyword Arguments:
*spots*: Optional list of dicts. Each dict specifies parameters for a single spot:
{'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method
of passing in data for the corresponding arguments.
*x*,*y*: 1D arrays of x,y values.
*pos*: 2D structure of x,y pairs (such as Nx2 array or list of tuples)
*pxMode*: If True, spots are always the same size regardless of scaling, and size is given in px.
Otherwise, size is in scene coordinates and the spots scale with the view. Otherwise, size is in scene coordinates and the spots scale with the view.
identical: If True, all spots are forced to look identical. Default is True
*identical*: If True, all spots are forced to look identical.
This can result in performance enhancement. This can result in performance enhancement.
Default is False
symbol can be one of: *symbol* can be one (or a list) of:
'o' circle 'o' circle (default)
's' square 's' square
't' triangle 't' triangle
'd' diamond 'd' diamond
'+' plus '+' plus
*pen*: The pen (or list of pens) to use for drawing spot outlines.
*brush*: The brush (or list of brushes) to use for filling spots.
*size*: The size (or list of sizes) of spots. If *pxMode* is True, this value is in pixels. Otherwise,
it is in the item's local coordinate system.
*data*: a list of python objects used to uniquely identify each spot.
""" """
self.clear()
GraphicsObject.__init__(self) ## deal with non-keyword arguments
self.spots = [] if len(args) == 1:
self.range = [[0,0], [0,0]] kargs['spots'] = args[0]
self.identical = identical elif len(args) == 2:
self._spotPixmap = None kargs['x'] = args[0]
kargs['y'] = args[1]
elif len(args) > 2:
raise Exception('Only accepts up to two non-keyword arguments.')
if brush == 'default': ## convert 'pos' argument to 'x' and 'y'
self.brush = QtGui.QBrush(QtGui.QColor(100, 100, 150)) if 'pos' in kargs:
pos = kargs['pos']
if isinstance(pos, np.ndarray):
kargs['x'] = pos[:,0]
kargs['y'] = pos[:,1]
else:
x = []
y = []
for p in pos:
if isinstance(p, QtCore.QPointF):
x.append(p.x())
y.append(p.y())
else:
x.append(p[0])
y.append(p[1])
kargs['x'] = x
kargs['y'] = y
## determine how many spots we have
if 'spots' in kargs:
numPts = len(kargs['spots'])
elif 'y' in kargs and kargs['y'] is not None:
numPts = len(kargs['y'])
else: else:
self.brush = fn.mkBrush(brush) kargs['x'] = []
kargs['y'] = []
numPts = 0
if pen == 'default': ## create empty record array
self.pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) self.data = np.empty(numPts, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('data', object), ('spot', object)])
else: self.data['size'] = -1 ## indicates use default size
self.pen = fn.mkPen(pen) self.data['symbol'] = ''
self.data['pen'] = None
self.data['brush'] = None
self.data['data'] = None
self.symbol = symbol if 'spots' in kargs:
self.size = size spots = kargs['spots']
for i in xrange(len(spots)):
spot = spots[i]
for k in spot:
if k == 'pen':
self.data[i][k] = fn.mkPen(spot[k])
elif k == 'brush':
self.data[i][k] = fn.mkBrush(spot[k])
elif k == 'pos':
pos = spot[k]
if isinstance(pos, QtCore.QPointF):
x,y = pos.x(), pos.y()
else:
x,y = pos[0], pos[1]
self.data[i]['x'] = x
self.data[i]['y'] = y
elif k in ['x', 'y', 'size', 'symbol', 'data']:
self.data[i][k] = spot[k]
else:
raise Exception("Unknown spot parameter: %s" % k)
elif 'y' in kargs:
self.data['x'] = kargs['x']
self.data['y'] = kargs['y']
self.pxMode = pxMode
if spots is not None or x is not None: ## Set any extra parameters provided in keyword arguments
self.setPoints(spots, x, y, data) for k in ['pxMode', 'identical', 'pen', 'brush', 'symbol', 'size']:
if k in kargs:
#self.optimize = optimize setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
#if optimize: setMethod(kargs[k])
#self.spotImage = QtGui.QImage(size, size, QtGui.QImage.Format_ARGB32_Premultiplied)
#self.spotImage.fill(0)
#p = QtGui.QPainter(self.spotImage)
#p.setRenderHint(p.Antialiasing)
#p.setBrush(brush)
#p.setPen(pen)
#p.drawEllipse(0, 0, size, size)
#p.end()
#self.optimizePixmap = QtGui.QPixmap(self.spotImage)
#self.optimizeFragments = []
#self.setFlags(self.flags() | self.ItemIgnoresTransformations)
self.updateSpots()
#pen = kargs.get('pen', (200,200,200))
#brush = kargs.get('pen', (100,100,150))
#if hasattr(pen, '__len__'):
#pen = map(pg.mkPen(pen))
#self.data['pen'] = pen
#if hasattr(pen, '__len__'):
#brush = map(pg.mkPen(pen))
#self.data['brush'] = pen
#self.data['size'] = kargs.get('size', 7)
#self.data['symbol'] = kargs.get('symbol', 'o')
#if spots is not None and len(spots) > 0:
#spot = spots[0]
#for k in spot:
#self.data[k] = []
#for spot in spots:
#for k,v in spot.iteritems():
#self.data[k].append(v)
def setPoints(self, *args, **kargs):
"""Deprecated; use setData"""
return self.setData(*args, **kargs)
#def setPoints(self, spots=None, x=None, y=None, data=None):
#"""
#Remove all existing points in the scatter plot and add a new set.
#Arguments:
#spots - list of dicts specifying parameters for each spot
#[ {'pos': (x,y), 'pen': 'r', ...}, ...]
#x, y - arrays specifying location of spots to add.
#all other parameters (pen, symbol, etc.) will be set to the default
#values for this scatter plot.
#these arguments are IGNORED if 'spots' is specified
#data - list of arbitrary objects to be assigned to spot.data for each spot
#(this is useful for identifying spots that are clicked on)
#"""
#self.clear()
#self.bounds = [[0,0],[0,0]]
#self.addPoints(spots, x, y, data)
def implements(self, interface=None): def implements(self, interface=None):
ints = ['plotData'] ints = ['plotData']
if interface is None: if interface is None:
return ints return ints
return interface in ints return interface in ints
def setPen(self, *args, **kargs):
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
pens = args[0]
if self.data is None:
raise Exception("Must set data before setting multiple pens.")
if len(pens) != len(self.data):
raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(self.data)))
for i in xrange(len(pens)):
self.data[i]['pen'] = fn.mkPen(pens[i])
else:
self.opts['pen'] = fn.mkPen(*args, **kargs)
self.updateSpots()
def setBrush(self, *args, **kargs):
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
brushes = args[0]
if self.data is None:
raise Exception("Must set data before setting multiple brushes.")
if len(brushes) != len(self.data):
raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(self.data)))
for i in xrange(len(brushes)):
self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs)
else:
self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.updateSpots()
def setSymbol(self, symbol):
if isinstance(symbol, np.ndarray) or isinstance(symbol, list):
symbols = symbol
if self.data is None:
raise Exception("Must set data before setting multiple symbols.")
if len(symbols) != len(self.data):
raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(self.data)))
self.data['symbol'] = symbols
else:
self.opts['symbol'] = symbol
self.updateSpots()
def setSize(self, size):
if isinstance(size, np.ndarray) or isinstance(size, list):
sizes = size
if self.data is None:
raise Exception("Must set data before setting multiple sizes.")
if len(sizes) != len(self.data):
raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(self.data)))
self.data['size'] = sizes
else:
self.opts['size'] = size
self.updateSpots()
def setIdentical(self, ident):
self.opts['identical'] = ident
self.updateSpots()
def setPxMode(self, mode): def setPxMode(self, mode):
self.pxMode = mode self.opts['pxMode'] = mode
self.updateSpots()
def updateSpots(self):
self.spotsValid = False
self.update()
def clear(self): def clear(self):
for i in self.spots: for i in self.spots:
i.setParentItem(None) i.setParentItem(None)
@ -85,73 +278,113 @@ class ScatterPlotItem(GraphicsObject):
if s is not None: if s is not None:
s.removeItem(i) s.removeItem(i)
self.spots = [] self.spots = []
self.data = None
self.spotsValid = False
self.bounds = [None, None]
def getRange(self, ax, percent): def dataBounds(self, ax, frac=1.0):
return self.range[ax] if frac >= 1.0 and self.bounds[ax] is not None:
return self.bounds[ax]
def setPoints(self, spots=None, x=None, y=None, data=None): if self.data is None or len(self.data) == 0:
""" return (None, None)
Remove all existing points in the scatter plot and add a new set.
Arguments: if ax == 0:
spots - list of dicts specifying parameters for each spot d = self.data['x']
[ {'pos': (x,y), 'pen': 'r', ...}, ...] elif ax == 1:
x, y - arrays specifying location of spots to add. d = self.data['y']
all other parameters (pen, symbol, etc.) will be set to the default
values for this scatter plot. if frac >= 1.0:
these arguments are IGNORED if 'spots' is specified minIndex = np.argmin(d)
data - list of arbitrary objects to be assigned to spot.data for each spot maxIndex = np.argmax(d)
(this is useful for identifying spots that are clicked on) minVal = d[minIndex]
""" maxVal = d[maxIndex]
self.clear() if not self.opts['pxMode']:
self.range = [[0,0],[0,0]] minVal -= self.data[minIndex]['size']
self.addPoints(spots, x, y, data) maxVal += self.data[maxIndex]['size']
self.bounds[ax] = (minVal, maxVal)
def addPoints(self, spots=None, x=None, y=None, data=None): return self.bounds[ax]
xmn = ymn = xmx = ymx = None elif frac <= 0.0:
if spots is not None: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
n = len(spots)
else: else:
n = len(x) return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
def addPoints(self, *args, **kargs):
"""
Add new points to the scatter plot.
Arguments are the same as setData()
Note: this is expensive; plenty of room for optimization here.
"""
if self.data is None:
self.setData(*args, **kargs)
return
for i in range(n): data1 = self.data[:]
if spots is not None: #range1 = [self.bounds[0][:], self.bounds[1][:]]
s = spots[i] self.setData(*args, **kargs)
pos = Point(s['pos']) newData = np.empty(len(self.data) + len(data1), dtype=self.data.dtype)
else: newData[:len(data1)] = data1
s = {} newData[len(data1):] = self.data
pos = Point(x[i], y[i]) #self.bounds = [
if data is not None: #[min(self.bounds[0][0], range1[0][0]), max(self.bounds[0][1], range1[0][1])],
s['data'] = data[i] #[min(self.bounds[1][0], range1[1][0]), max(self.bounds[1][1], range1[1][1])],
#]
size = s.get('size', self.size) self.data = newData
if self.pxMode: self.sigPlotChanged.emit(self)
def generateSpots(self):
xmn = ymn = xmx = ymx = None
## apply defaults
size = self.data['size'].copy()
size[size<0] = self.opts['size']
pen = self.data['pen'].copy()
pen[pen<0] = self.opts['pen'] ## note pen<0 checks for pen==None
brush = self.data['brush'].copy()
brush[brush<0] = self.opts['brush']
symbol = self.data['symbol'].copy()
symbol[symbol==''] = self.opts['symbol']
for i in xrange(len(self.data)):
s = self.data[i]
pos = Point(s['x'], s['y'])
if self.opts['pxMode']:
psize = 0 psize = 0
else: else:
psize = size psize = size[i]
if xmn is None:
xmn = pos[0]-psize #if xmn is None:
xmx = pos[0]+psize #xmn = pos[0]-psize
ymn = pos[1]-psize #xmx = pos[0]+psize
ymx = pos[1]+psize #ymn = pos[1]-psize
else: #ymx = pos[1]+psize
xmn = min(xmn, pos[0]-psize) #else:
xmx = max(xmx, pos[0]+psize) #xmn = min(xmn, pos[0]-psize)
ymn = min(ymn, pos[1]-psize) #xmx = max(xmx, pos[0]+psize)
ymx = max(ymx, pos[1]+psize) #ymn = min(ymn, pos[1]-psize)
#print pos, xmn, xmx, ymn, ymx #ymx = max(ymx, pos[1]+psize)
brush = s.get('brush', self.brush)
pen = s.get('pen', self.pen) item = self.mkSpot(pos, size[i], self.opts['pxMode'], brush[i], pen[i], s['data'], symbol=symbol[i], index=len(self.spots))
pen.setCosmetic(True)
symbol = s.get('symbol', self.symbol)
data2 = s.get('data', None)
item = self.mkSpot(pos, size, self.pxMode, brush, pen, data2, symbol=symbol, index=len(self.spots))
self.spots.append(item) self.spots.append(item)
self.data[i]['spot'] = item
#if self.optimize: #if self.optimize:
#item.hide() #item.hide()
#frag = QtGui.QPainter.PixmapFragment.create(pos, QtCore.QRectF(0, 0, size, size)) #frag = QtGui.QPainter.PixmapFragment.create(pos, QtCore.QRectF(0, 0, size, size))
#self.optimizeFragments.append(frag) #self.optimizeFragments.append(frag)
self.range = [[xmn, xmx], [ymn, ymx]]
#self.bounds = [[xmn, xmx], [ymn, ymx]]
self.spotsValid = True
self.sigPlotChanged.emit(self)
#def setPointSize(self, size): #def setPointSize(self, size):
#for s in self.spots: #for s in self.spots:
@ -166,24 +399,22 @@ class ScatterPlotItem(GraphicsObject):
#p.drawPixmapFragments(self.optimizeFragments, self.optimizePixmap) #p.drawPixmapFragments(self.optimizeFragments, self.optimizePixmap)
def paint(self, *args): def paint(self, *args):
pass if not self.spotsValid:
self.generateSpots()
def spotPixmap(self): def spotPixmap(self):
## If all spots are identical, return the pixmap to use for all spots ## If all spots are identical, return the pixmap to use for all spots
## Otherwise return None ## Otherwise return None
if not self.identical: if not self.opts['identical']:
return None return None
if self._spotPixmap is None: if self._spotPixmap is None:
#print 'spotPixmap' spot = SpotItem(size=self.opts['size'], pxMode=True, brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
spot = SpotItem(size=self.size, pxMode=True, brush=self.brush, pen=self.pen, symbol=self.symbol)
#self._spotPixmap = PixmapSpotItem.makeSpotImage(self.size, self.pen, self.brush, self.symbol)
self._spotPixmap = spot.pixmap self._spotPixmap = spot.pixmap
return self._spotPixmap return self._spotPixmap
def mkSpot(self, pos, size, pxMode, brush, pen, data, symbol=None, index=None): def mkSpot(self, pos, size, pxMode, brush, pen, data, symbol=None, index=None):
## Make and return a SpotItem (or PixmapSpotItem if in pxMode) ## Make and return a SpotItem (or PixmapSpotItem if in pxMode)
brush = fn.mkBrush(brush) brush = fn.mkBrush(brush)
pen = fn.mkPen(pen) pen = fn.mkPen(pen)
if pxMode: if pxMode:
@ -198,10 +429,19 @@ class ScatterPlotItem(GraphicsObject):
return item return item
def boundingRect(self): def boundingRect(self):
((xmn, xmx), (ymn, ymx)) = self.range (xmn, xmx) = self.dataBounds(ax=0)
if xmn is None or xmx is None or ymn is None or ymx is None: (ymn, ymx) = self.dataBounds(ax=1)
return QtCore.QRectF() if xmn is None or xmx is None:
xmn = 0
xmx = 0
if ymn is None or ymx is None:
ymn = 0
ymx = 0
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
#if xmn is None or xmx is None or ymn is None or ymx is None:
#return QtCore.QRectF()
#return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
#return QtCore.QRectF(xmn-1, ymn-1, xmx-xmn+2, ymx-ymn+2) #return QtCore.QRectF(xmn-1, ymn-1, xmx-xmn+2, ymx-ymn+2)
#def pointClicked(self, point): #def pointClicked(self, point):
@ -222,7 +462,7 @@ class ScatterPlotItem(GraphicsObject):
sx = sp.x() sx = sp.x()
sy = sp.y() sy = sp.y()
s2x = s2y = ss * 0.5 s2x = s2y = ss * 0.5
if self.pxMode: if self.opts['pxMode']:
s2x *= pw s2x *= pw
s2y *= ph s2y *= ph
if x > sx-s2x and x < sx+s2x and y > sy-s2y and y < sy+s2y: if x > sx-s2x and x < sx+s2x and y > sy-s2y and y < sy+s2y:
@ -281,10 +521,18 @@ class SpotItem(GraphicsObject):
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
self.pxMode = pxMode self.pxMode = pxMode
try:
symbol = int(symbol)
except:
pass
if symbol is None: if symbol is None:
symbol = 'o' ## circle by default symbol = 'o' ## circle by default
elif isinstance(symbol, int): ## allow symbols specified by integer for easy iteration elif isinstance(symbol, int): ## allow symbols specified by integer for easy iteration
symbol = ['o', 's', 't', 'd', '+'][symbol] symbol = ['o', 's', 't', 'd', '+'][symbol]
####print 'SpotItem symbol: ', symbol ####print 'SpotItem symbol: ', symbol
self.data = data self.data = data
self.pen = pen self.pen = pen
@ -294,11 +542,12 @@ class SpotItem(GraphicsObject):
self.symbol = symbol self.symbol = symbol
#s2 = size/2. #s2 = size/2.
self.path = QtGui.QPainterPath() self.path = QtGui.QPainterPath()
if symbol == 'o': if symbol == 'o':
self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
elif symbol == 's': elif symbol == 's':
self.path.addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) self.path.addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
elif symbol is 't' or symbol is '^': elif symbol == 't' or symbol == '^':
self.path.moveTo(-0.5, -0.5) self.path.moveTo(-0.5, -0.5)
self.path.lineTo(0, 0.5) self.path.lineTo(0, 0.5)
self.path.lineTo(0.5, -0.5) self.path.lineTo(0.5, -0.5)
@ -328,7 +577,7 @@ class SpotItem(GraphicsObject):
#self.path.connectPath(self.path) #self.path.connectPath(self.path)
#elif symbol == 'x': #elif symbol == 'x':
else: else:
raise Exception("Unknown spot symbol '%s'" % symbol) raise Exception("Unknown spot symbol '%s' (type=%s)" % (str(symbol), str(type(symbol))))
#self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) #self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
if pxMode: if pxMode: