10-100x speedup for ScatterPlotItem
This commit is contained in:
parent
50aa289ab6
commit
5ce8d09aa0
@ -26,7 +26,7 @@ win.show()
|
||||
p = ui.plot
|
||||
|
||||
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
|
||||
lastTime = time()
|
||||
fps = None
|
||||
|
112
functions.py
112
functions.py
@ -25,6 +25,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY'
|
||||
from .Qt import QtGui, QtCore
|
||||
import numpy as np
|
||||
import decimal, re
|
||||
import ctypes
|
||||
|
||||
try:
|
||||
import scipy.ndimage
|
||||
@ -223,13 +224,15 @@ def mkColor(*args):
|
||||
return QtGui.QColor(*args)
|
||||
|
||||
|
||||
def mkBrush(*args):
|
||||
def mkBrush(*args, **kwds):
|
||||
"""
|
||||
| Convenience function for constructing Brush.
|
||||
| This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>`
|
||||
| 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]
|
||||
if arg is None:
|
||||
return QtGui.QBrush(QtCore.Qt.NoBrush)
|
||||
@ -237,7 +240,7 @@ def mkBrush(*args):
|
||||
return QtGui.QBrush(arg)
|
||||
else:
|
||||
color = arg
|
||||
if len(args) > 1:
|
||||
elif len(args) > 1:
|
||||
color = args
|
||||
return QtGui.QBrush(mkColor(color))
|
||||
|
||||
@ -779,30 +782,107 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
|
||||
return imgData, alpha
|
||||
|
||||
|
||||
def makeQImage(imgData, alpha):
|
||||
"""Turn an ARGB array into QImage"""
|
||||
def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
||||
"""
|
||||
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
|
||||
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:
|
||||
imgFormat = QtGui.QImage.Format_ARGB32
|
||||
else:
|
||||
imgFormat = QtGui.QImage.Format_RGB32
|
||||
|
||||
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
|
||||
try:
|
||||
buf = imgData.data
|
||||
except AttributeError: ## happens when image data is non-contiguous
|
||||
if transpose:
|
||||
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
|
||||
|
||||
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)
|
||||
buf = imgData.data
|
||||
copied = True
|
||||
|
||||
prof.mark('1')
|
||||
qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
|
||||
prof.mark('2')
|
||||
qimage.data = imgData
|
||||
prof.finish()
|
||||
return qimage
|
||||
if copy is True and copied is False:
|
||||
imgData = imgData.copy()
|
||||
|
||||
addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
|
||||
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
|
||||
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):
|
||||
newData = np.empty((data.size,), dtype=np.int)
|
||||
|
@ -130,6 +130,8 @@ class PlotDataItem(GraphicsObject):
|
||||
'symbolBrush': (50, 50, 150),
|
||||
'pxMode': True,
|
||||
|
||||
'pointMode': None,
|
||||
|
||||
'data': None,
|
||||
}
|
||||
self.setData(*args, **kargs)
|
||||
@ -144,22 +146,30 @@ class PlotDataItem(GraphicsObject):
|
||||
return QtCore.QRectF() ## let child items handle this
|
||||
|
||||
def setAlpha(self, alpha, auto):
|
||||
if self.opts['alphaHint'] == alpha and self.opts['alphaMode'] == auto:
|
||||
return
|
||||
self.opts['alphaHint'] = alpha
|
||||
self.opts['alphaMode'] = auto
|
||||
self.setOpacity(alpha)
|
||||
#self.update()
|
||||
|
||||
def setFftMode(self, mode):
|
||||
if self.opts['fftMode'] == mode:
|
||||
return
|
||||
self.opts['fftMode'] = mode
|
||||
self.xDisp = self.yDisp = None
|
||||
self.updateItems()
|
||||
|
||||
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.updateItems()
|
||||
|
||||
def setPointMode(self, mode):
|
||||
if self.opts['pointMode'] == mode:
|
||||
return
|
||||
self.opts['pointMode'] = mode
|
||||
self.update()
|
||||
|
||||
@ -193,6 +203,8 @@ class PlotDataItem(GraphicsObject):
|
||||
|
||||
def setFillBrush(self, *args, **kargs):
|
||||
brush = fn.mkBrush(*args, **kargs)
|
||||
if self.opts['fillBrush'] == brush:
|
||||
return
|
||||
self.opts['fillBrush'] = brush
|
||||
self.updateItems()
|
||||
|
||||
@ -200,16 +212,22 @@ class PlotDataItem(GraphicsObject):
|
||||
return self.setFillBrush(*args, **kargs)
|
||||
|
||||
def setFillLevel(self, level):
|
||||
if self.opts['fillLevel'] == level:
|
||||
return
|
||||
self.opts['fillLevel'] = level
|
||||
self.updateItems()
|
||||
|
||||
def setSymbol(self, symbol):
|
||||
if self.opts['symbol'] == symbol:
|
||||
return
|
||||
self.opts['symbol'] = symbol
|
||||
#self.scatter.setSymbol(symbol)
|
||||
self.updateItems()
|
||||
|
||||
def setSymbolPen(self, *args, **kargs):
|
||||
pen = fn.mkPen(*args, **kargs)
|
||||
if self.opts['symbolPen'] == pen:
|
||||
return
|
||||
self.opts['symbolPen'] = pen
|
||||
#self.scatter.setSymbolPen(pen)
|
||||
self.updateItems()
|
||||
@ -218,21 +236,26 @@ class PlotDataItem(GraphicsObject):
|
||||
|
||||
def setSymbolBrush(self, *args, **kargs):
|
||||
brush = fn.mkBrush(*args, **kargs)
|
||||
if self.opts['symbolBrush'] == brush:
|
||||
return
|
||||
self.opts['symbolBrush'] = brush
|
||||
#self.scatter.setSymbolBrush(brush)
|
||||
self.updateItems()
|
||||
|
||||
|
||||
def setSymbolSize(self, size):
|
||||
if self.opts['symbolSize'] == size:
|
||||
return
|
||||
self.opts['symbolSize'] = size
|
||||
#self.scatter.setSymbolSize(symbolSize)
|
||||
self.updateItems()
|
||||
|
||||
def setDownsampling(self, ds):
|
||||
if self.opts['downsample'] != ds:
|
||||
self.opts['downsample'] = ds
|
||||
self.xDisp = self.yDisp = None
|
||||
self.updateItems()
|
||||
if self.opts['downsample'] == ds:
|
||||
return
|
||||
self.opts['downsample'] = ds
|
||||
self.xDisp = self.yDisp = None
|
||||
self.updateItems()
|
||||
|
||||
def setData(self, *args, **kargs):
|
||||
"""
|
||||
|
@ -32,26 +32,165 @@ for k, c in coords.items():
|
||||
Symbols[k].lineTo(x, y)
|
||||
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
|
||||
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)
|
||||
p = QtGui.QPainter(image)
|
||||
p.setRenderHint(p.Antialiasing)
|
||||
p.translate(image.width()*0.5, image.height()*0.5)
|
||||
p.scale(size, size)
|
||||
p.setPen(pen)
|
||||
p.setBrush(brush)
|
||||
if isinstance(symbol, basestring):
|
||||
symbol = Symbols[symbol]
|
||||
p.drawPath(symbol)
|
||||
drawSymbol(p, symbol, size, pen, brush)
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
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._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._spotPixmap = None
|
||||
self.opts = {'pxMode': True}
|
||||
self.opts = {'pxMode': True, 'useCache': True} ## If useCache is False, symbols are re-drawn on every paint.
|
||||
|
||||
self.setPen(200,200,200, update=False)
|
||||
self.setBrush(100,100,150, update=False)
|
||||
@ -96,6 +238,8 @@ class ScatterPlotItem(GraphicsObject):
|
||||
prof.mark('setData')
|
||||
prof.finish()
|
||||
|
||||
#self.setCacheMode(self.DeviceCoordinateCache)
|
||||
|
||||
def setData(self, *args, **kargs):
|
||||
"""
|
||||
**Ordered Arguments:**
|
||||
@ -130,6 +274,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
*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.addPoints(*args, **kargs)
|
||||
|
||||
@ -183,8 +328,8 @@ class ScatterPlotItem(GraphicsObject):
|
||||
## 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
|
||||
#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
|
||||
@ -217,7 +362,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
newData['y'] = kargs['y']
|
||||
|
||||
if 'pxMode' in kargs:
|
||||
self.setPxMode(kargs['pxMode'], update=False)
|
||||
self.setPxMode(kargs['pxMode'])
|
||||
|
||||
## Set any extra parameters provided in keyword arguments
|
||||
for k in ['pen', 'brush', 'symbol', 'size']:
|
||||
@ -228,12 +373,18 @@ class ScatterPlotItem(GraphicsObject):
|
||||
if 'data' in kargs:
|
||||
self.setPointData(kargs['data'], dataSet=newData)
|
||||
|
||||
#self.updateSpots()
|
||||
self.prepareGeometryChange()
|
||||
self.bounds = [None, None]
|
||||
self.generateSpotItems()
|
||||
self.invalidate()
|
||||
self.updateSpots(newData)
|
||||
self.sigPlotChanged.emit(self)
|
||||
|
||||
def invalidate(self):
|
||||
## clear any cached drawing state
|
||||
self.picture = None
|
||||
self.fragments = None
|
||||
self.update()
|
||||
|
||||
def getData(self):
|
||||
return self.data['x'], self.data['y']
|
||||
|
||||
@ -263,8 +414,8 @@ class ScatterPlotItem(GraphicsObject):
|
||||
dataSet['pen'] = pens
|
||||
else:
|
||||
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
||||
self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -285,8 +436,9 @@ class ScatterPlotItem(GraphicsObject):
|
||||
dataSet['brush'] = brushes
|
||||
else:
|
||||
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
||||
self._spotPixmap = None
|
||||
#self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -307,6 +459,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.opts['symbol'] = symbol
|
||||
self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -327,6 +480,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.opts['size'] = size
|
||||
self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -346,34 +500,52 @@ class ScatterPlotItem(GraphicsObject):
|
||||
else:
|
||||
dataSet['data'] = data
|
||||
|
||||
def setPxMode(self, mode, update=True):
|
||||
def setPxMode(self, mode):
|
||||
if self.opts['pxMode'] == mode:
|
||||
return
|
||||
|
||||
self.opts['pxMode'] = mode
|
||||
self.clearItems()
|
||||
if update:
|
||||
self.generateSpotItems()
|
||||
self.invalidate()
|
||||
|
||||
def updateSpots(self, dataSet=None):
|
||||
if dataSet is None:
|
||||
dataSet = self.data
|
||||
self._maxSpotWidth = 0
|
||||
self._maxSpotPxWidth = 0
|
||||
for spot in dataSet['item']:
|
||||
spot.updateItem()
|
||||
if self.opts['pxMode']:
|
||||
for rec in dataSet:
|
||||
if rec['fragCoords'] is None:
|
||||
|
||||
|
||||
rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
|
||||
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):
|
||||
for spot in dataSet['item']:
|
||||
for rec in dataSet:
|
||||
## keep track of the maximum spot size and pixel size
|
||||
symbol, size, pen, brush = self.getSpotOpts(rec)
|
||||
width = 0
|
||||
pxWidth = 0
|
||||
pen = spot.pen()
|
||||
if self.opts['pxMode']:
|
||||
pxWidth = spot.size() + pen.width()
|
||||
pxWidth = size + pen.width()
|
||||
else:
|
||||
width = spot.size()
|
||||
width = size
|
||||
if pen.isCosmetic():
|
||||
pxWidth += pen.width()
|
||||
else:
|
||||
@ -385,20 +557,11 @@ class ScatterPlotItem(GraphicsObject):
|
||||
|
||||
def clear(self):
|
||||
"""Remove all spots from the scatter plot"""
|
||||
self.clearItems()
|
||||
#self.clearItems()
|
||||
self.data = np.empty(0, dtype=self.data.dtype)
|
||||
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):
|
||||
if frac >= 1.0 and self.bounds[ax] is not None:
|
||||
return self.bounds[ax]
|
||||
@ -436,28 +599,12 @@ class ScatterPlotItem(GraphicsObject):
|
||||
else:
|
||||
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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.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 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):
|
||||
(xmn, xmx) = self.dataBounds(ax=0)
|
||||
@ -474,12 +621,63 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.prepareGeometryChange()
|
||||
GraphicsObject.viewRangeChanged(self)
|
||||
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):
|
||||
## NOTE: self.paint is disabled by this line in __init__:
|
||||
## self.setFlag(self.ItemHasNoContents, True)
|
||||
p.setPen(fn.mkPen('r'))
|
||||
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):
|
||||
@ -524,131 +722,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(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.
|
||||
#"""
|
||||
|
||||
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, 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 data(self):
|
||||
"""Return the user data associated with this spot."""
|
||||
return self._data['data']
|
||||
#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:
|
||||
return self._data['size']
|
||||
#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 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 is None:
|
||||
symbol = self._plot.opts['symbol']
|
||||
try:
|
||||
n = int(symbol)
|
||||
symbol = list(Symbols.keys())[n % len(Symbols)]
|
||||
except:
|
||||
pass
|
||||
return symbol
|
||||
#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 is None:
|
||||
#symbol = self._plot.opts['symbol']
|
||||
#try:
|
||||
#n = int(symbol)
|
||||
#symbol = list(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 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 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 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 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 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 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 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 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)
|
||||
#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 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'])
|
||||
#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)
|
||||
### 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)
|
||||
|
||||
|
||||
class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
|
||||
def __init__(self, data, plot):
|
||||
QtGui.QGraphicsPathItem.__init__(self)
|
||||
SpotItem.__init__(self, data, plot)
|
||||
#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)
|
||||
#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)
|
||||
|
Loading…
Reference in New Issue
Block a user