Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

custom ScatterPlotItem.py from scatterplottooltips

  • Loading branch information...
commit 3b8c111a31e681c7ef9c9feb6d4f32555f022879 1 parent 1f8a741
@ibressler authored
Showing with 173 additions and 287 deletions.
  1. +173 −287 graphicsItems/ScatterPlotItem.py
View
460 graphicsItems/ScatterPlotItem.py
@@ -1,9 +1,14 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
-from GraphicsObject import GraphicsObject
+from .GraphicsItem import GraphicsItem
+from .GraphicsObject import GraphicsObject
import numpy as np
import scipy.stats
+import weakref
+import pyqtgraph.debug as debug
+from collections import OrderedDict
+#import pyqtgraph as pg
__all__ = ['ScatterPlotItem', 'SpotItem']
@@ -84,24 +89,27 @@ def __init__(self, *args, **kargs):
"""
Accepts the same arguments as setData()
"""
-
+ prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True)
GraphicsObject.__init__(self)
- self.data = None
- self.spots = []
+ self.setFlag(self.ItemHasNoContents, True)
+ self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('item', object), ('data', object)])
+ #self.spots = []
+ #self.fragments = None
self.bounds = [None, None]
- self.opts = {}
- self.spotsValid = False
+ self.opts = {'pxMode': True}
+ #self.spotsValid = False
+ #self.itemsValid = False
self._spotPixmap = None
- self.setPen(200,200,200)
- self.setBrush(100,100,150)
- self.setSymbol('o')
- self.setSize(7)
- self.setPxMode(True)
- self.setIdentical(False)
-
+ self.setPen(200,200,200, update=False)
+ self.setBrush(100,100,150, update=False)
+ self.setSymbol('o', update=False)
+ self.setSize(7, update=False)
+ #self.setIdentical(False, update=False)
+ prof.mark('1')
self.setData(*args, **kargs)
-
+ prof.mark('setData')
+ prof.finish()
def setData(self, *args, **kargs):
"""
@@ -120,9 +128,6 @@ def setData(self, *args, **kargs):
*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)
@@ -135,11 +140,17 @@ def setData(self, *args, **kargs):
*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.
+ *identical* *Deprecated*. This functionality is handled automatically now.
====================== ===============================================================================================
"""
-
- self.clear()
+ self.clear() ## clear out all old data
+ self.addPoints(*args, **kargs)
+ def addPoints(self, *args, **kargs):
+ """
+ Add new points to the scatter plot.
+ Arguments are the same as setData()
+ """
## deal with non-keyword arguments
if len(args) == 1:
@@ -179,106 +190,66 @@ def setData(self, *args, **kargs):
kargs['y'] = []
numPts = 0
- ## create empty record array
- self.data = np.empty(numPts, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('spot', object)])
- self.data['size'] = -1 ## indicates use default size
- self.data['symbol'] = ''
- self.data['pen'] = None
- self.data['brush'] = None
- self.pointData = np.empty(numPts, dtype=object)
- self.pointData[:] = None
+ ## Extend record array
+ oldData = self.data
+ self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype)
+ ## note that np.empty initializes object fields to None and string fields to ''
+
+ self.data[:len(oldData)] = oldData
+ for i in range(len(oldData)):
+ oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array
+
+ newData = self.data[len(oldData):]
+ newData['size'] = -1 ## indicates to use default size
if 'spots' in kargs:
spots = kargs['spots']
- for i in xrange(len(spots)):
+ for i in range(len(spots)):
spot = spots[i]
for k in spot:
- if k == 'pen':
- self.data[i][k] = fn.mkPen(spot[k])
- elif k == 'brush':
- self.data[i][k] = fn.mkBrush(spot[k])
- elif k == 'pos':
+ #if k == 'pen':
+ #newData[k] = fn.mkPen(spot[k])
+ #elif k == 'brush':
+ #newData[k] = fn.mkBrush(spot[k])
+ if k == 'pos':
pos = spot[k]
if isinstance(pos, QtCore.QPointF):
x,y = pos.x(), pos.y()
else:
x,y = pos[0], pos[1]
- self.data[i]['x'] = x
- self.data[i]['y'] = y
- elif k in ['x', 'y', 'size', 'symbol']:
- self.data[i][k] = spot[k]
- elif k == 'data':
- self.pointData[i] = spot[k]
+ newData[i]['x'] = x
+ newData[i]['y'] = y
+ elif k in ['x', 'y', 'size', 'symbol', 'pen', 'brush', 'data']:
+ newData[i][k] = spot[k]
+ #elif k == 'data':
+ #self.pointData[i] = spot[k]
else:
raise Exception("Unknown spot parameter: %s" % k)
elif 'y' in kargs:
- self.data['x'] = kargs['x']
- self.data['y'] = kargs['y']
-
+ newData['x'] = kargs['x']
+ newData['y'] = kargs['y']
+ if 'pxMode' in kargs:
+ self.setPxMode(kargs['pxMode'], update=False)
+
## Set any extra parameters provided in keyword arguments
for k in ['pen', 'brush', 'symbol', 'size', 'toolTips']:
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()
-
-
-
-
-
-
-
-
- #pen = kargs.get('pen', (200,200,200))
- #brush = kargs.get('pen', (100,100,150))
-
- #if hasattr(pen, '__len__'):
- #pen = map(pg.mkPen(pen))
- #self.data['pen'] = pen
-
- #if hasattr(pen, '__len__'):
- #brush = map(pg.mkPen(pen))
- #self.data['brush'] = pen
-
- #self.data['size'] = kargs.get('size', 7)
- #self.data['symbol'] = kargs.get('symbol', 'o')
+ setMethod(kargs[k], update=False, dataSet=newData)
+ if 'data' in kargs:
+ self.setPointData(kargs['data'], dataSet=newData)
+ #self.updateSpots()
+ self.generateSpotItems()
+ self.sigPlotChanged.emit(self)
- #if spots is not None and len(spots) > 0:
- #spot = spots[0]
- #for k in spot:
- #self.data[k] = []
- #for spot in spots:
- #for k,v in spot.iteritems():
- #self.data[k].append(v)
def setPoints(self, *args, **kargs):
##Deprecated; use setData
return self.setData(*args, **kargs)
- #def setPoints(self, spots=None, x=None, y=None, data=None):
- #"""
- #Remove all existing points in the scatter plot and add a new set.
- #Arguments:
- #spots - list of dicts specifying parameters for each spot
- #[ {'pos': (x,y), 'pen': 'r', ...}, ...]
- #x, y - arrays specifying location of spots to add.
- #all other parameters (pen, symbol, etc.) will be set to the default
- #values for this scatter plot.
- #these arguments are IGNORED if 'spots' is specified
- #data - list of arbitrary objects to be assigned to spot.data for each spot
- #(this is useful for identifying spots that are clicked on)
- #"""
- #self.clear()
- #self.bounds = [[0,0],[0,0]]
- #self.addPoints(spots, x, y, data)
-
def implements(self, interface=None):
ints = ['plotData']
if interface is None:
@@ -286,70 +257,100 @@ def implements(self, interface=None):
return interface in ints
def setPen(self, *args, **kargs):
+ """Set the pen(s) used to draw the outline around each spot.
+ If a list or array is provided, then the pen for each spot will be set separately.
+ Otherwise, the arguments are passed to pg.mkPen and used as the default pen for
+ all spots which do not have a pen explicitly set."""
+ update = kargs.pop('update', True)
+ dataSet = kargs.pop('dataSet', self.data)
+
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
pens = args[0]
- if self.data is None:
- raise Exception("Must set data before setting multiple pens.")
- if len(pens) != len(self.data):
- raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(self.data)))
- for i in xrange(len(pens)):
- self.data[i]['pen'] = fn.mkPen(pens[i])
+ if len(pens) != len(dataSet):
+ raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet)))
+ dataSet['pen'] = pens
else:
self.opts['pen'] = fn.mkPen(*args, **kargs)
- self.updateSpots()
+ self._spotPixmap = None
+
+ if update:
+ self.updateSpots(dataSet)
def setBrush(self, *args, **kargs):
+ """Set the brush(es) used to fill the interior of each spot.
+ If a list or array is provided, then the brush for each spot will be set separately.
+ Otherwise, the arguments are passed to pg.mkBrush and used as the default brush for
+ all spots which do not have a brush explicitly set."""
+ update = kargs.pop('update', True)
+ dataSet = kargs.pop('dataSet', self.data)
+
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
brushes = args[0]
- if self.data is None:
- raise Exception("Must set data before setting multiple brushes.")
- if len(brushes) != len(self.data):
- raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(self.data)))
- for i in xrange(len(brushes)):
- self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs)
+ if len(brushes) != len(dataSet):
+ raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet)))
+ #for i in xrange(len(brushes)):
+ #self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs)
+ dataSet['brush'] = brushes
else:
self.opts['brush'] = fn.mkBrush(*args, **kargs)
- self.updateSpots()
+ self._spotPixmap = None
+
+ if update:
+ self.updateSpots(dataSet)
- def setSymbol(self, symbol):
+ def setSymbol(self, symbol, update=True, dataSet=None):
+ """Set the symbol(s) used to draw each spot.
+ If a list or array is provided, then the symbol for each spot will be set separately.
+ Otherwise, the argument will be used as the default symbol for
+ all spots which do not have a symbol explicitly set."""
+ if dataSet is None:
+ dataSet = self.data
+
if isinstance(symbol, np.ndarray) or isinstance(symbol, list):
symbols = symbol
- if self.data is None:
- raise Exception("Must set data before setting multiple symbols.")
- if len(symbols) != len(self.data):
- raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(self.data)))
- self.data['symbol'] = symbols
+ if len(symbols) != len(dataSet):
+ raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet)))
+ dataSet['symbol'] = symbols
else:
self.opts['symbol'] = symbol
- self.updateSpots()
+ self._spotPixmap = None
- def setSize(self, size):
+ if update:
+ self.updateSpots(dataSet)
+
+ def setSize(self, size, update=True, dataSet=None):
+ """Set the size(s) used to draw each spot.
+ If a list or array is provided, then the size for each spot will be set separately.
+ Otherwise, the argument will be used as the default size for
+ all spots which do not have a size explicitly set."""
+ if dataSet is None:
+ dataSet = self.data
+
if isinstance(size, np.ndarray) or isinstance(size, list):
sizes = size
- if self.data is None:
- raise Exception("Must set data before setting multiple sizes.")
- if len(sizes) != len(self.data):
- raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(self.data)))
- self.data['size'] = sizes
+ if len(sizes) != len(dataSet):
+ raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(dataSet)))
+ dataSet['size'] = sizes
else:
self.opts['size'] = size
- self.updateSpots()
+ self._spotPixmap = None
+
+ if update:
+ self.updateSpots(dataSet)
- def setPointData(self, data):
+ def setPointData(self, data, dataSet=None):
+ if dataSet is None:
+ dataSet = 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.pointData = data
- self.updateSpots()
-
+ if len(data) != len(dataSet):
+ raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet)))
+ dataSet['data'] = data
- def setIdentical(self, ident):
- self.opts['identical'] = ident
- self.updateSpots()
-
- def setPxMode(self, mode):
+ def setPxMode(self, mode, update=True):
+ if self.opts['pxMode'] == mode:
+ return
+
self.opts['pxMode'] = mode
self.clearItems()
if update:
@@ -369,18 +370,22 @@ def updateSpots(self, dataSet=None):
spot.updateItem()
def clear(self):
- for i in self.spots:
+ """Remove all spots from the scatter plot"""
+ self.clearItems()
+ self.data = np.empty(0, dtype=self.data.dtype)
+ self.bounds = [None, None]
+
+ def clearItems(self):
+ for i in self.data['item']:
+ if i is None:
+ continue
i.setParentItem(None)
s = i.scene()
if s is not None:
s.removeItem(i)
- self.spots = []
- self.data = None
- self.spotsValid = False
- self.bounds = [None, None]
+ self.data['item'] = None
-
- def dataBounds(self, ax, frac=1.0):
+ def dataBounds(self, ax, frac=1.0, orthoRange=None):
if frac >= 1.0 and self.bounds[ax] is not None:
return self.bounds[ax]
@@ -389,8 +394,15 @@ def dataBounds(self, ax, frac=1.0):
if ax == 0:
d = self.data['x']
+ d2 = self.data['y']
elif ax == 1:
d = self.data['y']
+ d2 = self.data['x']
+
+ if orthoRange is not None:
+ mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
+ d = d[mask]
+ d2 = d2[mask]
if frac >= 1.0:
minIndex = np.argmin(d)
@@ -410,113 +422,21 @@ def dataBounds(self, ax, frac=1.0):
- def addPoints(self, *args, **kargs):
- """
- Add new points to the scatter plot.
- Arguments are the same as setData()
- Note: this is expensive; plenty of room for optimization here.
- """
- if self.data is None:
- self.setData(*args, **kargs)
- return
-
-
- data1 = self.data[:]
- #range1 = [self.bounds[0][:], self.bounds[1][:]]
- self.setData(*args, **kargs)
- newData = np.empty(len(self.data) + len(data1), dtype=self.data.dtype)
- newData[:len(data1)] = data1
- newData[len(data1):] = self.data
- #self.bounds = [
- #[min(self.bounds[0][0], range1[0][0]), max(self.bounds[0][1], range1[0][1])],
- #[min(self.bounds[1][0], range1[1][0]), max(self.bounds[1][1], range1[1][1])],
- #]
- self.data = newData
- self.sigPlotChanged.emit(self)
- def generateSpots(self, clear=True):
- if clear:
- for spot in self.spots:
- self.scene().removeItem(spot)
- self.spots = []
-
-
- xmn = ymn = xmx = ymx = None
-
- ## apply defaults
- size = self.data['size'].copy()
- size[size<0] = self.opts['size']
-
- pen = self.data['pen'].copy()
- pen[pen<0] = self.opts['pen'] ## note pen<0 checks for pen==None
-
- brush = self.data['brush'].copy()
- brush[brush<0] = self.opts['brush']
-
- symbol = self.data['symbol'].copy()
- symbol[symbol==''] = self.opts['symbol']
-
-
- for i in xrange(len(self.data)):
- s = self.data[i]
- pos = Point(s['x'], s['y'])
- if self.opts['pxMode']:
- psize = 0
- else:
- psize = size[i]
-
- if self.pointData is None or self.pointData[i] is None:
- data = self.opts.get('data', None)
- else:
- data = self.pointData[i]
-
- #if xmn is None:
- #xmn = pos[0]-psize
- #xmx = pos[0]+psize
- #ymn = pos[1]-psize
- #ymx = pos[1]+psize
- #else:
- #xmn = min(xmn, pos[0]-psize)
- #xmx = max(xmx, pos[0]+psize)
- #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], data, symbol=symbol[i], index=len(self.spots))
- self.spots.append(item)
- self.data[i]['spot'] = item
- #if self.optimize:
- #item.hide()
- #frag = QtGui.QPainter.PixmapFragment.create(pos, QtCore.QRectF(0, 0, size, size))
- #self.optimizeFragments.append(frag)
-
- #self.bounds = [[xmn, xmx], [ymn, ymx]]
- self.spotsValid = True
+ def generateSpotItems(self):
+ if self.opts['pxMode']:
+ for rec in self.data:
+ if rec['item'] is None:
+ rec['item'] = PixmapSpotItem(rec, self)
+ else:
+ for rec in self.data:
+ if rec['item'] is None:
+ rec['item'] = PathSpotItem(rec, self)
self.sigPlotChanged.emit(self)
-
-
- #def setPointSize(self, size):
- #for s in self.spots:
- #s.size = size
- ##self.setPoints([{'size':s.size, 'pos':s.pos(), 'data':s.data} for s in self.spots])
- #self.setPoints()
-
- #def paint(self, p, *args):
- #if not self.optimize:
- #return
- ##p.setClipRegion(self.boundingRect())
- #p.drawPixmapFragments(self.optimizeFragments, self.optimizePixmap)
-
- def paint(self, *args):
- if not self.spotsValid:
- self.generateSpots()
- def spotPixmap(self):
- ## If all spots are identical, return the pixmap to use for all spots
- ## Otherwise return None
-
- if not self.opts['identical']:
- return None
+ def defaultSpotPixmap(self):
+ ## Return the default spot pixmap
if self._spotPixmap is None:
self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
return self._spotPixmap
@@ -541,30 +461,18 @@ def boundingRect(self):
size.width()*.5, size.height()*.5)
return br
- #if xmn is None or xmx is None or ymn is None or ymx is None:
- #return QtCore.QRectF()
- #return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
- #return QtCore.QRectF(xmn-1, ymn-1, xmx-xmn+2, ymx-ymn+2)
-
- #def pointClicked(self, point):
- #self.sigPointClicked.emit(self, point)
-
def points(self):
- if not self.spotsValid:
- self.generateSpots()
- return self.spots[:]
-
+ return self.data['item']
+
def pointsAt(self, pos):
- if not self.spotsValid:
- self.generateSpots()
x = pos.x()
y = pos.y()
pw = self.pixelWidth()
ph = self.pixelHeight()
pts = []
- for s in self.spots:
+ for s in self.points():
sp = s.pos()
- ss = s.size
+ ss = s.size()
sx = sp.x()
sy = sp.y()
s2x = s2y = ss * 0.5
@@ -581,30 +489,6 @@ def pointsAt(self, pos):
return pts
- #def mousePressEvent(self, ev):
- #QtGui.QGraphicsItem.mousePressEvent(self, ev)
- #if ev.button() == QtCore.Qt.LeftButton:
- #pts = self.pointsAt(ev.pos())
- #if len(pts) > 0:
- #self.mouseMoved = False
- #self.ptsClicked = pts
- #ev.accept()
- #else:
- ##print "no spots"
- #ev.ignore()
- #else:
- #ev.ignore()
-
- #def mouseMoveEvent(self, ev):
- #QtGui.QGraphicsItem.mouseMoveEvent(self, ev)
- #self.mouseMoved = True
- #pass
-
- #def mouseReleaseEvent(self, ev):
- #QtGui.QGraphicsItem.mouseReleaseEvent(self, ev)
- #if not self.mouseMoved:
- #self.sigClicked.emit(self, self.ptsClicked)
-
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
pts = self.pointsAt(ev.pos())
@@ -667,8 +551,9 @@ def symbol(self):
if symbol == '':
symbol = self._plot.opts['symbol']
try:
- symbol = int(symbol)
- except:
+ n = int(symbol)
+ symbol = list(Symbols.keys())[n % len(Symbols)]
+ except:
pass
return symbol
@@ -736,7 +621,8 @@ def updateItem(self):
if symbolOpts == (None, None, -1, ''):
pixmap = self._plot.defaultSpotPixmap()
else:
- self.scale(size, size)
+ pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
+ self.setPixmap(pixmap)
def mapToScene(self, shape):
"""
Please sign in to comment.
Something went wrong with that request. Please try again.