ScatterPlotItem overhaul:
- performance improvements - removed 'identical' argument; this is now handled automatically - some minor API changes to SpotItem
This commit is contained in:
parent
2a6cc84254
commit
13b201bebb
@ -28,19 +28,25 @@ print "Generating data, this takes a few seconds..."
|
||||
|
||||
## 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
|
||||
## image and just drawing that image repeatedly. (use identical=True in the constructor)
|
||||
## (An even faster approach might be to use QPainter.drawPixmapFragments)
|
||||
## image and just drawing that image repeatedly.
|
||||
|
||||
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)
|
||||
spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}]
|
||||
s1.addPoints(spots)
|
||||
w1.addItem(s1)
|
||||
|
||||
## This plot is clickable
|
||||
## Make all plots clickable
|
||||
lastClicked = []
|
||||
def clicked(plot, points):
|
||||
global lastClicked
|
||||
for p in lastClicked:
|
||||
p.resetPen()
|
||||
print "clicked points", points
|
||||
for p in points:
|
||||
p.setPen('b', width=2)
|
||||
lastClicked = points
|
||||
s1.sigClicked.connect(clicked)
|
||||
|
||||
|
||||
@ -72,12 +78,13 @@ w3.addItem(s3)
|
||||
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)
|
||||
s4.addPoints(x=pos[0], y=pos[1])
|
||||
w4.addItem(s4)
|
||||
s4.sigClicked.connect(clicked)
|
||||
|
||||
|
||||
|
||||
|
@ -13,25 +13,23 @@ from pyqtgraph.ptime import time
|
||||
app = QtGui.QApplication([])
|
||||
#mw = QtGui.QMainWindow()
|
||||
#mw.resize(800,800)
|
||||
from ScatterPlotSpeedTestTemplate import Ui_Form
|
||||
|
||||
p = pg.plot()
|
||||
p.setRange(QtCore.QRectF(0, -10, 5000, 20))
|
||||
p.setLabel('bottom', 'Index', units='B')
|
||||
win = QtGui.QWidget()
|
||||
ui = Ui_Form()
|
||||
ui.setupUi(win)
|
||||
win.show()
|
||||
|
||||
#curve.setFillBrush((0, 0, 100, 100))
|
||||
#curve.setFillLevel(0)
|
||||
p = ui.plot
|
||||
|
||||
#lr = pg.LinearRegionItem([100, 4900])
|
||||
#p.addItem(lr)
|
||||
|
||||
data = np.random.normal(size=(50,5000))
|
||||
data = np.random.normal(size=(50,500), scale=100)
|
||||
ptr = 0
|
||||
lastTime = time()
|
||||
fps = None
|
||||
def update():
|
||||
global curve, data, ptr, p, lastTime, fps
|
||||
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)
|
||||
ptr += 1
|
||||
now = time()
|
||||
|
@ -10,10 +10,11 @@ class GraphicsItem(object):
|
||||
Abstract class providing useful methods to GraphicsObject and GraphicsWidget.
|
||||
(This is required because we cannot have multiple inheritance with QObject subclasses.)
|
||||
"""
|
||||
def __init__(self):
|
||||
def __init__(self, register=True):
|
||||
self._viewWidget = None
|
||||
self._viewBox = None
|
||||
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
|
||||
if register:
|
||||
GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
|
||||
|
||||
def getViewWidget(self):
|
||||
"""
|
||||
|
@ -89,7 +89,7 @@ class PlotDataItem(GraphicsObject):
|
||||
**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
|
||||
========== ================================================
|
||||
|
||||
|
@ -1,16 +1,20 @@
|
||||
from pyqtgraph.Qt import QtGui, QtCore
|
||||
from pyqtgraph.Point import Point
|
||||
import pyqtgraph.functions as fn
|
||||
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']
|
||||
|
||||
|
||||
## 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['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
|
||||
coords = {
|
||||
@ -29,6 +33,21 @@ for k, c in coords.iteritems():
|
||||
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):
|
||||
"""
|
||||
@ -56,25 +75,27 @@ class ScatterPlotItem(GraphicsObject):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
@ -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.
|
||||
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)
|
||||
@ -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,
|
||||
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:
|
||||
@ -152,106 +176,66 @@ class ScatterPlotItem(GraphicsObject):
|
||||
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)):
|
||||
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 ['pxMode', 'identical', 'pen', 'brush', 'symbol', 'size']:
|
||||
for k in ['pen', 'brush', 'symbol', 'size']:
|
||||
if k in kargs:
|
||||
setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
|
||||
setMethod(kargs[k])
|
||||
|
||||
setMethod(kargs[k], update=False, dataSet=newData)
|
||||
|
||||
if 'data' in kargs:
|
||||
self.setPointData(kargs['data'])
|
||||
|
||||
self.updateSpots()
|
||||
self.setPointData(kargs['data'], dataSet=newData)
|
||||
|
||||
#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):
|
||||
##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:
|
||||
@ -259,89 +243,127 @@ class ScatterPlotItem(GraphicsObject):
|
||||
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.updateSpots()
|
||||
self.clearItems()
|
||||
if update:
|
||||
self.generateSpotItems()
|
||||
|
||||
def updateSpots(self):
|
||||
self.spotsValid = False
|
||||
self.update()
|
||||
def updateSpots(self, dataSet=None):
|
||||
if dataSet is None:
|
||||
dataSet = self.data
|
||||
for spot in dataSet['item']:
|
||||
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, orthoRange=None):
|
||||
if frac >= 1.0 and self.bounds[ax] is not None:
|
||||
return self.bounds[ax]
|
||||
@ -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[:]
|
||||
#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']:
|
||||
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, 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
|
||||
def defaultSpotPixmap(self):
|
||||
## Return the default spot pixmap
|
||||
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 = spot.pixmap
|
||||
self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
|
||||
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):
|
||||
(xmn, xmx) = self.dataBounds(ax=0)
|
||||
(ymn, ymx) = self.dataBounds(ax=1)
|
||||
@ -531,30 +431,18 @@ class ScatterPlotItem(GraphicsObject):
|
||||
ymx = 0
|
||||
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):
|
||||
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
|
||||
@ -571,30 +459,6 @@ class ScatterPlotItem(GraphicsObject):
|
||||
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())
|
||||
@ -609,77 +473,131 @@ class ScatterPlotItem(GraphicsObject):
|
||||
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):
|
||||
#sigClicked = QtCore.Signal(object)
|
||||
def __init__(self, data, plot):
|
||||
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):
|
||||
GraphicsObject.__init__(self)
|
||||
self.pxMode = pxMode
|
||||
|
||||
try:
|
||||
symbol = int(symbol)
|
||||
except:
|
||||
pass
|
||||
|
||||
if symbol is None:
|
||||
symbol = 'o' ## circle by default
|
||||
elif isinstance(symbol, int): ## allow symbols specified by integer for easy iteration
|
||||
symbol = ['o', 's', 't', 'd', '+'][symbol]
|
||||
|
||||
|
||||
|
||||
####print 'SpotItem symbol: ', symbol
|
||||
self.data = data
|
||||
self.pen = pen
|
||||
self.brush = brush
|
||||
self.size = size
|
||||
self.index = index
|
||||
self.symbol = symbol
|
||||
#s2 = size/2.
|
||||
self.path = Symbols[symbol]
|
||||
|
||||
if pxMode:
|
||||
## pre-render an image of the spot and display this rather than redrawing every time.
|
||||
if image is None:
|
||||
self.pixmap = self.makeSpotImage(size, pen, brush, symbol)
|
||||
else:
|
||||
self.pixmap = image ## image is already provided (probably shared with other spots)
|
||||
self.setFlags(self.flags() | self.ItemIgnoresTransformations | self.ItemHasNoContents)
|
||||
self.pi = QtGui.QGraphicsPixmapItem(self.pixmap, self)
|
||||
self.pi.setPos(-0.5*size, -0.5*size)
|
||||
def data(self):
|
||||
"""Return the user data associated with this spot."""
|
||||
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:
|
||||
self.scale(size, size)
|
||||
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:
|
||||
n = int(symbol)
|
||||
symbol = Symbols.keys()[n % len(Symbols)]
|
||||
except:
|
||||
pass
|
||||
return symbol
|
||||
|
||||
def setSymbol(self, symbol):
|
||||
"""Set the symbol for this spot.
|
||||
If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead."""
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
def setPixmap(self, pixmap):
|
||||
QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap)
|
||||
self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.)
|
||||
|
||||
def updateItem(self):
|
||||
symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol'])
|
||||
|
||||
## If all symbol options are default, use default pixmap
|
||||
if symbolOpts == (None, None, -1, ''):
|
||||
pixmap = self._plot.defaultSpotPixmap()
|
||||
else:
|
||||
pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
|
||||
self.setPixmap(pixmap)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
@ -52,6 +52,9 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
|
||||
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:
|
||||
brush = fn.mkBrush(background)
|
||||
self.setBackgroundBrush(brush)
|
||||
|
Loading…
Reference in New Issue
Block a user