Commit 33bc81a1 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Fixed click signal propagation for PlotDataItem

parent 59ad54c5
......@@ -12,17 +12,50 @@ __all__ = ['PlotCurveItem']
class PlotCurveItem(GraphicsObject):
"""Class representing a single plot curve. Provides:
- Fast data update
- FFT display mode
- shadow pen
- mouse interaction
"""
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
==================== ===============================================
"""
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, 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)
self.clear()
self.path = None
......@@ -62,6 +95,7 @@ class PlotCurveItem(GraphicsObject):
return interface in ints
def setClickable(self, s):
"""Sets whether the item responds to mouse clicks."""
self.clickable = s
......@@ -127,18 +161,25 @@ class PlotCurveItem(GraphicsObject):
#return self.metaData
def setPen(self, *args, **kargs):
"""Set the pen used to draw the curve."""
self.opts['pen'] = fn.mkPen(*args, **kargs)
self.update()
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.update()
def setBrush(self, *args, **kargs):
"""Set the brush used when filling the area under the curve"""
self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.update()
def setFillLevel(self, level):
"""Set the level filled to when filling under the curve"""
self.opts['fillLevel'] = level
self.fillPath = None
self.update()
......@@ -177,7 +218,9 @@ class PlotCurveItem(GraphicsObject):
#self.update()
def setData(self, *args, **kargs):
"""Same as updateData()"""
"""
Accepts most of the same arguments as __init__.
"""
self.updateData(*args, **kargs)
def updateData(self, *args, **kargs):
......
......@@ -24,15 +24,18 @@ class PlotDataItem(GraphicsObject):
usually created by plot() methods such as :func:`pyqtgraph.plot` and
:func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`.
===================== ==============================================
============================== ==============================================
**Signals:**
sigPlotChanged(self) Emitted when the data in this item is updated.
sigClicked(self) Emitted when the item is clicked.
===================== ==============================================
sigPlotChanged(self) Emitted when the data in this item is updated.
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)
sigClicked = QtCore.Signal(object)
sigPointsClicked = QtCore.Signal(object, object)
def __init__(self, *args, **kargs):
"""
......@@ -109,6 +112,10 @@ class PlotDataItem(GraphicsObject):
self.curve.setParentItem(self)
self.scatter.setParentItem(self)
self.curve.sigClicked.connect(self.curveClicked)
self.scatter.sigClicked.connect(self.scatterClicked)
#self.clear()
self.opts = {
'fftMode': False,
......@@ -127,6 +134,8 @@ class PlotDataItem(GraphicsObject):
'symbolPen': (200,200,200),
'symbolBrush': (50, 50, 150),
'identical': False,
'data': None,
}
self.setData(*args, **kargs)
......@@ -150,8 +159,8 @@ class PlotDataItem(GraphicsObject):
self.xDisp = self.yDisp = None
self.updateItems()
def setLogMode(self, mode):
self.opts['logMode'] = mode
def setLogMode(self, xMode, yMode):
self.opts['logMode'] = (xMode, yMode)
self.xDisp = self.yDisp = None
self.updateItems()
......@@ -244,7 +253,7 @@ class PlotDataItem(GraphicsObject):
data = args[0]
dt = dataType(data)
if dt == 'empty':
return
pass
elif dt == 'listOfValues':
y = np.array(data)
elif dt == 'Nx2array':
......@@ -260,6 +269,8 @@ class PlotDataItem(GraphicsObject):
x = np.array([d.get('x',None) for d in data])
if 'y' in data[0]:
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':
y = data.view(np.ndarray)
x = data.xvals(0).view(np.ndarray)
......@@ -349,8 +360,9 @@ class PlotDataItem(GraphicsObject):
curveArgs[v] = self.opts[k]
scatterArgs = {}
for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size')]:
scatterArgs[v] = self.opts[k]
for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size'), ('data', 'data')]:
if k in self.opts:
scatterArgs[v] = self.opts[k]
x,y = self.getData()
......@@ -398,6 +410,11 @@ class PlotDataItem(GraphicsObject):
x = np.log10(x)
if self.opts['logMode'][1]:
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.yDisp = y
#print self.yDisp.shape, self.yDisp.min(), self.yDisp.max()
......@@ -438,6 +455,13 @@ class PlotDataItem(GraphicsObject):
def appendData(self, *args, **kargs):
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):
if hasattr(obj, '__len__') and len(obj) == 0:
......
......@@ -7,7 +7,23 @@ import scipy.stats
__all__ = ['ScatterPlotItem', 'SpotItem']
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)
sigClicked = QtCore.Signal(object, object) ## self, points
sigPlotChanged = QtCore.Signal(object)
......@@ -37,35 +53,37 @@ class ScatterPlotItem(GraphicsObject):
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.
Default is True
*identical*: If True, all spots are forced to look identical.
This can result in performance enhancement.
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.
**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.
Default is True
*identical* If True, all spots are forced to look identical.
This can result in performance enhancement.
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()
......@@ -148,6 +166,9 @@ class ScatterPlotItem(GraphicsObject):
if k in kargs:
setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
setMethod(kargs[k])
if 'data' in kargs:
self.setPointData(kargs['data'])
self.updateSpots()
......@@ -183,7 +204,7 @@ class ScatterPlotItem(GraphicsObject):
#self.data[k].append(v)
def setPoints(self, *args, **kargs):
"""Deprecated; use setData"""
##Deprecated; use setData
return self.setData(*args, **kargs)
#def setPoints(self, spots=None, x=None, y=None, data=None):
......@@ -259,6 +280,16 @@ class ScatterPlotItem(GraphicsObject):
self.opts['size'] = size
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):
self.opts['identical'] = ident
self.updateSpots()
......@@ -353,6 +384,12 @@ class ScatterPlotItem(GraphicsObject):
symbol = self.data['symbol'].copy()
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)):
s = self.data[i]
......@@ -373,7 +410,7 @@ class ScatterPlotItem(GraphicsObject):
#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))
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.data[i]['spot'] = item
#if self.optimize:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment