From 59ed9397a3758339cbcd63314db50b621bf74f8c Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 18 Mar 2012 14:57:36 -0400 Subject: [PATCH] Fixes for PlotCurveItem, PlotDataItem, ScatterPlotItem. Made APIs more complete and consistent. --- examples/Plotting.py | 2 +- graphicsItems/PlotCurveItem.py | 311 ++++++++++---------- graphicsItems/PlotDataItem.py | 116 ++++++-- graphicsItems/ScatterPlotItem.py | 473 +++++++++++++++++++++++-------- 4 files changed, 601 insertions(+), 301 deletions(-) diff --git a/examples/Plotting.py b/examples/Plotting.py index 6f2516b8..79d0d4ac 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -56,7 +56,7 @@ def update(): global curve, data, ptr, p6 curve.setData(data[ptr%10]) if ptr == 0: - p6.enableAutoRange('xy', False) + p6.enableAutoRange('xy', False) ## stop auto-scaling after the first data set is plotted ptr += 1 timer = QtCore.QTimer() timer.timeout.connect(update) diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index 505333b9..8f6bacd7 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -22,41 +22,34 @@ class PlotCurveItem(GraphicsObject): sigPlotChanged = 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) self.clear() self.path = 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: - self.updateData(y, x, copy) + self.updateData(y, x) ## this is disastrous for performance. #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - self.fillLevel = fillLevel - self.brush = brush - self.metaData = {} self.opts = { - 'spectrumMode': False, - 'logMode': [False, False], - 'pointMode': False, - 'pointStyle': None, - 'downsample': False, - 'alphaHint': 1.0, - 'alphaMode': False + #'spectrumMode': False, + #'logMode': [False, False], + #'downsample': False, + #'alphaHint': 1.0, + #'alphaMode': False, + 'pen': 'w', + 'shadowPen': None, + 'fillLevel': fillLevel, + 'brush': brush, } - + self.setPen(pen) + self.setShadowPen(shadowPen) + self.setFillLevel(fillLevel) + self.setBrush(brush) self.setClickable(clickable) #self.fps = None @@ -71,35 +64,36 @@ class PlotCurveItem(GraphicsObject): def getData(self): - if self.xData is None: - return (None, None) - if self.xDisp is None: - nanMask = np.isnan(self.xData) | np.isnan(self.yData) - if any(nanMask): - x = self.xData[~nanMask] - y = self.yData[~nanMask] - else: - x = self.xData - y = self.yData - ds = self.opts['downsample'] - if ds > 1: - x = x[::ds] - #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing - y = y[::ds] - if self.opts['spectrumMode']: - f = fft(y) / len(y) - y = abs(f[1:len(f)/2]) - dt = x[-1] - x[0] - x = np.linspace(0, 0.5*len(x)/dt, len(y)) - if self.opts['logMode'][0]: - x = np.log10(x) - if self.opts['logMode'][1]: - y = np.log10(y) - self.xDisp = x - self.yDisp = y - #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() - #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() - return self.xDisp, self.yDisp + return self.xData, self.yData + #if self.xData is None: + #return (None, None) + #if self.xDisp is None: + #nanMask = np.isnan(self.xData) | np.isnan(self.yData) + #if any(nanMask): + #x = self.xData[~nanMask] + #y = self.yData[~nanMask] + #else: + #x = self.xData + #y = self.yData + #ds = self.opts['downsample'] + #if ds > 1: + #x = x[::ds] + ##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing + #y = y[::ds] + #if self.opts['spectrumMode']: + #f = fft(y) / len(y) + #y = abs(f[1:len(f)/2]) + #dt = x[-1] - x[0] + #x = np.linspace(0, 0.5*len(x)/dt, len(y)) + #if self.opts['logMode'][0]: + #x = np.log10(x) + #if self.opts['logMode'][1]: + #y = np.log10(y) + #self.xDisp = x + #self.yDisp = y + ##print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() + ##print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() + #return self.xDisp, self.yDisp #def generateSpecData(self): #f = fft(self.yData) / len(self.yData) @@ -124,120 +118,121 @@ class PlotCurveItem(GraphicsObject): else: return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - def setMeta(self, data): - self.metaData = data + #def setMeta(self, data): + #self.metaData = data - def meta(self): - return self.metaData + #def meta(self): + #return self.metaData - def setPen(self, pen): - self.pen = fn.mkPen(pen) + def setPen(self, *args, **kargs): + self.opts['pen'] = fn.mkPen(*args, **kargs) 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 setShadowPen(self, pen): - self.shadowPen = fn.mkPen(pen) + def setShadowPen(self, *args, **kargs): + self.opts['shadowPen'] = fn.mkPen(*args, **kargs) 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, x, y, copy=False): - """For Qwt compatibility""" - self.updateData(y, x, copy) + def setBrush(self, *args, **kargs): + self.opts['brush'] = fn.mkBrush(*args, **kargs) + self.update() - 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) - if isinstance(data, list): - data = np.array(data) - if isinstance(x, list): - x = np.array(x) - if not isinstance(data, np.ndarray) or data.ndim > 2: - raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape)) - if x == None: + + if len(args) == 1: + kargs['y'] = args[0] + elif len(args) == 2: + kargs['x'] = args[0] + kargs['y'] = args[1] + + 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): 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") - 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 - self.prepareGeometryChange() - if copy: - self.yData = y.view(np.ndarray).copy() - 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) + self.yData = kargs['y'].view(np.ndarray) + self.xData = kargs['x'].view(np.ndarray) + prof.mark('copy') - 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))) 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') self.update() prof.mark('update') - #self.emit(QtCore.SIGNAL('plotChanged'), self) self.sigPlotChanged.emit(self) prof.mark('emit') - #prof.finish() - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - prof.mark('set cache mode') prof.finish() def generatePath(self, x, y): @@ -303,10 +298,10 @@ class PlotCurveItem(GraphicsObject): return QtCore.QRectF() - if self.shadowPen is not None: - lineWidth = (max(self.pen.width(), self.shadowPen.width()) + 1) + if self.opts['shadowPen'] is not None: + lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) else: - lineWidth = (self.pen.width()+1) + lineWidth = (self.opts['pen'].width()+1) pixels = self.pixelVectors() @@ -343,34 +338,32 @@ class PlotCurveItem(GraphicsObject): path = self.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 x is None: x,y = self.getData() p2 = QtGui.QPainterPath(self.path) - p2.lineTo(x[-1], self.fillLevel) - p2.lineTo(x[0], self.fillLevel) + p2.lineTo(x[-1], self.opts['fillLevel']) + p2.lineTo(x[0], self.opts['fillLevel']) + p2.lineTo(x[0], y[0]) p2.closeSubpath() 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 - cp = QtGui.QPen(self.pen) - for pen in [sp, cp]: - if pen is None: - continue - c = pen.color() - c.setAlpha(c.alpha() * self.opts['alphaHint']) - pen.setColor(c) - #pen.setCosmetic(True) + sp = QtGui.QPen(self.opts['shadowPen']) + cp = QtGui.QPen(self.opts['pen']) + #for pen in [sp, cp]: + #if pen is None: + #continue + #c = pen.color() + #c.setAlpha(c.alpha() * self.opts['alphaHint']) + #pen.setColor(c) + ##pen.setCosmetic(True) - if self.shadowPen is not None: + if sp is not None: p.setPen(sp) p.drawPath(path) p.setPen(cp) diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index 91ea77a7..abb197eb 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -78,9 +78,16 @@ class PlotDataItem(GraphicsObject): self.setFlag(self.ItemHasNoContents) self.xData = None self.yData = None - self.curves = [] - self.scatters = [] - self.clear() + self.xDisp = None + self.yDisp = None + #self.curves = [] + #self.scatters = [] + self.curve = PlotCurveItem() + self.scatter = ScatterPlotItem() + self.curve.setParentItem(self) + self.scatter.setParentItem(self) + + #self.clear() self.opts = { 'fftMode': False, 'logMode': [False, False], @@ -130,17 +137,20 @@ class PlotDataItem(GraphicsObject): self.opts['pointMode'] = mode self.update() - def setPen(self, pen): + def setPen(self, *args, **kargs): """ | Sets the pen used to draw lines between points. | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() ` """ - self.opts['pen'] = fn.mkPen(pen) - for c in self.curves: - c.setPen(pen) - self.update() + pen = fn.mkPen(*args, **kargs) + self.opts['pen'] = pen + #self.curve.setPen(pen) + #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 emphacizing data). @@ -148,10 +158,46 @@ class PlotDataItem(GraphicsObject): and should generally be assigned greater width than the primary pen. | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() ` """ + pen = fn.mkPen(*args, **kargs) self.opts['shadowPen'] = pen - for c in self.curves: - c.setPen(pen) - self.update() + #for c in self.curves: + #c.setPen(pen) + #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): if self.opts['downsample'] != ds: @@ -165,7 +211,7 @@ class PlotDataItem(GraphicsObject): See :func:`__init__() ` for details; it accepts the same arguments. """ - self.clear() + #self.clear() y = 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' 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' 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.yData = y.view(np.ndarray) + self.xDisp = None + self.yDisp = None self.updateItems() view = self.getViewBox() @@ -260,29 +308,37 @@ class PlotDataItem(GraphicsObject): def updateItems(self): - for c in self.curves+self.scatters: - if c.scene() is not None: - c.scene().removeItem(c) + #for c in self.curves+self.scatters: + #if c.scene() is not None: + #c.scene().removeItem(c) curveArgs = {} for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: curveArgs[k] = self.opts[k] 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] x,y = self.getData() + self.curve.setData(x=x, y=y, **curveArgs) if curveArgs['pen'] is not None or curveArgs['brush'] is not None: - curve = PlotCurveItem(x=x, y=y, **curveArgs) - curve.setParentItem(self) - self.curves.append(curve) + self.curve.show() + else: + 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: - sp = ScatterPlotItem(x=x, y=y, **scatterArgs) - sp.setParentItem(self) - self.scatters.append(sp) + self.scatter.show() + else: + self.scatter.hide() + #sp = ScatterPlotItem(x=x, y=y, **scatterArgs) + #sp.setParentItem(self) + #self.scatters.append(sp) def getData(self): @@ -335,15 +391,17 @@ class PlotDataItem(GraphicsObject): def clear(self): - for i in self.curves+self.scatters: - if i.scene() is not None: - i.scene().removeItem(i) - self.curves = [] - self.scatters = [] + #for i in self.curves+self.scatters: + #if i.scene() is not None: + #i.scene().removeItem(i) + #self.curves = [] + #self.scatters = [] self.xData = None self.yData = None self.xDisp = None self.yDisp = None + self.curve.setData([]) + self.scatter.setData([]) def appendData(self, *args, **kargs): pass diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index 85427eb4..f40db2fa 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -2,6 +2,8 @@ from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Point import Point import pyqtgraph.functions as fn from GraphicsObject import GraphicsObject +import numpy as np +import scipy.stats __all__ = ['ScatterPlotItem', 'SpotItem'] class ScatterPlotItem(GraphicsObject): @@ -10,74 +12,265 @@ class ScatterPlotItem(GraphicsObject): sigClicked = QtCore.Signal(object, object) ## self, points sigPlotChanged = QtCore.Signal(object) - def __init__(self, spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=7, - symbol=None, identical=False, data=None): - + def __init__(self, *args, **kargs): """ - Arguments: - 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) - pxMode: If True, spots are always the same size regardless of scaling, and size is given in px. + Accepts the same arguments as setData() + """ + + GraphicsObject.__init__(self) + 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. - 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. - - symbol can be one of: - 'o' circle + Default is False + *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 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) - self.spots = [] - self.range = [[0,0], [0,0]] - self.identical = identical - self._spotPixmap = None + ## deal with non-keyword arguments + if len(args) == 1: + kargs['spots'] = args[0] + elif len(args) == 2: + 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': - self.brush = QtGui.QBrush(QtGui.QColor(100, 100, 150)) + ## convert 'pos' argument to 'x' and 'y' + 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: - self.brush = fn.mkBrush(brush) + kargs['x'] = [] + kargs['y'] = [] + numPts = 0 - if pen == 'default': - self.pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) - else: - self.pen = fn.mkPen(pen) + ## create empty record array + self.data = np.empty(numPts, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('data', object), ('spot', object)]) + self.data['size'] = -1 ## indicates use default size + self.data['symbol'] = '' + self.data['pen'] = None + self.data['brush'] = None + self.data['data'] = None - self.symbol = symbol - self.size = size + if 'spots' in kargs: + 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: - self.setPoints(spots, x, y, data) - - #self.optimize = optimize - #if optimize: - #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) + + ## Set any extra parameters provided in keyword arguments + for k in ['pxMode', 'identical', 'pen', 'brush', 'symbol', 'size']: + if k in kargs: + setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) + setMethod(kargs[k]) + 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): ints = ['plotData'] if interface is None: return 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): - self.pxMode = mode - + self.opts['pxMode'] = mode + self.updateSpots() + + def updateSpots(self): + self.spotsValid = False + self.update() + def clear(self): for i in self.spots: i.setParentItem(None) @@ -85,73 +278,113 @@ class ScatterPlotItem(GraphicsObject): if s is not None: s.removeItem(i) self.spots = [] + self.data = None + self.spotsValid = False + self.bounds = [None, None] - def getRange(self, ax, percent): - return self.range[ax] + def dataBounds(self, ax, frac=1.0): + 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): - """ - 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.range = [[0,0],[0,0]] - self.addPoints(spots, x, y, data) - - def addPoints(self, spots=None, x=None, y=None, data=None): - xmn = ymn = xmx = ymx = None - if spots is not None: - n = len(spots) + if self.data is None or len(self.data) == 0: + return (None, None) + + if ax == 0: + d = self.data['x'] + elif ax == 1: + d = self.data['y'] + + if frac >= 1.0: + minIndex = np.argmin(d) + maxIndex = np.argmax(d) + minVal = d[minIndex] + maxVal = d[maxIndex] + if not self.opts['pxMode']: + minVal -= self.data[minIndex]['size'] + maxVal += self.data[maxIndex]['size'] + self.bounds[ax] = (minVal, maxVal) + return self.bounds[ax] + elif frac <= 0.0: + raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) 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): - if spots is not None: - s = spots[i] - pos = Point(s['pos']) - else: - s = {} - pos = Point(x[i], y[i]) - if data is not None: - s['data'] = data[i] - - size = s.get('size', self.size) - if self.pxMode: + data1 = self.data[:] + #range1 = [self.bounds[0][:], self.bounds[1][:]] + self.setData(*args, **kargs) + newData = np.empty(len(self.data) + len(data1), dtype=self.data.dtype) + newData[:len(data1)] = data1 + newData[len(data1):] = self.data + #self.bounds = [ + #[min(self.bounds[0][0], range1[0][0]), max(self.bounds[0][1], range1[0][1])], + #[min(self.bounds[1][0], range1[1][0]), max(self.bounds[1][1], range1[1][1])], + #] + self.data = newData + 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 else: - psize = size - if xmn is None: - xmn = pos[0]-psize - xmx = pos[0]+psize - ymn = pos[1]-psize - ymx = pos[1]+psize - else: - xmn = min(xmn, pos[0]-psize) - xmx = max(xmx, pos[0]+psize) - ymn = min(ymn, pos[1]-psize) - ymx = max(ymx, pos[1]+psize) - #print pos, xmn, xmx, ymn, ymx - brush = s.get('brush', self.brush) - pen = s.get('pen', self.pen) - 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)) + psize = size[i] + + #if xmn is None: + #xmn = pos[0]-psize + #xmx = pos[0]+psize + #ymn = pos[1]-psize + #ymx = pos[1]+psize + #else: + #xmn = min(xmn, pos[0]-psize) + #xmx = max(xmx, pos[0]+psize) + #ymn = min(ymn, pos[1]-psize) + #ymx = max(ymx, pos[1]+psize) + + item = self.mkSpot(pos, size[i], self.opts['pxMode'], brush[i], pen[i], s['data'], symbol=symbol[i], index=len(self.spots)) self.spots.append(item) + self.data[i]['spot'] = item #if self.optimize: #item.hide() #frag = QtGui.QPainter.PixmapFragment.create(pos, QtCore.QRectF(0, 0, size, size)) #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): #for s in self.spots: @@ -166,24 +399,22 @@ class ScatterPlotItem(GraphicsObject): #p.drawPixmapFragments(self.optimizeFragments, self.optimizePixmap) def paint(self, *args): - pass + if not self.spotsValid: + self.generateSpots() def spotPixmap(self): ## If all spots are identical, return the pixmap to use for all spots ## Otherwise return None - if not self.identical: + if not self.opts['identical']: return None if self._spotPixmap is None: - #print 'spotPixmap' - 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) + spot = SpotItem(size=self.opts['size'], pxMode=True, brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol']) self._spotPixmap = spot.pixmap return self._spotPixmap def mkSpot(self, pos, size, pxMode, brush, pen, data, symbol=None, index=None): ## Make and return a SpotItem (or PixmapSpotItem if in pxMode) - brush = fn.mkBrush(brush) pen = fn.mkPen(pen) if pxMode: @@ -198,10 +429,19 @@ class ScatterPlotItem(GraphicsObject): return item def boundingRect(self): - ((xmn, xmx), (ymn, ymx)) = self.range - if xmn is None or xmx is None or ymn is None or ymx is None: - return QtCore.QRectF() + (xmn, xmx) = self.dataBounds(ax=0) + (ymn, ymx) = self.dataBounds(ax=1) + 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) + + #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) #def pointClicked(self, point): @@ -222,7 +462,7 @@ class ScatterPlotItem(GraphicsObject): sx = sp.x() sy = sp.y() s2x = s2y = ss * 0.5 - if self.pxMode: + if self.opts['pxMode']: s2x *= pw s2y *= ph 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) self.pxMode = pxMode + try: + symbol = int(symbol) + except: + pass + if symbol is None: symbol = 'o' ## circle by default elif isinstance(symbol, int): ## allow symbols specified by integer for easy iteration symbol = ['o', 's', 't', 'd', '+'][symbol] + + + ####print 'SpotItem symbol: ', symbol self.data = data self.pen = pen @@ -294,11 +542,12 @@ class SpotItem(GraphicsObject): self.symbol = symbol #s2 = size/2. self.path = QtGui.QPainterPath() + if symbol == 'o': self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) elif symbol == 's': 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.lineTo(0, 0.5) self.path.lineTo(0.5, -0.5) @@ -328,7 +577,7 @@ class SpotItem(GraphicsObject): #self.path.connectPath(self.path) #elif symbol == 'x': 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)) if pxMode: