10-100x speedup for ScatterPlotItem

This commit is contained in:
Luke Campagnola 2012-10-16 17:07:23 -04:00
parent 50aa289ab6
commit 5ce8d09aa0
4 changed files with 496 additions and 195 deletions

View File

@ -26,7 +26,7 @@ win.show()
p = ui.plot p = ui.plot
data = np.random.normal(size=(50,500), scale=100) data = np.random.normal(size=(50,500), scale=100)
sizeArray = np.random.random(500) * 20. sizeArray = (np.random.random(500) * 20.).astype(int)
ptr = 0 ptr = 0
lastTime = time() lastTime = time()
fps = None fps = None

View File

@ -25,6 +25,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY'
from .Qt import QtGui, QtCore from .Qt import QtGui, QtCore
import numpy as np import numpy as np
import decimal, re import decimal, re
import ctypes
try: try:
import scipy.ndimage import scipy.ndimage
@ -223,13 +224,15 @@ def mkColor(*args):
return QtGui.QColor(*args) return QtGui.QColor(*args)
def mkBrush(*args): def mkBrush(*args, **kwds):
""" """
| Convenience function for constructing Brush. | Convenience function for constructing Brush.
| This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>` | This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>`
| Calling mkBrush(None) returns an invisible brush. | Calling mkBrush(None) returns an invisible brush.
""" """
if len(args) == 1: if 'color' in kwds:
color = kwds['color']
elif len(args) == 1:
arg = args[0] arg = args[0]
if arg is None: if arg is None:
return QtGui.QBrush(QtCore.Qt.NoBrush) return QtGui.QBrush(QtCore.Qt.NoBrush)
@ -237,7 +240,7 @@ def mkBrush(*args):
return QtGui.QBrush(arg) return QtGui.QBrush(arg)
else: else:
color = arg color = arg
if len(args) > 1: elif len(args) > 1:
color = args color = args
return QtGui.QBrush(mkColor(color)) return QtGui.QBrush(mkColor(color))
@ -779,30 +782,107 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
return imgData, alpha return imgData, alpha
def makeQImage(imgData, alpha): def makeQImage(imgData, alpha=None, copy=True, transpose=True):
"""Turn an ARGB array into QImage""" """
Turn an ARGB array into QImage.
By default, the data is copied; changes to the array will not
be reflected in the image. The image will be given a 'data' attribute
pointing to the array which shares its data to prevent python
freeing that memory while the image is in use.
=========== ===================================================================
Arguments:
imgData Array of data to convert. Must have shape (width, height, 3 or 4)
and dtype=ubyte. The order of values in the 3rd axis must be
(b, g, r, a).
alpha If True, the QImage returned will have format ARGB32. If False,
the format will be RGB32. By default, _alpha_ is True if
array.shape[2] == 4.
copy If True, the data is copied before converting to QImage.
If False, the new QImage points directly to the data in the array.
Note that the array must be contiguous for this to work.
transpose If True (the default), the array x/y axes are transposed before
creating the image. Note that Qt expects the axes to be in
(height, width) order whereas pyqtgraph usually prefers the
opposite.
=========== ===================================================================
"""
## create QImage from buffer ## create QImage from buffer
prof = debug.Profiler('functions.makeQImage', disabled=True) prof = debug.Profiler('functions.makeQImage', disabled=True)
## If we didn't explicitly specify alpha, check the array shape.
if alpha is None:
alpha = (imgData.shape[2] == 4)
copied = False
if imgData.shape[2] == 3: ## need to make alpha channel (even if alpha==False; QImage requires 32 bpp)
if copy is True:
d2 = np.empty(imgData.shape[:2] + (4,), dtype=imgData.dtype)
d2[:,:,:3] = imgData
d2[:,:,3] = 255
imgData = d2
copied = True
else:
raise Exception('Array has only 3 channels; cannot make QImage without copying.')
if alpha: if alpha:
imgFormat = QtGui.QImage.Format_ARGB32 imgFormat = QtGui.QImage.Format_ARGB32
else: else:
imgFormat = QtGui.QImage.Format_RGB32 imgFormat = QtGui.QImage.Format_RGB32
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite if transpose:
try: imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
buf = imgData.data
except AttributeError: ## happens when image data is non-contiguous if not imgData.flags['C_CONTIGUOUS']:
if copy is False:
extra = ' (try setting transpose=False)' if transpose else ''
raise Exception('Array is not contiguous; cannot make QImage without copying.'+extra)
imgData = np.ascontiguousarray(imgData) imgData = np.ascontiguousarray(imgData)
buf = imgData.data copied = True
prof.mark('1') if copy is True and copied is False:
qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) imgData = imgData.copy()
prof.mark('2')
qimage.data = imgData addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
prof.finish() img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
return qimage img.data = imgData
return img
#try:
#buf = imgData.data
#except AttributeError: ## happens when image data is non-contiguous
#buf = imgData.data
#prof.mark('1')
#qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
#prof.mark('2')
#qimage.data = imgData
#prof.finish()
#return qimage
def imageToArray(img, copy=False, transpose=True):
"""
Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied.
By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if
the QImage is collected before the array, there may be trouble).
The array will have shape (width, height, (b,g,r,a)).
"""
ptr = img.bits()
ptr.setsize(img.byteCount())
fmt = img.format()
if fmt == img.Format_RGB32:
arr = np.asarray(ptr).reshape(img.height(), img.width(), 3)
elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied:
arr = np.asarray(ptr).reshape(img.height(), img.width(), 4)
if copy:
arr = arr.copy()
if transpose:
return arr.transpose((1,0,2))
else:
return arr
def rescaleData(data, scale, offset): def rescaleData(data, scale, offset):
newData = np.empty((data.size,), dtype=np.int) newData = np.empty((data.size,), dtype=np.int)

View File

@ -130,6 +130,8 @@ class PlotDataItem(GraphicsObject):
'symbolBrush': (50, 50, 150), 'symbolBrush': (50, 50, 150),
'pxMode': True, 'pxMode': True,
'pointMode': None,
'data': None, 'data': None,
} }
self.setData(*args, **kargs) self.setData(*args, **kargs)
@ -144,22 +146,30 @@ class PlotDataItem(GraphicsObject):
return QtCore.QRectF() ## let child items handle this return QtCore.QRectF() ## let child items handle this
def setAlpha(self, alpha, auto): def setAlpha(self, alpha, auto):
if self.opts['alphaHint'] == alpha and self.opts['alphaMode'] == auto:
return
self.opts['alphaHint'] = alpha self.opts['alphaHint'] = alpha
self.opts['alphaMode'] = auto self.opts['alphaMode'] = auto
self.setOpacity(alpha) self.setOpacity(alpha)
#self.update() #self.update()
def setFftMode(self, mode): def setFftMode(self, mode):
if self.opts['fftMode'] == mode:
return
self.opts['fftMode'] = mode self.opts['fftMode'] = mode
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
def setLogMode(self, xMode, yMode): def setLogMode(self, xMode, yMode):
self.opts['logMode'] = (xMode, yMode) if self.opts['logMode'] == [xMode, yMode]:
return
self.opts['logMode'] = [xMode, yMode]
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
def setPointMode(self, mode): def setPointMode(self, mode):
if self.opts['pointMode'] == mode:
return
self.opts['pointMode'] = mode self.opts['pointMode'] = mode
self.update() self.update()
@ -193,6 +203,8 @@ class PlotDataItem(GraphicsObject):
def setFillBrush(self, *args, **kargs): def setFillBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs) brush = fn.mkBrush(*args, **kargs)
if self.opts['fillBrush'] == brush:
return
self.opts['fillBrush'] = brush self.opts['fillBrush'] = brush
self.updateItems() self.updateItems()
@ -200,16 +212,22 @@ class PlotDataItem(GraphicsObject):
return self.setFillBrush(*args, **kargs) return self.setFillBrush(*args, **kargs)
def setFillLevel(self, level): def setFillLevel(self, level):
if self.opts['fillLevel'] == level:
return
self.opts['fillLevel'] = level self.opts['fillLevel'] = level
self.updateItems() self.updateItems()
def setSymbol(self, symbol): def setSymbol(self, symbol):
if self.opts['symbol'] == symbol:
return
self.opts['symbol'] = symbol self.opts['symbol'] = symbol
#self.scatter.setSymbol(symbol) #self.scatter.setSymbol(symbol)
self.updateItems() self.updateItems()
def setSymbolPen(self, *args, **kargs): def setSymbolPen(self, *args, **kargs):
pen = fn.mkPen(*args, **kargs) pen = fn.mkPen(*args, **kargs)
if self.opts['symbolPen'] == pen:
return
self.opts['symbolPen'] = pen self.opts['symbolPen'] = pen
#self.scatter.setSymbolPen(pen) #self.scatter.setSymbolPen(pen)
self.updateItems() self.updateItems()
@ -218,21 +236,26 @@ class PlotDataItem(GraphicsObject):
def setSymbolBrush(self, *args, **kargs): def setSymbolBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs) brush = fn.mkBrush(*args, **kargs)
if self.opts['symbolBrush'] == brush:
return
self.opts['symbolBrush'] = brush self.opts['symbolBrush'] = brush
#self.scatter.setSymbolBrush(brush) #self.scatter.setSymbolBrush(brush)
self.updateItems() self.updateItems()
def setSymbolSize(self, size): def setSymbolSize(self, size):
if self.opts['symbolSize'] == size:
return
self.opts['symbolSize'] = size self.opts['symbolSize'] = size
#self.scatter.setSymbolSize(symbolSize) #self.scatter.setSymbolSize(symbolSize)
self.updateItems() self.updateItems()
def setDownsampling(self, ds): def setDownsampling(self, ds):
if self.opts['downsample'] != ds: if self.opts['downsample'] == ds:
self.opts['downsample'] = ds return
self.xDisp = self.yDisp = None self.opts['downsample'] = ds
self.updateItems() self.xDisp = self.yDisp = None
self.updateItems()
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """

View File

@ -32,26 +32,165 @@ for k, c in coords.items():
Symbols[k].lineTo(x, y) Symbols[k].lineTo(x, y)
Symbols[k].closeSubpath() Symbols[k].closeSubpath()
def drawSymbol(painter, symbol, size, pen, brush):
painter.scale(size, size)
painter.setPen(pen)
painter.setBrush(brush)
if isinstance(symbol, basestring):
symbol = Symbols[symbol]
if np.isscalar(symbol):
symbol = Symbols.values()[symbol % len(Symbols)]
painter.drawPath(symbol)
def makeSymbolPixmap(size, pen, brush, symbol):
def renderSymbol(symbol, size, pen, brush, device=None):
"""
Render a symbol specification to QImage.
Symbol may be either a QPainterPath or one of the keys in the Symbols dict.
If *device* is None, a new QPixmap will be returned. Otherwise,
the symbol will be rendered into the device specified (See QPainter documentation
for more information).
"""
## see if this pixmap is already cached
#global SymbolPixmapCache
#key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color()))
#if key in SymbolPixmapCache:
#return SymbolPixmapCache[key]
## Render a spot with the given parameters to a pixmap ## Render a spot with the given parameters to a pixmap
penPxWidth = max(np.ceil(pen.width()), 1) penPxWidth = max(np.ceil(pen.width()), 1)
image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32_Premultiplied) image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
image.fill(0) image.fill(0)
p = QtGui.QPainter(image) p = QtGui.QPainter(image)
p.setRenderHint(p.Antialiasing) p.setRenderHint(p.Antialiasing)
p.translate(image.width()*0.5, image.height()*0.5) p.translate(image.width()*0.5, image.height()*0.5)
p.scale(size, size) drawSymbol(p, symbol, size, pen, brush)
p.setPen(pen)
p.setBrush(brush)
if isinstance(symbol, basestring):
symbol = Symbols[symbol]
p.drawPath(symbol)
p.end() p.end()
return QtGui.QPixmap(image) return image
#pixmap = QtGui.QPixmap(image)
#SymbolPixmapCache[key] = pixmap
#return pixmap
def makeSymbolPixmap(size, pen, brush, symbol):
## deprecated
img = renderSymbol(symbol, size, pen, brush)
return QtGui.QPixmap(img)
class SymbolAtlas:
"""
Used to efficiently construct a single QPixmap containing all rendered symbols
for a ScatterPlotItem. This is required for fragment rendering.
Use example:
atlas = SymbolAtlas()
sc1 = atlas.getSymbolCoords('o', 5, QPen(..), QBrush(..))
sc2 = atlas.getSymbolCoords('t', 10, QPen(..), QBrush(..))
pm = atlas.getAtlas()
"""
class SymbolCoords(list): ## needed because lists are not allowed in weak references.
pass
def __init__(self):
# symbol key : [x, y, w, h] atlas coordinates
# note that the coordinate list will always be the same list object as
# long as the symbol is in the atlas, but the coordinates may
# change if the atlas is rebuilt.
# weak value; if all external refs to this list disappear,
# the symbol will be forgotten.
self.symbolMap = weakref.WeakValueDictionary()
self.atlasData = None # numpy array of atlas image
self.atlas = None # atlas as QPixmap
self.atlasValid = False
def getSymbolCoords(self, symbol, size, pen, brush):
key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color()))
if key not in self.symbolMap:
newCoords = SymbolAtlas.SymbolCoords()
self.symbolMap[key] = newCoords
self.invalidateAtlas()
#try:
#self.addToAtlas(key) ## squeeze this into the atlas if there is room
#except:
#self.buildAtlas() ## otherwise, we need to rebuild
return self.symbolMap[key]
def invalidateAtlas(self):
self.atlasValid = False
def buildAtlas(self):
# get rendered array for all symbols, keep track of avg/max width
rendered = {}
avgWidth = 0.0
maxWidth = 0
images = []
for key, coords in self.symbolMap.items():
if len(coords) == 0:
pen = fn.mkPen(color=key[2], width=key[3], style=key[4])
brush = fn.mkBrush(color=key[5])
img = renderSymbol(key[0], key[1], pen, brush)
images.append(img) ## we only need this to prevent the images being garbage collected immediately
arr = fn.imageToArray(img, copy=False, transpose=False)
else:
(x,y,w,h) = self.symbolMap[key]
arr = self.atlasData[x:x+w, y:y+w]
rendered[key] = arr
w = arr.shape[0]
avgWidth += w
maxWidth = max(maxWidth, w)
nSymbols = len(rendered)
if nSymbols > 0:
avgWidth /= nSymbols
width = max(maxWidth, avgWidth * (nSymbols**0.5))
else:
avgWidth = 0
width = 0
# sort symbols by height
symbols = sorted(rendered.keys(), key=lambda x: rendered[x].shape[1], reverse=True)
self.atlasRows = []
x = width
y = 0
rowheight = 0
for key in symbols:
arr = rendered[key]
w,h = arr.shape[:2]
if x+w > width:
y += rowheight
x = 0
rowheight = h
self.atlasRows.append([y, rowheight, 0])
self.symbolMap[key][:] = x, y, w, h
x += w
self.atlasRows[-1][2] = x
height = y + rowheight
self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte)
for key in symbols:
x, y, w, h = self.symbolMap[key]
self.atlasData[x:x+w, y:y+h] = rendered[key]
self.atlas = None
self.atlasValid = True
def getAtlas(self):
if not self.atlasValid:
self.buildAtlas()
if self.atlas is None:
if len(self.atlasData) == 0:
return QtGui.QPixmap(0,0)
img = fn.makeQImage(self.atlasData, copy=False, transpose=False)
self.atlas = QtGui.QPixmap(img)
return self.atlas
class ScatterPlotItem(GraphicsObject): class ScatterPlotItem(GraphicsObject):
""" """
Displays a set of x/y points. Instances of this class are created Displays a set of x/y points. Instances of this class are created
@ -79,13 +218,16 @@ class ScatterPlotItem(GraphicsObject):
""" """
prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True) prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True)
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
self.setFlag(self.ItemHasNoContents, True)
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('item', object), ('data', object)]) self.picture = None # QPicture used for rendering when pxmode==False
self.fragments = None # fragment specification for pxmode; updated every time the view changes.
self.fragmentAtlas = SymbolAtlas()
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object)])
self.bounds = [None, None] ## caches data bounds self.bounds = [None, None] ## caches data bounds
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
self._spotPixmap = None self.opts = {'pxMode': True, 'useCache': True} ## If useCache is False, symbols are re-drawn on every paint.
self.opts = {'pxMode': True}
self.setPen(200,200,200, update=False) self.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False) self.setBrush(100,100,150, update=False)
@ -96,6 +238,8 @@ class ScatterPlotItem(GraphicsObject):
prof.mark('setData') prof.mark('setData')
prof.finish() prof.finish()
#self.setCacheMode(self.DeviceCoordinateCache)
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """
**Ordered Arguments:** **Ordered Arguments:**
@ -130,6 +274,7 @@ class ScatterPlotItem(GraphicsObject):
*identical* *Deprecated*. This functionality is handled automatically now. *identical* *Deprecated*. This functionality is handled automatically now.
====================== =============================================================================================== ====================== ===============================================================================================
""" """
oldData = self.data ## this causes cached pixmaps to be preserved while new data is registered.
self.clear() ## clear out all old data self.clear() ## clear out all old data
self.addPoints(*args, **kargs) self.addPoints(*args, **kargs)
@ -183,8 +328,8 @@ class ScatterPlotItem(GraphicsObject):
## note that np.empty initializes object fields to None and string fields to '' ## note that np.empty initializes object fields to None and string fields to ''
self.data[:len(oldData)] = oldData self.data[:len(oldData)] = oldData
for i in range(len(oldData)): #for i in range(len(oldData)):
oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array #oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array
newData = self.data[len(oldData):] newData = self.data[len(oldData):]
newData['size'] = -1 ## indicates to use default size newData['size'] = -1 ## indicates to use default size
@ -217,7 +362,7 @@ class ScatterPlotItem(GraphicsObject):
newData['y'] = kargs['y'] newData['y'] = kargs['y']
if 'pxMode' in kargs: if 'pxMode' in kargs:
self.setPxMode(kargs['pxMode'], update=False) self.setPxMode(kargs['pxMode'])
## Set any extra parameters provided in keyword arguments ## Set any extra parameters provided in keyword arguments
for k in ['pen', 'brush', 'symbol', 'size']: for k in ['pen', 'brush', 'symbol', 'size']:
@ -228,12 +373,18 @@ class ScatterPlotItem(GraphicsObject):
if 'data' in kargs: if 'data' in kargs:
self.setPointData(kargs['data'], dataSet=newData) self.setPointData(kargs['data'], dataSet=newData)
#self.updateSpots()
self.prepareGeometryChange() self.prepareGeometryChange()
self.bounds = [None, None] self.bounds = [None, None]
self.generateSpotItems() self.invalidate()
self.updateSpots(newData)
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
def invalidate(self):
## clear any cached drawing state
self.picture = None
self.fragments = None
self.update()
def getData(self): def getData(self):
return self.data['x'], self.data['y'] return self.data['x'], self.data['y']
@ -263,8 +414,8 @@ class ScatterPlotItem(GraphicsObject):
dataSet['pen'] = pens dataSet['pen'] = pens
else: else:
self.opts['pen'] = fn.mkPen(*args, **kargs) self.opts['pen'] = fn.mkPen(*args, **kargs)
self._spotPixmap = None
dataSet['fragCoords'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -285,8 +436,9 @@ class ScatterPlotItem(GraphicsObject):
dataSet['brush'] = brushes dataSet['brush'] = brushes
else: else:
self.opts['brush'] = fn.mkBrush(*args, **kargs) self.opts['brush'] = fn.mkBrush(*args, **kargs)
self._spotPixmap = None #self._spotPixmap = None
dataSet['fragCoords'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -307,6 +459,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['symbol'] = symbol self.opts['symbol'] = symbol
self._spotPixmap = None self._spotPixmap = None
dataSet['fragCoords'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -327,6 +480,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['size'] = size self.opts['size'] = size
self._spotPixmap = None self._spotPixmap = None
dataSet['fragCoords'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -346,34 +500,52 @@ class ScatterPlotItem(GraphicsObject):
else: else:
dataSet['data'] = data dataSet['data'] = data
def setPxMode(self, mode, update=True): def setPxMode(self, mode):
if self.opts['pxMode'] == mode: if self.opts['pxMode'] == mode:
return return
self.opts['pxMode'] = mode self.opts['pxMode'] = mode
self.clearItems() self.invalidate()
if update:
self.generateSpotItems()
def updateSpots(self, dataSet=None): def updateSpots(self, dataSet=None):
if dataSet is None: if dataSet is None:
dataSet = self.data dataSet = self.data
self._maxSpotWidth = 0 self._maxSpotWidth = 0
self._maxSpotPxWidth = 0 self._maxSpotPxWidth = 0
for spot in dataSet['item']: if self.opts['pxMode']:
spot.updateItem() for rec in dataSet:
if rec['fragCoords'] is None:
rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
self.measureSpotSizes(dataSet) self.measureSpotSizes(dataSet)
def getSpotOpts(self, rec):
symbol = rec['symbol']
if symbol is None:
symbol = self.opts['symbol']
size = rec['size']
if size < 0:
size = self.opts['size']
pen = rec['pen']
if pen is None:
pen = self.opts['pen']
brush = rec['brush']
if brush is None:
brush = self.opts['brush']
return (symbol, size, fn.mkPen(pen), fn.mkBrush(brush))
def measureSpotSizes(self, dataSet): def measureSpotSizes(self, dataSet):
for spot in dataSet['item']: for rec in dataSet:
## keep track of the maximum spot size and pixel size ## keep track of the maximum spot size and pixel size
symbol, size, pen, brush = self.getSpotOpts(rec)
width = 0 width = 0
pxWidth = 0 pxWidth = 0
pen = spot.pen()
if self.opts['pxMode']: if self.opts['pxMode']:
pxWidth = spot.size() + pen.width() pxWidth = size + pen.width()
else: else:
width = spot.size() width = size
if pen.isCosmetic(): if pen.isCosmetic():
pxWidth += pen.width() pxWidth += pen.width()
else: else:
@ -385,20 +557,11 @@ class ScatterPlotItem(GraphicsObject):
def clear(self): def clear(self):
"""Remove all spots from the scatter plot""" """Remove all spots from the scatter plot"""
self.clearItems() #self.clearItems()
self.data = np.empty(0, dtype=self.data.dtype) self.data = np.empty(0, dtype=self.data.dtype)
self.bounds = [None, None] self.bounds = [None, None]
self.invalidate()
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.data['item'] = 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:
return self.bounds[ax] return self.bounds[ax]
@ -436,28 +599,12 @@ class ScatterPlotItem(GraphicsObject):
else: else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
#def defaultSpotPixmap(self):
### Return the default spot pixmap
#if self._spotPixmap is None:
def generateSpotItems(self): #self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
if self.opts['pxMode']: #return self._spotPixmap
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.measureSpotSizes(self.data)
self.sigPlotChanged.emit(self)
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
def boundingRect(self): def boundingRect(self):
(xmn, xmx) = self.dataBounds(ax=0) (xmn, xmx) = self.dataBounds(ax=0)
@ -474,12 +621,63 @@ class ScatterPlotItem(GraphicsObject):
self.prepareGeometryChange() self.prepareGeometryChange()
GraphicsObject.viewRangeChanged(self) GraphicsObject.viewRangeChanged(self)
self.bounds = [None, None] self.bounds = [None, None]
self.fragments = None
def generateFragments(self):
tr = self.deviceTransform()
if tr is None:
return
pts = np.empty((2,len(self.data['x'])))
pts[0] = self.data['x']
pts[1] = self.data['y']
pts = fn.transformCoordinates(tr, pts)
self.fragments = []
for i in xrange(len(self.data)):
rec = self.data[i]
pos = QtCore.QPointF(pts[0,i], pts[1,i])
x,y,w,h = rec['fragCoords']
rect = QtCore.QRectF(y, x, h, w)
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
def paint(self, p, *args): def paint(self, p, *args):
## NOTE: self.paint is disabled by this line in __init__: #p.setPen(fn.mkPen('r'))
## self.setFlag(self.ItemHasNoContents, True) #p.drawRect(self.boundingRect())
p.setPen(fn.mkPen('r'))
p.drawRect(self.boundingRect()) if self.opts['pxMode']:
atlas = self.fragmentAtlas.getAtlas()
#arr = fn.imageToArray(atlas.toImage(), copy=True)
#if hasattr(self, 'lastAtlas'):
#if np.any(self.lastAtlas != arr):
#print "Atlas changed:", arr
#self.lastAtlas = arr
if self.fragments is None:
self.updateSpots()
self.generateFragments()
p.resetTransform()
if self.opts['useCache']:
p.drawPixmapFragments(self.fragments, atlas)
else:
for i in range(len(self.data)):
rec = self.data[i]
frag = self.fragments[i]
p.resetTransform()
p.translate(frag.x, frag.y)
drawSymbol(p, *self.getSpotOpts(rec))
else:
if self.picture is None:
self.picture = QtGui.QPicture()
p2 = QtGui.QPainter(self.picture)
for rec in self.data:
p2.resetTransform()
p2.translate(rec['x'], rec['y'])
drawSymbol(p2, *self.getSpotOpts(rec))
p2.end()
self.picture.play(p)
def points(self): def points(self):
@ -524,131 +722,131 @@ class ScatterPlotItem(GraphicsObject):
ev.ignore() ev.ignore()
class SpotItem(GraphicsItem): #class SpotItem(GraphicsItem):
""" #"""
Class referring to individual spots in a scatter plot. #Class referring to individual spots in a scatter plot.
These can be retrieved by calling ScatterPlotItem.points() or #These can be retrieved by calling ScatterPlotItem.points() or
by connecting to the ScatterPlotItem's click signals. #by connecting to the ScatterPlotItem's click signals.
""" #"""
def __init__(self, data, plot): #def __init__(self, data, plot):
GraphicsItem.__init__(self, register=False) #GraphicsItem.__init__(self, register=False)
self._data = data #self._data = data
self._plot = plot #self._plot = plot
#self._viewBox = None ##self._viewBox = None
#self._viewWidget = None ##self._viewWidget = None
self.setParentItem(plot) #self.setParentItem(plot)
self.setPos(QtCore.QPointF(data['x'], data['y'])) #self.setPos(QtCore.QPointF(data['x'], data['y']))
self.updateItem() #self.updateItem()
def data(self): #def data(self):
"""Return the user data associated with this spot.""" #"""Return the user data associated with this spot."""
return self._data['data'] #return self._data['data']
def size(self): #def size(self):
"""Return the size of this spot. #"""Return the size of this spot.
If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" #If the spot has no explicit size set, then return the ScatterPlotItem's default size instead."""
if self._data['size'] == -1: #if self._data['size'] == -1:
return self._plot.opts['size'] #return self._plot.opts['size']
else: #else:
return self._data['size'] #return self._data['size']
def setSize(self, size): #def setSize(self, size):
"""Set the size of this spot. #"""Set the size of this spot.
If the size is set to -1, then the ScatterPlotItem's default size #If the size is set to -1, then the ScatterPlotItem's default size
will be used instead.""" #will be used instead."""
self._data['size'] = size #self._data['size'] = size
self.updateItem() #self.updateItem()
def symbol(self): #def symbol(self):
"""Return the symbol of this spot. #"""Return the symbol of this spot.
If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. #If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead.
""" #"""
symbol = self._data['symbol'] #symbol = self._data['symbol']
if symbol is None: #if symbol is None:
symbol = self._plot.opts['symbol'] #symbol = self._plot.opts['symbol']
try: #try:
n = int(symbol) #n = int(symbol)
symbol = list(Symbols.keys())[n % len(Symbols)] #symbol = list(Symbols.keys())[n % len(Symbols)]
except: #except:
pass #pass
return symbol #return symbol
def setSymbol(self, symbol): #def setSymbol(self, symbol):
"""Set the symbol for this spot. #"""Set the symbol for this spot.
If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" #If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead."""
self._data['symbol'] = symbol #self._data['symbol'] = symbol
self.updateItem() #self.updateItem()
def pen(self): #def pen(self):
pen = self._data['pen'] #pen = self._data['pen']
if pen is None: #if pen is None:
pen = self._plot.opts['pen'] #pen = self._plot.opts['pen']
return fn.mkPen(pen) #return fn.mkPen(pen)
def setPen(self, *args, **kargs): #def setPen(self, *args, **kargs):
"""Set the outline pen for this spot""" #"""Set the outline pen for this spot"""
pen = fn.mkPen(*args, **kargs) #pen = fn.mkPen(*args, **kargs)
self._data['pen'] = pen #self._data['pen'] = pen
self.updateItem() #self.updateItem()
def resetPen(self): #def resetPen(self):
"""Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" #"""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._data['pen'] = None ## Note this is NOT the same as calling setPen(None)
self.updateItem() #self.updateItem()
def brush(self): #def brush(self):
brush = self._data['brush'] #brush = self._data['brush']
if brush is None: #if brush is None:
brush = self._plot.opts['brush'] #brush = self._plot.opts['brush']
return fn.mkBrush(brush) #return fn.mkBrush(brush)
def setBrush(self, *args, **kargs): #def setBrush(self, *args, **kargs):
"""Set the fill brush for this spot""" #"""Set the fill brush for this spot"""
brush = fn.mkBrush(*args, **kargs) #brush = fn.mkBrush(*args, **kargs)
self._data['brush'] = brush #self._data['brush'] = brush
self.updateItem() #self.updateItem()
def resetBrush(self): #def resetBrush(self):
"""Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" #"""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._data['brush'] = None ## Note this is NOT the same as calling setBrush(None)
self.updateItem() #self.updateItem()
def setData(self, data): #def setData(self, data):
"""Set the user-data associated with this spot""" #"""Set the user-data associated with this spot"""
self._data['data'] = data #self._data['data'] = data
class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): #class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem):
def __init__(self, data, plot): #def __init__(self, data, plot):
QtGui.QGraphicsPixmapItem.__init__(self) #QtGui.QGraphicsPixmapItem.__init__(self)
self.setFlags(self.flags() | self.ItemIgnoresTransformations) #self.setFlags(self.flags() | self.ItemIgnoresTransformations)
SpotItem.__init__(self, data, plot) #SpotItem.__init__(self, data, plot)
def setPixmap(self, pixmap): #def setPixmap(self, pixmap):
QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap) #QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap)
self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.) #self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.)
def updateItem(self): #def updateItem(self):
symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) #symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol'])
## If all symbol options are default, use default pixmap ### If all symbol options are default, use default pixmap
if symbolOpts == (None, None, -1, ''): #if symbolOpts == (None, None, -1, ''):
pixmap = self._plot.defaultSpotPixmap() #pixmap = self._plot.defaultSpotPixmap()
else: #else:
pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol()) #pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
self.setPixmap(pixmap) #self.setPixmap(pixmap)
class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem): #class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
def __init__(self, data, plot): #def __init__(self, data, plot):
QtGui.QGraphicsPathItem.__init__(self) #QtGui.QGraphicsPathItem.__init__(self)
SpotItem.__init__(self, data, plot) #SpotItem.__init__(self, data, plot)
def updateItem(self): #def updateItem(self):
QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()]) #QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()])
QtGui.QGraphicsPathItem.setPen(self, self.pen()) #QtGui.QGraphicsPathItem.setPen(self, self.pen())
QtGui.QGraphicsPathItem.setBrush(self, self.brush()) #QtGui.QGraphicsPathItem.setBrush(self, self.brush())
size = self.size() #size = self.size()
self.resetTransform() #self.resetTransform()
self.scale(size, size) #self.scale(size, size)