2012-03-02 02:55:32 +00:00
from pyqtgraph . Qt import QtGui , QtCore
from scipy . fftpack import fft
import numpy as np
import scipy . stats
2012-05-11 22:05:41 +00:00
from . GraphicsObject import GraphicsObject
2012-03-02 02:55:32 +00:00
import pyqtgraph . functions as fn
from pyqtgraph import debug
from pyqtgraph . Point import Point
2012-05-11 22:05:41 +00:00
import struct , sys
2012-03-02 02:55:32 +00:00
__all__ = [ ' PlotCurveItem ' ]
class PlotCurveItem ( GraphicsObject ) :
2012-04-21 19:55:27 +00:00
"""
Class representing a single plot curve . Instances of this class are created
automatically as part of PlotDataItem ; these rarely need to be instantiated
directly .
Features :
- Fast data update
- FFT display mode ( accessed via PlotItem context menu )
- Fill under curve
- Mouse interaction
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
* * Signals : * *
sigPlotChanged ( self ) Emitted when the data being plotted has changed
sigClicked ( self ) Emitted when the curve is clicked
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
2012-03-02 02:55:32 +00:00
"""
sigPlotChanged = QtCore . Signal ( object )
sigClicked = QtCore . Signal ( object )
2012-04-23 18:57:49 +00:00
def __init__ ( self , * args , * * kargs ) :
2012-04-21 19:55:27 +00:00
"""
2012-04-23 18:57:49 +00:00
Forwards all arguments to : func : ` setData < pyqtgraph . PlotCurveItem . setData > ` .
Some extra arguments are accepted as well :
2012-04-21 19:55:27 +00:00
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
* * Arguments : * *
2012-04-23 18:57:49 +00:00
parent The parent GraphicsObject ( optional )
2012-04-21 19:55:27 +00:00
clickable If True , the item will emit sigClicked when it is
2012-04-23 18:57:49 +00:00
clicked on . Defaults to False .
2012-04-21 19:55:27 +00:00
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
"""
2012-04-23 18:57:49 +00:00
GraphicsObject . __init__ ( self , kargs . get ( ' parent ' , None ) )
2012-03-02 02:55:32 +00:00
self . clear ( )
self . path = None
self . fillPath = None
2012-03-23 06:41:10 +00:00
self . exportOpts = False
self . antialias = False
2012-03-17 16:10:51 +00:00
2012-03-02 02:55:32 +00:00
## this is disastrous for performance.
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
self . metaData = { }
self . opts = {
2012-04-23 18:57:49 +00:00
' pen ' : fn . mkPen ( ' w ' ) ,
2012-03-18 18:57:36 +00:00
' shadowPen ' : None ,
2012-04-23 18:57:49 +00:00
' fillLevel ' : None ,
' brush ' : None ,
2012-10-22 17:35:58 +00:00
' stepMode ' : False ,
2012-11-23 21:05:14 +00:00
' name ' : None
2012-03-02 02:55:32 +00:00
}
2012-04-23 18:57:49 +00:00
self . setClickable ( kargs . get ( ' clickable ' , False ) )
self . setData ( * args , * * kargs )
2012-03-02 02:55:32 +00:00
def implements ( self , interface = None ) :
ints = [ ' plotData ' ]
if interface is None :
return ints
return interface in ints
def setClickable ( self , s ) :
2012-04-21 19:55:27 +00:00
""" Sets whether the item responds to mouse clicks. """
2012-03-02 02:55:32 +00:00
self . clickable = s
def getData ( self ) :
2012-03-18 18:57:36 +00:00
return self . xData , self . yData
2012-03-02 02:55:32 +00:00
2012-05-08 21:56:55 +00:00
def dataBounds ( self , ax , frac = 1.0 , orthoRange = None ) :
2012-03-02 02:55:32 +00:00
( x , y ) = self . getData ( )
if x is None or len ( x ) == 0 :
return ( 0 , 0 )
if ax == 0 :
d = x
2012-05-08 21:56:55 +00:00
d2 = y
2012-03-02 02:55:32 +00:00
elif ax == 1 :
d = y
2012-05-08 21:56:55 +00:00
d2 = x
if orthoRange is not None :
mask = ( d2 > = orthoRange [ 0 ] ) * ( d2 < = orthoRange [ 1 ] )
d = d [ mask ]
d2 = d2 [ mask ]
2012-03-02 02:55:32 +00:00
if frac > = 1.0 :
return ( d . min ( ) , d . max ( ) )
elif frac < = 0.0 :
raise Exception ( " Value for parameter ' frac ' must be > 0. (got %s ) " % str ( frac ) )
else :
return ( scipy . stats . scoreatpercentile ( d , 50 - ( frac * 50 ) ) , scipy . stats . scoreatpercentile ( d , 50 + ( frac * 50 ) ) )
2012-03-18 18:57:36 +00:00
def setPen ( self , * args , * * kargs ) :
2012-04-21 19:55:27 +00:00
""" Set the pen used to draw the curve. """
2012-03-18 18:57:36 +00:00
self . opts [ ' pen ' ] = fn . mkPen ( * args , * * kargs )
2012-03-02 02:55:32 +00:00
self . update ( )
2012-03-18 18:57:36 +00:00
def setShadowPen ( self , * args , * * kargs ) :
2012-04-21 19:55:27 +00:00
""" Set the shadow pen used to draw behind tyhe primary pen.
This pen must have a larger width than the primary
pen to be visible .
"""
2012-03-18 18:57:36 +00:00
self . opts [ ' shadowPen ' ] = fn . mkPen ( * args , * * kargs )
2012-03-02 02:55:32 +00:00
self . update ( )
2012-03-18 18:57:36 +00:00
def setBrush ( self , * args , * * kargs ) :
2012-04-21 19:55:27 +00:00
""" Set the brush used when filling the area under the curve """
2012-03-18 18:57:36 +00:00
self . opts [ ' brush ' ] = fn . mkBrush ( * args , * * kargs )
2012-03-02 02:55:32 +00:00
self . update ( )
2012-03-18 18:57:36 +00:00
def setFillLevel ( self , level ) :
2012-04-21 19:55:27 +00:00
""" Set the level filled to when filling under the curve """
2012-03-18 18:57:36 +00:00
self . opts [ ' fillLevel ' ] = level
self . fillPath = None
2012-03-02 02:55:32 +00:00
self . update ( )
2012-03-18 18:57:36 +00:00
#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()
2012-03-02 02:55:32 +00:00
2012-03-18 18:57:36 +00:00
#def setLogMode(self, mode):
#self.opts['logMode'] = mode
#self.xDisp = self.yDisp = None
#self.path = None
#self.update()
2012-03-02 02:55:32 +00:00
2012-03-18 18:57:36 +00:00
#def setPointMode(self, mode):
#self.opts['pointMode'] = mode
#self.update()
2012-03-02 02:55:32 +00:00
2012-03-18 18:57:36 +00:00
#def setDownsampling(self, ds):
#if self.opts['downsample'] != ds:
#self.opts['downsample'] = ds
#self.xDisp = self.yDisp = None
#self.path = None
#self.update()
2012-03-02 02:55:32 +00:00
2012-03-18 18:57:36 +00:00
def setData ( self , * args , * * kargs ) :
2012-04-21 19:55:27 +00:00
"""
2012-04-23 18:57:49 +00:00
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
* * Arguments : * *
x , y ( numpy arrays ) Data to show
pen Pen to use when drawing . Any single argument accepted by
: func : ` mkPen < pyqtgraph . mkPen > ` is allowed .
shadowPen Pen for drawing behind the primary pen . Usually this
is used to emphasize the curve by providing a
high - contrast border . Any single argument accepted by
: func : ` mkPen < pyqtgraph . mkPen > ` is allowed .
fillLevel ( float or None ) Fill the area ' under ' the curve to
* fillLevel *
brush QBrush to use when filling . Any single argument accepted
by : func : ` mkBrush < pyqtgraph . mkBrush > ` is allowed .
== == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
If non - keyword arguments are used , they will be interpreted as
setData ( y ) for a single argument and setData ( x , y ) for two
arguments .
2012-04-21 19:55:27 +00:00
"""
2012-03-18 18:57:36 +00:00
self . updateData ( * args , * * kargs )
2012-03-02 02:55:32 +00:00
2012-03-18 18:57:36 +00:00
def updateData ( self , * args , * * kargs ) :
2012-03-02 02:55:32 +00:00
prof = debug . Profiler ( ' PlotCurveItem.updateData ' , disabled = True )
2012-03-18 18:57:36 +00:00
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 ) :
2012-03-18 23:48:40 +00:00
data = np . array ( data )
kargs [ k ] = data
2012-03-18 18:57:36 +00:00
if not isinstance ( data , np . ndarray ) or data . ndim > 1 :
raise Exception ( " Plot data must be 1D ndarray. " )
2012-03-02 02:55:32 +00:00
if ' complex ' in str ( data . dtype ) :
raise Exception ( " Can not plot complex data types. " )
2012-03-18 18:57:36 +00:00
2012-03-02 02:55:32 +00:00
prof . mark ( " data checks " )
2012-03-18 18:57:36 +00:00
#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
2012-03-02 02:55:32 +00:00
## Test this bug with test_PlotWidget and zoom in on the animated plot
self . prepareGeometryChange ( )
2012-03-18 18:57:36 +00:00
self . yData = kargs [ ' y ' ] . view ( np . ndarray )
self . xData = kargs [ ' x ' ] . view ( np . ndarray )
2012-03-02 02:55:32 +00:00
prof . mark ( ' copy ' )
2012-10-22 17:35:58 +00:00
if ' stepMode ' in kargs :
self . opts [ ' stepMode ' ] = kargs [ ' stepMode ' ]
if self . opts [ ' stepMode ' ] is True :
if len ( self . xData ) != len ( self . yData ) + 1 : ## allow difference of 1 for step mode plots
raise Exception ( " len(X) must be len(Y)+1 since stepMode=True (got %s and %s ) " % ( str ( x . shape ) , str ( y . shape ) ) )
else :
if self . xData . shape != self . yData . shape : ## allow difference of 1 for step mode plots
raise Exception ( " X and Y arrays must be the same shape--got %s and %s . " % ( str ( x . shape ) , str ( y . shape ) ) )
2012-03-02 02:55:32 +00:00
self . path = None
2012-03-18 18:57:36 +00:00
self . fillPath = None
#self.xDisp = self.yDisp = None
2012-11-23 21:05:14 +00:00
if ' name ' in kargs :
self . opts [ ' name ' ] = kargs [ ' name ' ]
2012-03-18 18:57:36 +00:00
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 ' ] )
2012-03-02 02:55:32 +00:00
prof . mark ( ' set ' )
self . update ( )
prof . mark ( ' update ' )
self . sigPlotChanged . emit ( self )
prof . mark ( ' emit ' )
prof . finish ( )
def generatePath ( self , x , y ) :
prof = debug . Profiler ( ' PlotCurveItem.generatePath ' , disabled = True )
path = QtGui . QPainterPath ( )
## Create all vertices in path. The method used below creates a binary format so that all
## vertices can be read in at once. This binary format may change in future versions of Qt,
## so the original (slower) method is left here for emergencies:
#path.moveTo(x[0], y[0])
#for i in range(1, y.shape[0]):
# path.lineTo(x[i], y[i])
## Speed this up using >> operator
## Format is:
## numVerts(i4) 0(i4)
## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect
## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex
## ...
## 0(i4)
##
## All values are big endian--pack using struct.pack('>d') or struct.pack('>i')
2012-10-22 17:35:58 +00:00
if self . opts [ ' stepMode ' ] :
## each value in the x/y arrays generates 2 points.
x2 = np . empty ( ( len ( x ) , 2 ) , dtype = x . dtype )
x2 [ : ] = x [ : , np . newaxis ]
if self . opts [ ' fillLevel ' ] is None :
x = x2 . reshape ( x2 . size ) [ 1 : - 1 ]
y2 = np . empty ( ( len ( y ) , 2 ) , dtype = y . dtype )
y2 [ : ] = y [ : , np . newaxis ]
y = y2 . reshape ( y2 . size )
else :
## If we have a fill level, add two extra points at either end
x = x2 . reshape ( x2 . size )
y2 = np . empty ( ( len ( y ) + 2 , 2 ) , dtype = y . dtype )
y2 [ 1 : - 1 ] = y [ : , np . newaxis ]
y = y2 . reshape ( y2 . size ) [ 1 : - 1 ]
y [ 0 ] = self . opts [ ' fillLevel ' ]
y [ - 1 ] = self . opts [ ' fillLevel ' ]
2012-10-03 01:23:59 +00:00
if sys . version_info [ 0 ] == 2 : ## So this is disabled for python 3... why??
2012-05-11 22:05:41 +00:00
n = x . shape [ 0 ]
# create empty array, pad with extra space on either end
arr = np . empty ( n + 2 , dtype = [ ( ' x ' , ' >f8 ' ) , ( ' y ' , ' >f8 ' ) , ( ' c ' , ' >i4 ' ) ] )
# write first two integers
prof . mark ( ' allocate empty ' )
arr . data [ 12 : 20 ] = struct . pack ( ' >ii ' , n , 0 )
prof . mark ( ' pack header ' )
# Fill array with vertex values
arr [ 1 : - 1 ] [ ' x ' ] = x
arr [ 1 : - 1 ] [ ' y ' ] = y
arr [ 1 : - 1 ] [ ' c ' ] = 1
prof . mark ( ' fill array ' )
# write last 0
lastInd = 20 * ( n + 1 )
arr . data [ lastInd : lastInd + 4 ] = struct . pack ( ' >i ' , 0 )
prof . mark ( ' footer ' )
# create datastream object and stream into path
buf = QtCore . QByteArray ( arr . data [ 12 : lastInd + 4 ] ) # I think one unnecessary copy happens here
prof . mark ( ' create buffer ' )
ds = QtCore . QDataStream ( buf )
prof . mark ( ' create datastream ' )
ds >> path
prof . mark ( ' load ' )
prof . finish ( )
else :
path . moveTo ( x [ 0 ] , y [ 0 ] )
for i in range ( 1 , y . shape [ 0 ] ) :
path . lineTo ( x [ i ] , y [ i ] )
2012-03-02 02:55:32 +00:00
return path
def shape ( self ) :
if self . path is None :
try :
self . path = self . generatePath ( * self . getData ( ) )
except :
return QtGui . QPainterPath ( )
return self . path
def boundingRect ( self ) :
( x , y ) = self . getData ( )
if x is None or y is None or len ( x ) == 0 or len ( y ) == 0 :
return QtCore . QRectF ( )
2012-03-18 18:57:36 +00:00
if self . opts [ ' shadowPen ' ] is not None :
lineWidth = ( max ( self . opts [ ' pen ' ] . width ( ) , self . opts [ ' shadowPen ' ] . width ( ) ) + 1 )
2012-03-02 02:55:32 +00:00
else :
2012-03-18 18:57:36 +00:00
lineWidth = ( self . opts [ ' pen ' ] . width ( ) + 1 )
2012-03-02 02:55:32 +00:00
pixels = self . pixelVectors ( )
2012-05-30 03:18:34 +00:00
if pixels == ( None , None ) :
2012-03-02 02:55:32 +00:00
pixels = [ Point ( 0 , 0 ) , Point ( 0 , 0 ) ]
2012-10-22 17:35:58 +00:00
xmin = x . min ( )
xmax = x . max ( )
ymin = y . min ( )
ymax = y . max ( )
2012-03-02 02:55:32 +00:00
2012-10-22 17:35:58 +00:00
if self . opts [ ' fillLevel ' ] is not None :
ymin = min ( ymin , self . opts [ ' fillLevel ' ] )
ymax = max ( ymax , self . opts [ ' fillLevel ' ] )
2012-03-02 02:55:32 +00:00
2012-10-22 17:35:58 +00:00
xmin - = pixels [ 0 ] . x ( ) * lineWidth
xmax + = pixels [ 0 ] . x ( ) * lineWidth
ymin - = abs ( pixels [ 1 ] . y ( ) ) * lineWidth
ymax + = abs ( pixels [ 1 ] . y ( ) ) * lineWidth
2012-03-02 02:55:32 +00:00
return QtCore . QRectF ( xmin , ymin , xmax - xmin , ymax - ymin )
def paint ( self , p , opt , widget ) :
prof = debug . Profiler ( ' PlotCurveItem.paint ' + str ( id ( self ) ) , disabled = True )
if self . xData is None :
return
#if self.opts['spectrumMode']:
#if self.specPath is None:
#self.specPath = self.generatePath(*self.getData())
#path = self.specPath
#else:
x = None
y = None
if self . path is None :
x , y = self . getData ( )
if x is None or len ( x ) == 0 or y is None or len ( y ) == 0 :
return
self . path = self . generatePath ( x , y )
self . fillPath = None
path = self . path
prof . mark ( ' generate path ' )
2012-03-18 18:57:36 +00:00
if self . opts [ ' brush ' ] is not None and self . opts [ ' fillLevel ' ] is not None :
2012-03-02 02:55:32 +00:00
if self . fillPath is None :
if x is None :
x , y = self . getData ( )
p2 = QtGui . QPainterPath ( self . path )
2012-03-18 18:57:36 +00:00
p2 . lineTo ( x [ - 1 ] , self . opts [ ' fillLevel ' ] )
p2 . lineTo ( x [ 0 ] , self . opts [ ' fillLevel ' ] )
p2 . lineTo ( x [ 0 ] , y [ 0 ] )
2012-03-02 02:55:32 +00:00
p2 . closeSubpath ( )
self . fillPath = p2
2012-04-04 13:29:35 +00:00
prof . mark ( ' generate fill path ' )
2012-03-18 18:57:36 +00:00
p . fillPath ( self . fillPath , self . opts [ ' brush ' ] )
2012-04-04 13:29:35 +00:00
prof . mark ( ' draw fill path ' )
2012-03-02 02:55:32 +00:00
## Copy pens and apply alpha adjustment
2012-03-18 18:57:36 +00:00
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)
2012-03-02 02:55:32 +00:00
2012-03-23 06:41:10 +00:00
if self . exportOpts is not False :
aa = self . exportOpts [ ' antialias ' ]
else :
aa = self . antialias
p . setRenderHint ( p . Antialiasing , aa )
2012-03-18 18:57:36 +00:00
if sp is not None :
2012-03-02 02:55:32 +00:00
p . setPen ( sp )
p . drawPath ( path )
p . setPen ( cp )
p . drawPath ( path )
prof . mark ( ' drawPath ' )
#print "Render hints:", int(p.renderHints())
prof . finish ( )
#p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
#p.drawRect(self.boundingRect())
def clear ( self ) :
self . xData = None ## raw values
self . yData = None
self . xDisp = None ## display values (after log / fft)
self . yDisp = None
self . path = None
#del self.xData, self.yData, self.xDisp, self.yDisp, self.path
#def mousePressEvent(self, ev):
##GraphicsObject.mousePressEvent(self, ev)
#if not self.clickable:
#ev.ignore()
#if ev.button() != QtCore.Qt.LeftButton:
#ev.ignore()
#self.mousePressPos = ev.pos()
#self.mouseMoved = False
#def mouseMoveEvent(self, ev):
##GraphicsObject.mouseMoveEvent(self, ev)
#self.mouseMoved = True
##print "move"
#def mouseReleaseEvent(self, ev):
##GraphicsObject.mouseReleaseEvent(self, ev)
#if not self.mouseMoved:
#self.sigClicked.emit(self)
def mouseClickEvent ( self , ev ) :
if not self . clickable or ev . button ( ) != QtCore . Qt . LeftButton :
return
ev . accept ( )
self . sigClicked . emit ( self )
2012-03-23 06:41:10 +00:00
def setExportMode ( self , export , opts ) :
if export :
self . exportOpts = opts
if ' antialias ' not in opts :
self . exportOpts [ ' antialias ' ] = True
else :
self . exportOpts = False
2012-03-02 02:55:32 +00:00
class ROIPlotItem ( PlotCurveItem ) :
""" Plot curve that monitors an ROI and image for changes to automatically replot. """
def __init__ ( self , roi , data , img , axes = ( 0 , 1 ) , xVals = None , color = None ) :
self . roi = roi
self . roiData = data
self . roiImg = img
self . axes = axes
self . xVals = xVals
PlotCurveItem . __init__ ( self , self . getRoiData ( ) , x = self . xVals , color = color )
#roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent)
roi . sigRegionChanged . connect ( self . roiChangedEvent )
#self.roiChangedEvent()
def getRoiData ( self ) :
d = self . roi . getArrayRegion ( self . roiData , self . roiImg , axes = self . axes )
if d is None :
return
while d . ndim > 1 :
d = d . mean ( axis = 1 )
return d
def roiChangedEvent ( self ) :
d = self . getRoiData ( )
self . updateData ( d , self . xVals )