Commit 59ed9397 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Fixes for PlotCurveItem, PlotDataItem, ScatterPlotItem.

Made APIs more complete and consistent.
parent 66dd6f97
...@@ -56,7 +56,7 @@ def update(): ...@@ -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)
......
...@@ -22,41 +22,34 @@ class PlotCurveItem(GraphicsObject): ...@@ -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): ...@@ -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): ...@@ -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() self.update()
def setAlpha(self, alpha, auto): def setBrush(self, *args, **kargs):
self.opts['alphaHint'] = alpha self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.opts['alphaMode'] = auto
self.update() self.update()
def setSpectrumMode(self, mode): def setFillLevel(self, level):
self.opts['spectrumMode'] = mode self.opts['fillLevel'] = level
self.xDisp = self.yDisp = None self.fillPath = None
self.path = None
self.update() 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): #def setLogMode(self, mode):
self.opts['logMode'] = mode #self.opts['logMode'] = mode
self.xDisp = self.yDisp = None #self.xDisp = self.yDisp = None
self.path = None #self.path = None
self.update() #self.update()
def setPointMode(self, mode): #def setPointMode(self, mode):
self.opts['pointMode'] = mode #self.opts['pointMode'] = mode
self.update() #self.update()
def setShadowPen(self, pen):
self.shadowPen = fn.mkPen(pen)
self.update()
def setDownsampling(self, ds): #def setDownsampling(self, ds):
if self.opts['downsample'] != ds: #if self.opts['downsample'] != ds:
self.opts['downsample'] = ds #self.opts['downsample'] = ds
self.xDisp = self.yDisp = None #self.xDisp = self.yDisp = None
self.path = None #self.path = None
self.update() #self.update()
def setData(self, x, y, copy=False): def setData(self, *args, **kargs):
"""For Qwt compatibility""" """Same as updateData()"""
self.updateData(y, x, copy) self.updateData(*args, **kargs)
def updateData(self, data, x=None, copy=False): 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): ...@@ -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): ...@@ -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)
......
...@@ -78,9 +78,16 @@ class PlotDataItem(GraphicsObject): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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