From 33bc81a121579c33fcca0bd07bb4b89355e5b683 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 21 Apr 2012 15:55:27 -0400 Subject: [PATCH] Fixed click signal propagation for PlotDataItem --- graphicsItems/PlotCurveItem.py | 55 ++++++++++++++++-- graphicsItems/PlotDataItem.py | 42 +++++++++++--- graphicsItems/ScatterPlotItem.py | 97 ++++++++++++++++++++++---------- 3 files changed, 149 insertions(+), 45 deletions(-) diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index bc3629d2..4ce9af46 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -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 ` 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 ` 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 ` 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): diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index 44493799..bb249bf3 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -24,15 +24,18 @@ class PlotDataItem(GraphicsObject): usually created by plot() methods such as :func:`pyqtgraph.plot` and :func:`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: diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index f40db2fa..b93c134f 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -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. + **Ordered Arguments:** - 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. + * 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: