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():
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)
......
......@@ -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)
def setShadowPen(self, *args, **kargs):
self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
self.update()
def setAlpha(self, alpha, auto):
self.opts['alphaHint'] = alpha
self.opts['alphaMode'] = auto
def setBrush(self, *args, **kargs):
self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.update()
def setSpectrumMode(self, mode):
self.opts['spectrumMode'] = mode
self.xDisp = self.yDisp = None
self.path = None
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 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 setPointMode(self, mode):
#self.opts['pointMode'] = mode
#self.update()
def setShadowPen(self, pen):
self.shadowPen = fn.mkPen(pen)
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 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 setData(self, *args, **kargs):
"""Same as updateData()"""
self.updateData(*args, **kargs)
def updateData(self, data, x=None, copy=False):
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)
......
......@@ -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() <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() <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__() <pyqtgraph.PlotDataItem.__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
......
......@@ -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):