Overhaul of ScatterPlotItem to improve performance. (API should be mostly unchanged)
Much more efficient at rapid updates.
This commit is contained in:
parent
50aa289ab6
commit
c4019b900d
@ -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
|
||||||
@ -49,7 +49,8 @@ def update():
|
|||||||
s = np.clip(dt*3., 0, 1)
|
s = np.clip(dt*3., 0, 1)
|
||||||
fps = fps * (1-s) + (1.0/dt) * s
|
fps = fps * (1-s) + (1.0/dt) * s
|
||||||
p.setTitle('%0.2f fps' % fps)
|
p.setTitle('%0.2f fps' % fps)
|
||||||
app.processEvents() ## force complete redraw for every plot
|
p.repaint()
|
||||||
|
#app.processEvents() ## force complete redraw for every plot
|
||||||
timer = QtCore.QTimer()
|
timer = QtCore.QTimer()
|
||||||
timer.timeout.connect(update)
|
timer.timeout.connect(update)
|
||||||
timer.start(0)
|
timer.start(0)
|
||||||
|
119
functions.py
119
functions.py
@ -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))
|
||||||
|
|
||||||
@ -579,7 +582,10 @@ def solveBilinearTransform(points1, points2):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def makeRGBA(*args, **kwds):
|
||||||
|
"""Equivalent to makeARGB(..., useRGBA=True)"""
|
||||||
|
kwds['useRGBA'] = True
|
||||||
|
return makeARGB(*args, **kwds)
|
||||||
|
|
||||||
def makeARGB(data, lut=None, levels=None, useRGBA=False):
|
def makeARGB(data, lut=None, levels=None, useRGBA=False):
|
||||||
"""
|
"""
|
||||||
@ -605,7 +611,7 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
|
|||||||
Lookup tables can be built using GradientWidget.
|
Lookup tables can be built using GradientWidget.
|
||||||
levels - List [min, max]; optionally rescale data before converting through the
|
levels - List [min, max]; optionally rescale data before converting through the
|
||||||
lookup table. rescaled = (data-min) * len(lut) / (max-min)
|
lookup table. rescaled = (data-min) * len(lut) / (max-min)
|
||||||
useRGBA - If True, the data is returned in RGBA order. The default is
|
useRGBA - If True, the data is returned in RGBA order (useful for building OpenGL textures). The default is
|
||||||
False, which returns in BGRA order for use with QImage.
|
False, which returns in BGRA order for use with QImage.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -779,29 +785,106 @@ 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
|
||||||
|
|
||||||
|
if transpose:
|
||||||
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
|
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
|
|
||||||
imgData = np.ascontiguousarray(imgData)
|
|
||||||
buf = imgData.data
|
|
||||||
|
|
||||||
prof.mark('1')
|
if not imgData.flags['C_CONTIGUOUS']:
|
||||||
qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
|
if copy is False:
|
||||||
prof.mark('2')
|
extra = ' (try setting transpose=False)' if transpose else ''
|
||||||
qimage.data = imgData
|
raise Exception('Array is not contiguous; cannot make QImage without copying.'+extra)
|
||||||
prof.finish()
|
imgData = np.ascontiguousarray(imgData)
|
||||||
return qimage
|
copied = True
|
||||||
|
|
||||||
|
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):
|
def rescaleData(data, scale, offset):
|
||||||
|
@ -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,18 +236,23 @@ 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:
|
||||||
|
return
|
||||||
self.opts['downsample'] = ds
|
self.opts['downsample'] = ds
|
||||||
self.xDisp = self.yDisp = None
|
self.xDisp = self.yDisp = None
|
||||||
self.updateItems()
|
self.updateItems()
|
||||||
@ -436,9 +459,12 @@ class PlotDataItem(GraphicsObject):
|
|||||||
and max)
|
and max)
|
||||||
=============== =============================================================
|
=============== =============================================================
|
||||||
"""
|
"""
|
||||||
|
if frac <= 0.0:
|
||||||
|
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
|
||||||
|
|
||||||
(x, y) = self.getData()
|
(x, y) = self.getData()
|
||||||
if x is None or len(x) == 0:
|
if x is None or len(x) == 0:
|
||||||
return (0, 0)
|
return None
|
||||||
|
|
||||||
if ax == 0:
|
if ax == 0:
|
||||||
d = x
|
d = x
|
||||||
@ -450,14 +476,15 @@ class PlotDataItem(GraphicsObject):
|
|||||||
if orthoRange is not None:
|
if orthoRange is not None:
|
||||||
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
|
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
|
||||||
d = d[mask]
|
d = d[mask]
|
||||||
d2 = d2[mask]
|
#d2 = d2[mask]
|
||||||
|
|
||||||
|
if len(d) > 0:
|
||||||
if frac >= 1.0:
|
if frac >= 1.0:
|
||||||
return (np.min(d), np.max(d))
|
return (np.min(d), np.max(d))
|
||||||
elif frac <= 0.0:
|
|
||||||
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
|
|
||||||
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)))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
@ -33,22 +33,167 @@ for k, c in coords.items():
|
|||||||
Symbols[k].closeSubpath()
|
Symbols[k].closeSubpath()
|
||||||
|
|
||||||
|
|
||||||
def makeSymbolPixmap(size, pen, brush, symbol):
|
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 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, opts):
|
||||||
|
"""
|
||||||
|
Given a list of spot records, return an object representing the coordinates of that symbol within the atlas
|
||||||
|
"""
|
||||||
|
coords = np.empty(len(opts), dtype=object)
|
||||||
|
for i, rec in enumerate(opts):
|
||||||
|
symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush']
|
||||||
|
pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen
|
||||||
|
brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else 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.atlasValid = False
|
||||||
|
#try:
|
||||||
|
#self.addToAtlas(key) ## squeeze this into the atlas if there is room
|
||||||
|
#except:
|
||||||
|
#self.buildAtlas() ## otherwise, we need to rebuild
|
||||||
|
|
||||||
|
coords[i] = self.symbolMap[key]
|
||||||
|
return coords
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -79,13 +224,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), ('item', 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 +244,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 +280,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 +334,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 +368,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 +379,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 +420,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 +442,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 +465,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 +486,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 +506,71 @@ 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']:
|
invalidate = False
|
||||||
spot.updateItem()
|
|
||||||
self.measureSpotSizes(dataSet)
|
self.measureSpotSizes(dataSet)
|
||||||
|
if self.opts['pxMode']:
|
||||||
|
mask = np.equal(dataSet['fragCoords'], None)
|
||||||
|
if np.any(mask):
|
||||||
|
invalidate = True
|
||||||
|
opts = self.getSpotOpts(dataSet[mask])
|
||||||
|
coords = self.fragmentAtlas.getSymbolCoords(opts)
|
||||||
|
dataSet['fragCoords'][mask] = coords
|
||||||
|
|
||||||
|
#for rec in dataSet:
|
||||||
|
#if rec['fragCoords'] is None:
|
||||||
|
#invalidate = True
|
||||||
|
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
|
||||||
|
if invalidate:
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
def getSpotOpts(self, recs):
|
||||||
|
if recs.ndim == 0:
|
||||||
|
rec = recs
|
||||||
|
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))
|
||||||
|
else:
|
||||||
|
recs = recs.copy()
|
||||||
|
recs['symbol'][np.equal(recs['symbol'], None)] = self.opts['symbol']
|
||||||
|
recs['size'][np.equal(recs['size'], -1)] = self.opts['size']
|
||||||
|
recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen'])
|
||||||
|
recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush'])
|
||||||
|
return recs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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,19 +582,10 @@ 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:
|
||||||
@ -437,27 +625,11 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
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:
|
||||||
|
#self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
|
||||||
def generateSpotItems(self):
|
#return self._spotPixmap
|
||||||
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 boundingRect(self):
|
def boundingRect(self):
|
||||||
(xmn, xmx) = self.dataBounds(ax=0)
|
(xmn, xmx) = self.dataBounds(ax=0)
|
||||||
@ -474,15 +646,68 @@ 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'))
|
if self.opts['pxMode']:
|
||||||
p.drawRect(self.boundingRect())
|
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):
|
||||||
|
for rec in self.data:
|
||||||
|
if rec['item'] is None:
|
||||||
|
rec['item'] = SpotItem(rec, self)
|
||||||
return self.data['item']
|
return self.data['item']
|
||||||
|
|
||||||
def pointsAt(self, pos):
|
def pointsAt(self, pos):
|
||||||
@ -506,8 +731,8 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
#else:
|
#else:
|
||||||
#print "No hit:", (x, y), (sx, sy)
|
#print "No hit:", (x, y), (sx, sy)
|
||||||
#print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y)
|
#print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y)
|
||||||
pts.sort(lambda a,b: cmp(b.zValue(), a.zValue()))
|
#pts.sort(lambda a,b: cmp(b.zValue(), a.zValue()))
|
||||||
return pts
|
return pts[::-1]
|
||||||
|
|
||||||
|
|
||||||
def mouseClickEvent(self, ev):
|
def mouseClickEvent(self, ev):
|
||||||
@ -524,7 +749,7 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
ev.ignore()
|
ev.ignore()
|
||||||
|
|
||||||
|
|
||||||
class SpotItem(GraphicsItem):
|
class SpotItem(object):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
@ -532,14 +757,12 @@ class SpotItem(GraphicsItem):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
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.setParentItem(plot)
|
||||||
#self._viewWidget = None
|
#self.setPos(QtCore.QPointF(data['x'], data['y']))
|
||||||
self.setParentItem(plot)
|
#self.updateItem()
|
||||||
self.setPos(QtCore.QPointF(data['x'], data['y']))
|
|
||||||
self.updateItem()
|
|
||||||
|
|
||||||
def data(self):
|
def data(self):
|
||||||
"""Return the user data associated with this spot."""
|
"""Return the user data associated with this spot."""
|
||||||
@ -553,6 +776,12 @@ class SpotItem(GraphicsItem):
|
|||||||
else:
|
else:
|
||||||
return self._data['size']
|
return self._data['size']
|
||||||
|
|
||||||
|
def pos(self):
|
||||||
|
return Point(self._data['x'], self._data['y'])
|
||||||
|
|
||||||
|
def viewPos(self):
|
||||||
|
return self._plot.mapToView(self.pos())
|
||||||
|
|
||||||
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
|
||||||
@ -618,37 +847,41 @@ class SpotItem(GraphicsItem):
|
|||||||
"""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):
|
|
||||||
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):
|
def updateItem(self):
|
||||||
symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol'])
|
self._data['fragCoords'] = None
|
||||||
|
self._plot.updateSpots([self._data])
|
||||||
|
self._plot.invalidate()
|
||||||
|
|
||||||
## If all symbol options are default, use default pixmap
|
#class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem):
|
||||||
if symbolOpts == (None, None, -1, ''):
|
#def __init__(self, data, plot):
|
||||||
pixmap = self._plot.defaultSpotPixmap()
|
#QtGui.QGraphicsPixmapItem.__init__(self)
|
||||||
else:
|
#self.setFlags(self.flags() | self.ItemIgnoresTransformations)
|
||||||
pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
|
#SpotItem.__init__(self, data, plot)
|
||||||
self.setPixmap(pixmap)
|
|
||||||
|
#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)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user