Fixed click signal propagation for PlotDataItem

This commit is contained in:
Luke Campagnola 2012-04-21 15:55:27 -04:00
parent 59ad54c55e
commit 33bc81a121
3 changed files with 149 additions and 45 deletions

View File

@ -12,17 +12,50 @@ __all__ = ['PlotCurveItem']
class PlotCurveItem(GraphicsObject): class PlotCurveItem(GraphicsObject):
"""Class representing a single plot curve. Provides: """
- Fast data update Class representing a single plot curve. Instances of this class are created
- FFT display mode automatically as part of PlotDataItem; these rarely need to be instantiated
- shadow pen directly.
- mouse interaction
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
==================== ===============================================
""" """
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, clickable=False): def __init__(self, y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, clickable=False):
"""
============== =======================================================
**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.
clickable If True, the item will emit sigClicked when it is
clicked on.
============== =======================================================
"""
GraphicsObject.__init__(self, parent) GraphicsObject.__init__(self, parent)
self.clear() self.clear()
self.path = None self.path = None
@ -62,6 +95,7 @@ class PlotCurveItem(GraphicsObject):
return interface in ints return interface in ints
def setClickable(self, s): def setClickable(self, s):
"""Sets whether the item responds to mouse clicks."""
self.clickable = s self.clickable = s
@ -127,18 +161,25 @@ class PlotCurveItem(GraphicsObject):
#return self.metaData #return self.metaData
def setPen(self, *args, **kargs): def setPen(self, *args, **kargs):
"""Set the pen used to draw the curve."""
self.opts['pen'] = fn.mkPen(*args, **kargs) self.opts['pen'] = fn.mkPen(*args, **kargs)
self.update() self.update()
def setShadowPen(self, *args, **kargs): def setShadowPen(self, *args, **kargs):
"""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.
"""
self.opts['shadowPen'] = fn.mkPen(*args, **kargs) self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
self.update() self.update()
def setBrush(self, *args, **kargs): def setBrush(self, *args, **kargs):
"""Set the brush used when filling the area under the curve"""
self.opts['brush'] = fn.mkBrush(*args, **kargs) self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.update() self.update()
def setFillLevel(self, level): def setFillLevel(self, level):
"""Set the level filled to when filling under the curve"""
self.opts['fillLevel'] = level self.opts['fillLevel'] = level
self.fillPath = None self.fillPath = None
self.update() self.update()
@ -177,7 +218,9 @@ class PlotCurveItem(GraphicsObject):
#self.update() #self.update()
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
"""Same as updateData()""" """
Accepts most of the same arguments as __init__.
"""
self.updateData(*args, **kargs) self.updateData(*args, **kargs)
def updateData(self, *args, **kargs): def updateData(self, *args, **kargs):

View File

@ -24,15 +24,18 @@ class PlotDataItem(GraphicsObject):
usually created by plot() methods such as :func:`pyqtgraph.plot` and usually created by plot() methods such as :func:`pyqtgraph.plot` and
:func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`. :func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`.
===================== ============================================== ============================== ==============================================
**Signals:** **Signals:**
sigPlotChanged(self) Emitted when the data in this item is updated. sigPlotChanged(self) Emitted when the data in this item is updated.
sigClicked(self) Emitted when the item is clicked. sigClicked(self) Emitted when the item is clicked.
===================== ============================================== sigPointsClicked(self, points) Emitted when a plot point is clicked
Sends the list of points under the mouse.
============================== ==============================================
""" """
sigPlotChanged = QtCore.Signal(object) sigPlotChanged = QtCore.Signal(object)
sigClicked = QtCore.Signal(object) sigClicked = QtCore.Signal(object)
sigPointsClicked = QtCore.Signal(object, object)
def __init__(self, *args, **kargs): def __init__(self, *args, **kargs):
""" """
@ -109,6 +112,10 @@ class PlotDataItem(GraphicsObject):
self.curve.setParentItem(self) self.curve.setParentItem(self)
self.scatter.setParentItem(self) self.scatter.setParentItem(self)
self.curve.sigClicked.connect(self.curveClicked)
self.scatter.sigClicked.connect(self.scatterClicked)
#self.clear() #self.clear()
self.opts = { self.opts = {
'fftMode': False, 'fftMode': False,
@ -127,6 +134,8 @@ class PlotDataItem(GraphicsObject):
'symbolPen': (200,200,200), 'symbolPen': (200,200,200),
'symbolBrush': (50, 50, 150), 'symbolBrush': (50, 50, 150),
'identical': False, 'identical': False,
'data': None,
} }
self.setData(*args, **kargs) self.setData(*args, **kargs)
@ -150,8 +159,8 @@ class PlotDataItem(GraphicsObject):
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
def setLogMode(self, mode): def setLogMode(self, xMode, yMode):
self.opts['logMode'] = mode self.opts['logMode'] = (xMode, yMode)
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
@ -244,7 +253,7 @@ class PlotDataItem(GraphicsObject):
data = args[0] data = args[0]
dt = dataType(data) dt = dataType(data)
if dt == 'empty': if dt == 'empty':
return pass
elif dt == 'listOfValues': elif dt == 'listOfValues':
y = np.array(data) y = np.array(data)
elif dt == 'Nx2array': elif dt == 'Nx2array':
@ -260,6 +269,8 @@ class PlotDataItem(GraphicsObject):
x = np.array([d.get('x',None) for d in data]) x = np.array([d.get('x',None) for d in data])
if 'y' in data[0]: if 'y' in data[0]:
y = np.array([d.get('y',None) for d in data]) y = np.array([d.get('y',None) for d in data])
for k in ['data', 'symbolSize', 'symbolPen', 'symbolBrush', 'symbolShape']:
kargs[k] = [d.get(k, None) for d in data]
elif dt == 'MetaArray': elif dt == 'MetaArray':
y = data.view(np.ndarray) y = data.view(np.ndarray)
x = data.xvals(0).view(np.ndarray) x = data.xvals(0).view(np.ndarray)
@ -349,8 +360,9 @@ class PlotDataItem(GraphicsObject):
curveArgs[v] = self.opts[k] curveArgs[v] = self.opts[k]
scatterArgs = {} scatterArgs = {}
for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size')]: for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size'), ('data', 'data')]:
scatterArgs[v] = self.opts[k] if k in self.opts:
scatterArgs[v] = self.opts[k]
x,y = self.getData() x,y = self.getData()
@ -398,6 +410,11 @@ class PlotDataItem(GraphicsObject):
x = np.log10(x) x = np.log10(x)
if self.opts['logMode'][1]: if self.opts['logMode'][1]:
y = np.log10(y) y = np.log10(y)
if any(self.opts['logMode']): ## re-check for NANs after log
nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y)
if any(nanMask):
x = x[~nanMask]
y = y[~nanMask]
self.xDisp = x self.xDisp = x
self.yDisp = y self.yDisp = y
#print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max()
@ -438,6 +455,13 @@ class PlotDataItem(GraphicsObject):
def appendData(self, *args, **kargs): def appendData(self, *args, **kargs):
pass pass
def curveClicked(self):
self.sigClicked.emit(self)
def scatterClicked(self, plt, points):
self.sigClicked.emit(self)
self.sigPointsClicked.emit(self, points)
def dataType(obj): def dataType(obj):
if hasattr(obj, '__len__') and len(obj) == 0: if hasattr(obj, '__len__') and len(obj) == 0:

View File

@ -7,7 +7,23 @@ import scipy.stats
__all__ = ['ScatterPlotItem', 'SpotItem'] __all__ = ['ScatterPlotItem', 'SpotItem']
class ScatterPlotItem(GraphicsObject): class ScatterPlotItem(GraphicsObject):
"""
Displays a set of x/y points. Instances of this class are created
automatically as part of PlotDataItem; these rarely need to be instantiated
directly.
The size, shape, pen, and fill brush may be set for each point individually
or for all points.
======================== ===============================================
**Signals:**
sigPlotChanged(self) Emitted when the data being plotted has changed
sigClicked(self, points) Emitted when the curve is clicked. Sends a list
of all the points under the mouse pointer.
======================== ===============================================
"""
#sigPointClicked = QtCore.Signal(object, object) #sigPointClicked = QtCore.Signal(object, object)
sigClicked = QtCore.Signal(object, object) ## self, points sigClicked = QtCore.Signal(object, object) ## self, points
sigPlotChanged = QtCore.Signal(object) sigPlotChanged = QtCore.Signal(object)
@ -37,35 +53,37 @@ class ScatterPlotItem(GraphicsObject):
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """
Ordered Arguments: **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. * 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: **Keyword Arguments:**
{'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method *spots* Optional list of dicts. Each dict specifies parameters for a single spot:
of passing in data for the corresponding arguments. {'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method
*x*,*y*: 1D arrays of x,y values. of passing in data for the corresponding arguments.
*pos*: 2D structure of x,y pairs (such as Nx2 array or list of tuples) *x*,*y* 1D arrays of x,y values.
*pxMode*: If True, spots are always the same size regardless of scaling, and size is given in px. *pos* 2D structure of x,y pairs (such as Nx2 array or list of tuples)
Otherwise, size is in scene coordinates and the spots scale with the view. *pxMode* If True, spots are always the same size regardless of scaling, and size is given in px.
Default is True 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
This can result in performance enhancement. *identical* If True, all spots are forced to look identical.
Default is False This can result in performance enhancement.
*symbol* can be one (or a list) of: Default is False
'o' circle (default) *symbol* can be one (or a list) of:
's' square
't' triangle
'd' diamond
'+' plus
*pen*: The pen (or list of pens) to use for drawing spot outlines. * 'o' circle (default)
*brush*: The brush (or list of brushes) to use for filling spots. * 's' square
*size*: The size (or list of sizes) of spots. If *pxMode* is True, this value is in pixels. Otherwise, * 't' triangle
it is in the item's local coordinate system. * 'd' diamond
*data*: a list of python objects used to uniquely identify each spot. * '+' 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() self.clear()
@ -149,6 +167,9 @@ class ScatterPlotItem(GraphicsObject):
setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
setMethod(kargs[k]) setMethod(kargs[k])
if 'data' in kargs:
self.setPointData(kargs['data'])
self.updateSpots() self.updateSpots()
@ -183,7 +204,7 @@ class ScatterPlotItem(GraphicsObject):
#self.data[k].append(v) #self.data[k].append(v)
def setPoints(self, *args, **kargs): def setPoints(self, *args, **kargs):
"""Deprecated; use setData""" ##Deprecated; use setData
return self.setData(*args, **kargs) return self.setData(*args, **kargs)
#def setPoints(self, spots=None, x=None, y=None, data=None): #def setPoints(self, spots=None, x=None, y=None, data=None):
@ -259,6 +280,16 @@ class ScatterPlotItem(GraphicsObject):
self.opts['size'] = size self.opts['size'] = size
self.updateSpots() self.updateSpots()
def setPointData(self, data):
if isinstance(data, np.ndarray) or isinstance(data, list):
if self.data is None:
raise Exception("Must set xy data before setting meta data.")
if len(data) != len(self.data):
raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(self.data)))
self.data['data'] = data
self.updateSpots()
def setIdentical(self, ident): def setIdentical(self, ident):
self.opts['identical'] = ident self.opts['identical'] = ident
self.updateSpots() self.updateSpots()
@ -354,6 +385,12 @@ class ScatterPlotItem(GraphicsObject):
symbol = self.data['symbol'].copy() symbol = self.data['symbol'].copy()
symbol[symbol==''] = self.opts['symbol'] symbol[symbol==''] = self.opts['symbol']
data = self.data['data'].copy()
if 'data' in self.opts:
data[data==None] = self.opts['data']
for i in xrange(len(self.data)): for i in xrange(len(self.data)):
s = self.data[i] s = self.data[i]
pos = Point(s['x'], s['y']) pos = Point(s['x'], s['y'])
@ -373,7 +410,7 @@ class ScatterPlotItem(GraphicsObject):
#ymn = min(ymn, pos[1]-psize) #ymn = min(ymn, pos[1]-psize)
#ymx = max(ymx, 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)) item = self.mkSpot(pos, size[i], self.opts['pxMode'], brush[i], pen[i], data[i], symbol=symbol[i], index=len(self.spots))
self.spots.append(item) self.spots.append(item)
self.data[i]['spot'] = item self.data[i]['spot'] = item
#if self.optimize: #if self.optimize: