ScatterPlotItem overhaul:

- performance improvements
  - removed 'identical' argument; this is now handled automatically
  - some minor API changes to SpotItem
This commit is contained in:
Luke Campagnola 2012-05-10 23:37:07 -04:00
parent 2a6cc84254
commit 13b201bebb
6 changed files with 332 additions and 405 deletions

View File

@ -28,19 +28,25 @@ print "Generating data, this takes a few seconds..."
## 1) All spots identical and transform-invariant (top-left plot). ## 1) All spots identical and transform-invariant (top-left plot).
## In this case we can get a huge performance boost by pre-rendering the spot ## In this case we can get a huge performance boost by pre-rendering the spot
## image and just drawing that image repeatedly. (use identical=True in the constructor) ## image and just drawing that image repeatedly.
## (An even faster approach might be to use QPainter.drawPixmapFragments)
n = 300 n = 300
s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 20), identical=True) s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120))
pos = np.random.normal(size=(2,n), scale=1e-5) pos = np.random.normal(size=(2,n), scale=1e-5)
spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}] spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}]
s1.addPoints(spots) s1.addPoints(spots)
w1.addItem(s1) w1.addItem(s1)
## This plot is clickable ## Make all plots clickable
lastClicked = []
def clicked(plot, points): def clicked(plot, points):
global lastClicked
for p in lastClicked:
p.resetPen()
print "clicked points", points print "clicked points", points
for p in points:
p.setPen('b', width=2)
lastClicked = points
s1.sigClicked.connect(clicked) s1.sigClicked.connect(clicked)
@ -72,12 +78,13 @@ w3.addItem(s3)
s3.sigClicked.connect(clicked) s3.sigClicked.connect(clicked)
## Coming: use qpainter.drawpixmapfragments for scatterplots which do not require mouse interaction ## Test performance of large scatterplots
s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 20), identical=True) s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 20))
pos = np.random.normal(size=(2,10000), scale=1e-9) pos = np.random.normal(size=(2,10000), scale=1e-9)
s4.addPoints(x=pos[0], y=pos[1]) s4.addPoints(x=pos[0], y=pos[1])
w4.addItem(s4) w4.addItem(s4)
s4.sigClicked.connect(clicked)

View File

@ -13,25 +13,23 @@ from pyqtgraph.ptime import time
app = QtGui.QApplication([]) app = QtGui.QApplication([])
#mw = QtGui.QMainWindow() #mw = QtGui.QMainWindow()
#mw.resize(800,800) #mw.resize(800,800)
from ScatterPlotSpeedTestTemplate import Ui_Form
p = pg.plot() win = QtGui.QWidget()
p.setRange(QtCore.QRectF(0, -10, 5000, 20)) ui = Ui_Form()
p.setLabel('bottom', 'Index', units='B') ui.setupUi(win)
win.show()
#curve.setFillBrush((0, 0, 100, 100)) p = ui.plot
#curve.setFillLevel(0)
#lr = pg.LinearRegionItem([100, 4900]) data = np.random.normal(size=(50,500), scale=100)
#p.addItem(lr)
data = np.random.normal(size=(50,5000))
ptr = 0 ptr = 0
lastTime = time() lastTime = time()
fps = None fps = None
def update(): def update():
global curve, data, ptr, p, lastTime, fps global curve, data, ptr, p, lastTime, fps
p.clear() p.clear()
curve = pg.ScatterPlotItem(x=data[ptr%10], y=data[(ptr+1)%10], pen='w', brush='b', size=10, pxMode=True, identical=True) curve = pg.ScatterPlotItem(x=data[ptr%10], y=data[(ptr+1)%10], pen='w', brush='b', size=10, pxMode=ui.pixelModeCheck.isChecked())
p.addItem(curve) p.addItem(curve)
ptr += 1 ptr += 1
now = time() now = time()

View File

@ -10,9 +10,10 @@ class GraphicsItem(object):
Abstract class providing useful methods to GraphicsObject and GraphicsWidget. Abstract class providing useful methods to GraphicsObject and GraphicsWidget.
(This is required because we cannot have multiple inheritance with QObject subclasses.) (This is required because we cannot have multiple inheritance with QObject subclasses.)
""" """
def __init__(self): def __init__(self, register=True):
self._viewWidget = None self._viewWidget = None
self._viewBox = None self._viewBox = None
if register:
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
def getViewWidget(self): def getViewWidget(self):

View File

@ -89,7 +89,7 @@ class PlotDataItem(GraphicsObject):
**Optimization keyword arguments:** **Optimization keyword arguments:**
========== ================================================ ========== ================================================
identical spots are all identical. The spot image will be rendered only once and repeated for every point identical *deprecated*
decimate (int) decimate data decimate (int) decimate data
========== ================================================ ========== ================================================

View File

@ -1,16 +1,20 @@
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point from pyqtgraph.Point import Point
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
from GraphicsItem import GraphicsItem
from GraphicsObject import GraphicsObject from GraphicsObject import GraphicsObject
import numpy as np import numpy as np
import scipy.stats import scipy.stats
import weakref
import pyqtgraph.debug as debug
from collections import OrderedDict
#import pyqtgraph as pg
__all__ = ['ScatterPlotItem', 'SpotItem'] __all__ = ['ScatterPlotItem', 'SpotItem']
## Build all symbol paths ## Build all symbol paths
Symbols = {name: QtGui.QPainterPath() for name in ['o', 's', 't', 'd', '+']} Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+']])
Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
coords = { coords = {
@ -29,6 +33,21 @@ for k, c in coords.iteritems():
Symbols[k].closeSubpath() Symbols[k].closeSubpath()
def makeSymbolPixmap(size, pen, brush, symbol):
## Render a spot with the given parameters to a pixmap
image = QtGui.QImage(size+2, size+2, QtGui.QImage.Format_ARGB32_Premultiplied)
image.fill(0)
p = QtGui.QPainter(image)
p.setRenderHint(p.Antialiasing)
p.translate(size*0.5+1, size*0.5+1)
p.scale(size, size)
p.setPen(pen)
p.setBrush(brush)
p.drawPath(Symbols[symbol])
p.end()
return QtGui.QPixmap(image)
class ScatterPlotItem(GraphicsObject): class ScatterPlotItem(GraphicsObject):
""" """
@ -56,25 +75,27 @@ class ScatterPlotItem(GraphicsObject):
""" """
Accepts the same arguments as setData() Accepts the same arguments as setData()
""" """
prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True)
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
self.data = None self.setFlag(self.ItemHasNoContents, True)
self.spots = [] 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.fragments = None
self.bounds = [None, None] self.bounds = [None, None]
self.opts = {} self.opts = {'pxMode': True}
self.spotsValid = False #self.spotsValid = False
#self.itemsValid = False
self._spotPixmap = None self._spotPixmap = None
self.setPen(200,200,200) self.setPen(200,200,200, update=False)
self.setBrush(100,100,150) self.setBrush(100,100,150, update=False)
self.setSymbol('o') self.setSymbol('o', update=False)
self.setSize(7) self.setSize(7, update=False)
self.setPxMode(True) #self.setIdentical(False, update=False)
self.setIdentical(False) prof.mark('1')
self.setData(*args, **kargs) self.setData(*args, **kargs)
prof.mark('setData')
prof.finish()
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """
@ -93,9 +114,6 @@ class ScatterPlotItem(GraphicsObject):
*pxMode* If True, spots are always the same size regardless of scaling, and size is given in px. *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. Otherwise, size is in scene coordinates and the spots scale with the view.
Default is True 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: *symbol* can be one (or a list) of:
* 'o' circle (default) * 'o' circle (default)
@ -108,11 +126,17 @@ class ScatterPlotItem(GraphicsObject):
*size* The size (or list of sizes) of spots. If *pxMode* is True, this value is in pixels. Otherwise, *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. it is in the item's local coordinate system.
*data* a list of python objects used to uniquely identify each spot. *data* a list of python objects used to uniquely identify each spot.
*identical* *Deprecated*. This functionality is handled automatically now.
====================== =============================================================================================== ====================== ===============================================================================================
""" """
self.clear() ## clear out all old data
self.addPoints(*args, **kargs)
self.clear() def addPoints(self, *args, **kargs):
"""
Add new points to the scatter plot.
Arguments are the same as setData()
"""
## deal with non-keyword arguments ## deal with non-keyword arguments
if len(args) == 1: if len(args) == 1:
@ -152,106 +176,66 @@ class ScatterPlotItem(GraphicsObject):
kargs['y'] = [] kargs['y'] = []
numPts = 0 numPts = 0
## create empty record array ## Extend record array
self.data = np.empty(numPts, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('spot', object)]) oldData = self.data
self.data['size'] = -1 ## indicates use default size self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype)
self.data['symbol'] = '' ## note that np.empty initializes object fields to None and string fields to ''
self.data['pen'] = None
self.data['brush'] = None self.data[:len(oldData)] = oldData
self.pointData = np.empty(numPts, dtype=object) for i in range(len(oldData)):
self.pointData[:] = None 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: if 'spots' in kargs:
spots = kargs['spots'] spots = kargs['spots']
for i in xrange(len(spots)): for i in xrange(len(spots)):
spot = spots[i] spot = spots[i]
for k in spot: for k in spot:
if k == 'pen': #if k == 'pen':
self.data[i][k] = fn.mkPen(spot[k]) #newData[k] = fn.mkPen(spot[k])
elif k == 'brush': #elif k == 'brush':
self.data[i][k] = fn.mkBrush(spot[k]) #newData[k] = fn.mkBrush(spot[k])
elif k == 'pos': if k == 'pos':
pos = spot[k] pos = spot[k]
if isinstance(pos, QtCore.QPointF): if isinstance(pos, QtCore.QPointF):
x,y = pos.x(), pos.y() x,y = pos.x(), pos.y()
else: else:
x,y = pos[0], pos[1] x,y = pos[0], pos[1]
self.data[i]['x'] = x newData[i]['x'] = x
self.data[i]['y'] = y newData[i]['y'] = y
elif k in ['x', 'y', 'size', 'symbol']: elif k in ['x', 'y', 'size', 'symbol', 'pen', 'brush', 'data']:
self.data[i][k] = spot[k] newData[i][k] = spot[k]
elif k == 'data': #elif k == 'data':
self.pointData[i] = spot[k] #self.pointData[i] = spot[k]
else: else:
raise Exception("Unknown spot parameter: %s" % k) raise Exception("Unknown spot parameter: %s" % k)
elif 'y' in kargs: elif 'y' in kargs:
self.data['x'] = kargs['x'] newData['x'] = kargs['x']
self.data['y'] = kargs['y'] newData['y'] = kargs['y']
if 'pxMode' in kargs:
self.setPxMode(kargs['pxMode'], update=False)
## Set any extra parameters provided in keyword arguments ## Set any extra parameters provided in keyword arguments
for k in ['pxMode', 'identical', 'pen', 'brush', 'symbol', 'size']: for k in ['pen', 'brush', 'symbol', 'size']:
if k in kargs: if k in kargs:
setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
setMethod(kargs[k]) setMethod(kargs[k], update=False, dataSet=newData)
if 'data' in kargs: if 'data' in kargs:
self.setPointData(kargs['data']) self.setPointData(kargs['data'], dataSet=newData)
self.updateSpots() #self.updateSpots()
self.generateSpotItems()
self.sigPlotChanged.emit(self)
#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')
#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): 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):
#"""
#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): def implements(self, interface=None):
ints = ['plotData'] ints = ['plotData']
if interface is None: if interface is None:
@ -259,88 +243,126 @@ class ScatterPlotItem(GraphicsObject):
return interface in ints return interface in ints
def setPen(self, *args, **kargs): 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)): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
pens = args[0] pens = args[0]
if self.data is None: if len(pens) != len(dataSet):
raise Exception("Must set data before setting multiple pens.") raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet)))
if len(pens) != len(self.data): dataSet['pen'] = pens
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])
else: else:
self.opts['pen'] = fn.mkPen(*args, **kargs) self.opts['pen'] = fn.mkPen(*args, **kargs)
self.updateSpots() self._spotPixmap = None
if update:
self.updateSpots(dataSet)
def setBrush(self, *args, **kargs): 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)): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
brushes = args[0] brushes = args[0]
if self.data is None: if len(brushes) != len(dataSet):
raise Exception("Must set data before setting multiple brushes.") raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet)))
if len(brushes) != len(self.data): #for i in xrange(len(brushes)):
raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(self.data))) #self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs)
for i in xrange(len(brushes)): dataSet['brush'] = brushes
self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs)
else: else:
self.opts['brush'] = fn.mkBrush(*args, **kargs) self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.updateSpots() self._spotPixmap = None
if update:
self.updateSpots(dataSet)
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
def setSymbol(self, symbol):
if isinstance(symbol, np.ndarray) or isinstance(symbol, list): if isinstance(symbol, np.ndarray) or isinstance(symbol, list):
symbols = symbol symbols = symbol
if self.data is None: if len(symbols) != len(dataSet):
raise Exception("Must set data before setting multiple symbols.") raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet)))
if len(symbols) != len(self.data): dataSet['symbol'] = symbols
raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(self.data)))
self.data['symbol'] = symbols
else: else:
self.opts['symbol'] = symbol self.opts['symbol'] = symbol
self.updateSpots() self._spotPixmap = None
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
def setSize(self, size):
if isinstance(size, np.ndarray) or isinstance(size, list): if isinstance(size, np.ndarray) or isinstance(size, list):
sizes = size sizes = size
if self.data is None: if len(sizes) != len(dataSet):
raise Exception("Must set data before setting multiple sizes.") raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(dataSet)))
if len(sizes) != len(self.data): dataSet['size'] = sizes
raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(self.data)))
self.data['size'] = sizes
else: else:
self.opts['size'] = size self.opts['size'] = size
self.updateSpots() self._spotPixmap = None
if update:
self.updateSpots(dataSet)
def setPointData(self, data, dataSet=None):
if dataSet is None:
dataSet = self.data
def setPointData(self, data):
if isinstance(data, np.ndarray) or isinstance(data, list): if isinstance(data, np.ndarray) or isinstance(data, list):
if self.data is None: if len(data) != len(dataSet):
raise Exception("Must set xy data before setting meta data.") raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet)))
if len(data) != len(self.data): dataSet['data'] = 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()
def setPxMode(self, mode, update=True):
if self.opts['pxMode'] == mode:
return
def setIdentical(self, ident):
self.opts['identical'] = ident
self.updateSpots()
def setPxMode(self, mode):
self.opts['pxMode'] = mode self.opts['pxMode'] = mode
self.updateSpots() self.clearItems()
if update:
self.generateSpotItems()
def updateSpots(self): def updateSpots(self, dataSet=None):
self.spotsValid = False if dataSet is None:
self.update() dataSet = self.data
for spot in dataSet['item']:
spot.updateItem()
def clear(self): 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) i.setParentItem(None)
s = i.scene() s = i.scene()
if s is not None: if s is not None:
s.removeItem(i) s.removeItem(i)
self.spots = [] self.data['item'] = None
self.data = None
self.spotsValid = False
self.bounds = [None, None]
def dataBounds(self, ax, frac=1.0, orthoRange=None): def dataBounds(self, ax, frac=1.0, orthoRange=None):
if frac >= 1.0 and self.bounds[ax] is not None: if frac >= 1.0 and self.bounds[ax] is not None:
@ -379,147 +401,25 @@ class ScatterPlotItem(GraphicsObject):
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[:] def generateSpotItems(self):
#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 = []
#if self.opts['identical'] and self.opts['pxMode']:
#pm = self.spotPixmap()
#sr = QtCore.QRectF(0, 0, pm.width(), pm.height())
#self.fragments = []
#for i in range(len(self.data)):
#self.fragments.append(QtGui.QPainter.PixmapFragment.create(QtCore.QPointF(self.data[i]['x'], self.data[i]['y']), sr))
#self.spotsValid = True
#self.sigPlotChanged.emit(self)
#return
#else:
#self.fragments = None
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']: if self.opts['pxMode']:
psize = 0 for rec in self.data:
if rec['item'] is None:
rec['item'] = PixmapSpotItem(rec, self)
else: else:
psize = size[i] for rec in self.data:
if rec['item'] is None:
if self.pointData is None or self.pointData[i] is None: rec['item'] = PathSpotItem(rec, self)
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
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
def defaultSpotPixmap(self):
#def setPointSize(self, size): ## Return the default spot pixmap
#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, p, *args):
if not self.spotsValid:
self.generateSpots()
#if self.fragments is not None:
#pm = self.spotPixmap()
#p.drawPixmapFragments(self.fragments, pm)
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
if self._spotPixmap is None: if self._spotPixmap is None:
spot = SpotItem(size=self.opts['size'], pxMode=True, brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol']) self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
self._spotPixmap = spot.pixmap
return self._spotPixmap return self._spotPixmap
def mkSpot(self, pos, size, pxMode, brush, pen, data, symbol=None, index=None):
## Make and return a SpotItem (or PixmapSpotItem if in pxMode)
brush = fn.mkBrush(brush)
pen = fn.mkPen(pen)
if pxMode:
img = self.spotPixmap() ## returns None if not using identical mode
#item = PixmapSpotItem(size, brush, pen, data, image=img, symbol=symbol, index=index)
item = SpotItem(size, pxMode, brush, pen, data, symbol=symbol, image=img, index=index)
else:
item = SpotItem(size, pxMode, brush, pen, data, symbol=symbol, index=index)
item.setParentItem(self)
item.setPos(pos)
#item.sigClicked.connect(self.pointClicked)
return item
def boundingRect(self): def boundingRect(self):
(xmn, xmx) = self.dataBounds(ax=0) (xmn, xmx) = self.dataBounds(ax=0)
(ymn, ymx) = self.dataBounds(ax=1) (ymn, ymx) = self.dataBounds(ax=1)
@ -531,30 +431,18 @@ class ScatterPlotItem(GraphicsObject):
ymx = 0 ymx = 0
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
#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): def points(self):
if not self.spotsValid: return self.data['item']
self.generateSpots()
return self.spots[:]
def pointsAt(self, pos): def pointsAt(self, pos):
if not self.spotsValid:
self.generateSpots()
x = pos.x() x = pos.x()
y = pos.y() y = pos.y()
pw = self.pixelWidth() pw = self.pixelWidth()
ph = self.pixelHeight() ph = self.pixelHeight()
pts = [] pts = []
for s in self.spots: for s in self.points():
sp = s.pos() sp = s.pos()
ss = s.size ss = s.size()
sx = sp.x() sx = sp.x()
sy = sp.y() sy = sp.y()
s2x = s2y = ss * 0.5 s2x = s2y = ss * 0.5
@ -571,30 +459,6 @@ class ScatterPlotItem(GraphicsObject):
return pts 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): def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton: if ev.button() == QtCore.Qt.LeftButton:
pts = self.pointsAt(ev.pos()) pts = self.pointsAt(ev.pos())
@ -609,77 +473,131 @@ class ScatterPlotItem(GraphicsObject):
ev.ignore() ev.ignore()
class SpotItem(GraphicsItem):
"""
Class referring to individual spots in a scatter plot.
These can be retrieved by calling ScatterPlotItem.points() or
by connecting to the ScatterPlotItem's click signals.
"""
class SpotItem(GraphicsObject): def __init__(self, data, plot):
#sigClicked = QtCore.Signal(object) GraphicsItem.__init__(self, register=False)
self._data = data
self._plot = plot
#self._viewBox = None
#self._viewWidget = None
self.setParentItem(plot)
self.setPos(QtCore.QPointF(data['x'], data['y']))
self.updateItem()
def __init__(self, size, pxMode, brush, pen, data=None, symbol=None, image=None, index=None): def data(self):
GraphicsObject.__init__(self) """Return the user data associated with this spot."""
self.pxMode = pxMode return self._data['data']
def size(self):
"""Return the size of this spot.
If the spot has no explicit size set, then return the ScatterPlotItem's default size instead."""
if self._data['size'] == -1:
return self._plot.opts['size']
else:
return self._data['size']
def setSize(self, size):
"""Set the size of this spot.
If the size is set to -1, then the ScatterPlotItem's default size
will be used instead."""
self._data['size'] = size
self.updateItem()
def symbol(self):
"""Return the symbol of this spot.
If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead.
"""
symbol = self._data['symbol']
if symbol == '':
symbol = self._plot.opts['symbol']
try: try:
symbol = int(symbol) n = int(symbol)
symbol = Symbols.keys()[n % len(Symbols)]
except: except:
pass pass
return symbol
if symbol is None: def setSymbol(self, symbol):
symbol = 'o' ## circle by default """Set the symbol for this spot.
elif isinstance(symbol, int): ## allow symbols specified by integer for easy iteration If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead."""
symbol = ['o', 's', 't', 'd', '+'][symbol] self._data['symbol'] = symbol
self.updateItem()
def pen(self):
pen = self._data['pen']
if pen is None:
pen = self._plot.opts['pen']
return fn.mkPen(pen)
def setPen(self, *args, **kargs):
"""Set the outline pen for this spot"""
pen = fn.mkPen(*args, **kargs)
self._data['pen'] = pen
self.updateItem()
def resetPen(self):
"""Remove the pen set for this spot; the scatter plot's default pen will be used instead."""
self._data['pen'] = None ## Note this is NOT the same as calling setPen(None)
self.updateItem()
def brush(self):
brush = self._data['brush']
if brush is None:
brush = self._plot.opts['brush']
return fn.mkBrush(brush)
def setBrush(self, *args, **kargs):
"""Set the fill brush for this spot"""
brush = fn.mkBrush(*args, **kargs)
self._data['brush'] = brush
self.updateItem()
def resetBrush(self):
"""Remove the brush set for this spot; the scatter plot's default brush will be used instead."""
self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None)
self.updateItem()
def setData(self, data):
"""Set the user-data associated with this spot"""
self._data['data'] = data
class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem):
def __init__(self, data, plot):
QtGui.QGraphicsPixmapItem.__init__(self)
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
SpotItem.__init__(self, data, plot)
####print 'SpotItem symbol: ', symbol def setPixmap(self, pixmap):
self.data = data QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap)
self.pen = pen self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.)
self.brush = brush
self.size = size
self.index = index
self.symbol = symbol
#s2 = size/2.
self.path = Symbols[symbol]
if pxMode: def updateItem(self):
## pre-render an image of the spot and display this rather than redrawing every time. symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol'])
if image is None:
self.pixmap = self.makeSpotImage(size, pen, brush, symbol) ## If all symbol options are default, use default pixmap
else: if symbolOpts == (None, None, -1, ''):
self.pixmap = image ## image is already provided (probably shared with other spots) pixmap = self._plot.defaultSpotPixmap()
self.setFlags(self.flags() | self.ItemIgnoresTransformations | self.ItemHasNoContents)
self.pi = QtGui.QGraphicsPixmapItem(self.pixmap, self)
self.pi.setPos(-0.5*size, -0.5*size)
else: else:
pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
self.setPixmap(pixmap)
class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
def __init__(self, data, plot):
QtGui.QGraphicsPathItem.__init__(self)
SpotItem.__init__(self, data, plot)
def updateItem(self):
QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()])
QtGui.QGraphicsPathItem.setPen(self, self.pen())
QtGui.QGraphicsPathItem.setBrush(self, self.brush())
size = self.size()
self.resetTransform()
self.scale(size, size) self.scale(size, size)
def makeSpotImage(self, size, pen, brush, symbol=None):
self.spotImage = QtGui.QImage(size+2, size+2, QtGui.QImage.Format_ARGB32_Premultiplied)
self.spotImage.fill(0)
p = QtGui.QPainter(self.spotImage)
p.setRenderHint(p.Antialiasing)
p.translate(size*0.5+1, size*0.5+1)
p.scale(size, size)
self.paint(p, None, None)
p.end()
return QtGui.QPixmap(self.spotImage)
def setBrush(self, brush):
self.brush = fn.mkBrush(brush)
self.update()
def setPen(self, pen):
self.pen = fn.mkPen(pen)
self.update()
def boundingRect(self):
return self.path.boundingRect()
def shape(self):
return self.path
def paint(self, p, *opts):
p.setPen(self.pen)
p.setBrush(self.brush)
p.drawPath(self.path)

View File

@ -52,6 +52,9 @@ class GraphicsView(QtGui.QGraphicsView):
self.setCacheMode(self.CacheBackground) self.setCacheMode(self.CacheBackground)
## This might help, but it's probably dangerous in the general case..
#self.setOptimizationFlag(self.DontSavePainterState, True)
if background is not None: if background is not None:
brush = fn.mkBrush(background) brush = fn.mkBrush(background)
self.setBackgroundBrush(brush) self.setBackgroundBrush(brush)