Merge remote-tracking branch 'gpoulin/scatter_optim' into scatter-optim
Conflicts: pyqtgraph/functions.py pyqtgraph/graphicsItems/AxisItem.py
This commit is contained in:
commit
704f2f2048
@ -27,7 +27,6 @@ w2.setAspectLocked(True)
|
||||
view.nextRow()
|
||||
w3 = view.addPlot()
|
||||
w4 = view.addPlot()
|
||||
print("Generating data, this takes a few seconds...")
|
||||
|
||||
## There are a few different ways we can draw scatter plots; each is optimized for different types of data:
|
||||
|
||||
@ -58,8 +57,9 @@ s1.sigClicked.connect(clicked)
|
||||
|
||||
|
||||
## 2) Spots are transform-invariant, but not identical (top-right plot).
|
||||
## In this case, drawing is as fast as 1), but there is more startup overhead
|
||||
## and memory usage since each spot generates its own pre-rendered image.
|
||||
## In this case, drawing is almsot as fast as 1), but there is more startup
|
||||
## overhead and memory usage since each spot generates its own pre-rendered
|
||||
## image.
|
||||
|
||||
s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True)
|
||||
pos = np.random.normal(size=(2,n), scale=1e-5)
|
||||
|
@ -48,8 +48,8 @@ else:
|
||||
CONFIG_OPTIONS = {
|
||||
'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl.
|
||||
'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox
|
||||
'foreground': (150, 150, 150), ## default foreground color for axes, labels, etc.
|
||||
'background': (0, 0, 0), ## default background for GraphicsWidget
|
||||
'foreground': 'd', ## default foreground color for axes, labels, etc.
|
||||
'background': 'k', ## default background for GraphicsWidget
|
||||
'antialias': False,
|
||||
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
||||
'useWeave': True, ## Use weave to speed up some operations, if it is available
|
||||
|
@ -7,15 +7,19 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
|
||||
from __future__ import division
|
||||
from .python2_3 import asUnicode
|
||||
from .Qt import QtGui, QtCore, USE_PYSIDE
|
||||
Colors = {
|
||||
'b': (0,0,255,255),
|
||||
'g': (0,255,0,255),
|
||||
'r': (255,0,0,255),
|
||||
'c': (0,255,255,255),
|
||||
'm': (255,0,255,255),
|
||||
'y': (255,255,0,255),
|
||||
'k': (0,0,0,255),
|
||||
'w': (255,255,255,255),
|
||||
'b': QtGui.QColor(0,0,255,255),
|
||||
'g': QtGui.QColor(0,255,0,255),
|
||||
'r': QtGui.QColor(255,0,0,255),
|
||||
'c': QtGui.QColor(0,255,255,255),
|
||||
'm': QtGui.QColor(255,0,255,255),
|
||||
'y': QtGui.QColor(255,255,0,255),
|
||||
'k': QtGui.QColor(0,0,0,255),
|
||||
'w': QtGui.QColor(255,255,255,255),
|
||||
'd': QtGui.QColor(150,150,150,255),
|
||||
'l': QtGui.QColor(200,200,200,255),
|
||||
's': QtGui.QColor(100,100,150,255),
|
||||
}
|
||||
|
||||
SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY')
|
||||
@ -168,17 +172,15 @@ def mkColor(*args):
|
||||
"""
|
||||
err = 'Not sure how to make a color from "%s"' % str(args)
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], QtGui.QColor):
|
||||
return QtGui.QColor(args[0])
|
||||
elif isinstance(args[0], float):
|
||||
r = g = b = int(args[0] * 255)
|
||||
a = 255
|
||||
elif isinstance(args[0], basestring):
|
||||
if isinstance(args[0], basestring):
|
||||
c = args[0]
|
||||
if c[0] == '#':
|
||||
c = c[1:]
|
||||
if len(c) == 1:
|
||||
(r, g, b, a) = Colors[c]
|
||||
try:
|
||||
return Colors[c]
|
||||
except KeyError:
|
||||
raise Exception(err)
|
||||
if len(c) == 3:
|
||||
r = int(c[0]*2, 16)
|
||||
g = int(c[1]*2, 16)
|
||||
@ -199,6 +201,11 @@ def mkColor(*args):
|
||||
g = int(c[2:4], 16)
|
||||
b = int(c[4:6], 16)
|
||||
a = int(c[6:8], 16)
|
||||
elif isinstance(args[0], QtGui.QColor):
|
||||
return QtGui.QColor(args[0])
|
||||
elif isinstance(args[0], float):
|
||||
r = g = b = int(args[0] * 255)
|
||||
a = 255
|
||||
elif hasattr(args[0], '__len__'):
|
||||
if len(args[0]) == 3:
|
||||
(r, g, b) = args[0]
|
||||
@ -282,7 +289,7 @@ def mkPen(*args, **kargs):
|
||||
color = args
|
||||
|
||||
if color is None:
|
||||
color = mkColor(200, 200, 200)
|
||||
color = mkColor('l')
|
||||
if hsv is not None:
|
||||
color = hsvColor(*hsv)
|
||||
else:
|
||||
|
@ -277,11 +277,11 @@ class AxisItem(GraphicsWidget):
|
||||
if pen == None, the default will be used (see :func:`setConfigOption
|
||||
<pyqtgraph.setConfigOption>`)
|
||||
"""
|
||||
self._pen = pen
|
||||
self.picture = None
|
||||
if pen is None:
|
||||
pen = getConfigOption('foreground')
|
||||
self.labelStyle['color'] = '#' + fn.colorStr(fn.mkPen(pen).color())[:6]
|
||||
self._pen = fn.mkPen(pen)
|
||||
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
|
||||
self.setLabel()
|
||||
self.update()
|
||||
|
||||
|
@ -3,6 +3,11 @@ from ..Point import Point
|
||||
from .. import functions as fn
|
||||
from .GraphicsItem import GraphicsItem
|
||||
from .GraphicsObject import GraphicsObject
|
||||
from itertools import starmap, repeat
|
||||
try:
|
||||
from itertools import imap
|
||||
except ImportError:
|
||||
imap = map
|
||||
import numpy as np
|
||||
import weakref
|
||||
from .. import getConfigOption
|
||||
@ -86,9 +91,6 @@ class SymbolAtlas(object):
|
||||
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
|
||||
@ -96,33 +98,39 @@ class SymbolAtlas(object):
|
||||
# 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.symbolPen = weakref.WeakValueDictionary()
|
||||
self.symbolBrush = weakref.WeakValueDictionary()
|
||||
self.symbolRectSrc = weakref.WeakValueDictionary()
|
||||
|
||||
self.atlasData = None # numpy array of atlas image
|
||||
self.atlas = None # atlas as QPixmap
|
||||
self.atlasValid = False
|
||||
self.max_width=0
|
||||
|
||||
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)
|
||||
rectSrc = np.empty(len(opts), dtype=object)
|
||||
keyi = None
|
||||
rectSrci = None
|
||||
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.widthF(), 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
|
||||
key = (rec[3], rec[2], id(rec[4]), id(rec[5]))
|
||||
if key == keyi:
|
||||
rectSrc[i] = rectSrci
|
||||
else:
|
||||
try:
|
||||
rectSrc[i] = self.symbolRectSrc[key]
|
||||
except KeyError:
|
||||
newRectSrc = QtCore.QRectF()
|
||||
self.symbolPen[key] = rec['pen']
|
||||
self.symbolBrush[key] = rec['brush']
|
||||
self.symbolRectSrc[key] = newRectSrc
|
||||
self.atlasValid = False
|
||||
rectSrc[i] = self.symbolRectSrc[key]
|
||||
keyi = key
|
||||
rectSrci = self.symbolRectSrc[key]
|
||||
return rectSrc
|
||||
|
||||
def buildAtlas(self):
|
||||
# get rendered array for all symbols, keep track of avg/max width
|
||||
@ -130,15 +138,15 @@ class SymbolAtlas(object):
|
||||
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])
|
||||
for key, rectSrc in self.symbolRectSrc.items():
|
||||
if rectSrc.width() == 0:
|
||||
pen = self.symbolPen[key]
|
||||
brush = self.symbolBrush[key]
|
||||
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]
|
||||
(y,x,h,w) = rectSrc.getRect()
|
||||
arr = self.atlasData[x:x+w, y:y+w]
|
||||
rendered[key] = arr
|
||||
w = arr.shape[0]
|
||||
@ -169,17 +177,18 @@ class SymbolAtlas(object):
|
||||
x = 0
|
||||
rowheight = h
|
||||
self.atlasRows.append([y, rowheight, 0])
|
||||
self.symbolMap[key][:] = x, y, w, h
|
||||
self.symbolRectSrc[key].setRect(y, x, h, w)
|
||||
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]
|
||||
y, x, h, w = self.symbolRectSrc[key].getRect()
|
||||
self.atlasData[x:x+w, y:y+h] = rendered[key]
|
||||
self.atlas = None
|
||||
self.atlasValid = True
|
||||
self.max_width=maxWidth
|
||||
|
||||
def getAtlas(self):
|
||||
if not self.atlasValid:
|
||||
@ -224,9 +233,10 @@ class ScatterPlotItem(GraphicsObject):
|
||||
|
||||
self.picture = None # QPicture used for rendering when pxmode==False
|
||||
self.fragments = None # fragment specification for pxmode; updated every time the view changes.
|
||||
self.target = None
|
||||
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.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('rectSrc', object), ('rectTarg', object), ('width', float)])
|
||||
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
|
||||
@ -237,8 +247,8 @@ class ScatterPlotItem(GraphicsObject):
|
||||
'name': None,
|
||||
}
|
||||
|
||||
self.setPen(200,200,200, update=False)
|
||||
self.setBrush(100,100,150, update=False)
|
||||
self.setPen('l', update=False)
|
||||
self.setBrush('s', update=False)
|
||||
self.setSymbol('o', update=False)
|
||||
self.setSize(7, update=False)
|
||||
profiler()
|
||||
@ -397,6 +407,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
## clear any cached drawing state
|
||||
self.picture = None
|
||||
self.fragments = None
|
||||
self.target = None
|
||||
self.update()
|
||||
|
||||
def getData(self):
|
||||
@ -434,7 +445,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
else:
|
||||
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
dataSet['rectSrc'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -459,7 +470,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
||||
#self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
dataSet['rectSrc'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -482,7 +493,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.opts['symbol'] = symbol
|
||||
self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
dataSet['rectSrc'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -505,7 +516,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
self.opts['size'] = size
|
||||
self._spotPixmap = None
|
||||
|
||||
dataSet['fragCoords'] = None
|
||||
dataSet['rectSrc'] = None
|
||||
if update:
|
||||
self.updateSpots(dataSet)
|
||||
|
||||
@ -537,22 +548,30 @@ class ScatterPlotItem(GraphicsObject):
|
||||
def updateSpots(self, dataSet=None):
|
||||
if dataSet is None:
|
||||
dataSet = self.data
|
||||
self._maxSpotWidth = 0
|
||||
self._maxSpotPxWidth = 0
|
||||
|
||||
invalidate = False
|
||||
self.measureSpotSizes(dataSet)
|
||||
if self.opts['pxMode']:
|
||||
mask = np.equal(dataSet['fragCoords'], None)
|
||||
mask = np.equal(dataSet['rectSrc'], None)
|
||||
if np.any(mask):
|
||||
invalidate = True
|
||||
opts = self.getSpotOpts(dataSet[mask])
|
||||
coords = self.fragmentAtlas.getSymbolCoords(opts)
|
||||
dataSet['fragCoords'][mask] = coords
|
||||
rectSrc = self.fragmentAtlas.getSymbolCoords(opts)
|
||||
dataSet['rectSrc'][mask] = rectSrc
|
||||
|
||||
|
||||
#for rec in dataSet:
|
||||
#if rec['fragCoords'] is None:
|
||||
#invalidate = True
|
||||
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
|
||||
self.fragmentAtlas.getAtlas()
|
||||
dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['rectSrc'])))/2
|
||||
dataSet['rectTarg'] = list(imap(QtCore.QRectF, repeat(0), repeat(0), dataSet['width']*2, dataSet['width']*2))
|
||||
self._maxSpotPxWidth=self.fragmentAtlas.max_width
|
||||
else:
|
||||
self._maxSpotWidth = 0
|
||||
self._maxSpotPxWidth = 0
|
||||
self.measureSpotSizes(dataSet)
|
||||
|
||||
if invalidate:
|
||||
self.invalidate()
|
||||
|
||||
@ -670,29 +689,43 @@ class ScatterPlotItem(GraphicsObject):
|
||||
GraphicsObject.viewTransformChanged(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 = []
|
||||
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
|
||||
## Still won't be able to render correctly, though.
|
||||
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))
|
||||
self.target = None
|
||||
|
||||
def setExportMode(self, *args, **kwds):
|
||||
GraphicsObject.setExportMode(self, *args, **kwds)
|
||||
self.invalidate()
|
||||
|
||||
|
||||
def getTransformedPoint(self):
|
||||
tr = self.deviceTransform()
|
||||
if tr is None:
|
||||
return None, None
|
||||
## Remove out of view points
|
||||
w = np.empty((2,len(self.data['width'])))
|
||||
w[0] = self.data['width']
|
||||
w[1] = self.data['width']
|
||||
q, intv = tr.inverted()
|
||||
if intv:
|
||||
w = fn.transformCoordinates(q, w)
|
||||
w=np.abs(w)
|
||||
range = self.getViewBox().viewRange()
|
||||
mask = np.logical_and(
|
||||
np.logical_and(self.data['x'] + w[0,:] > range[0][0],
|
||||
self.data['x'] - w[0,:] < range[0][1]),
|
||||
np.logical_and(self.data['y'] + w[0,:] > range[1][0],
|
||||
self.data['y'] - w[0,:] < range[1][1])) ## remove out of view points
|
||||
data = self.data[mask]
|
||||
else:
|
||||
data = self.data
|
||||
|
||||
pts = np.empty((2,len(data['x'])))
|
||||
pts[0] = data['x']
|
||||
pts[1] = data['y']
|
||||
pts = fn.transformCoordinates(tr, pts)
|
||||
pts -= data['width']
|
||||
pts = np.clip(pts, -2**30, 2**30)
|
||||
return data, pts
|
||||
|
||||
@debug.warnOnException ## raising an exception here causes crash
|
||||
def paint(self, p, *args):
|
||||
|
||||
@ -708,28 +741,28 @@ class ScatterPlotItem(GraphicsObject):
|
||||
|
||||
if self.opts['pxMode'] is True:
|
||||
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 not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False:
|
||||
p.drawPixmapFragments(self.fragments, atlas)
|
||||
data, pts = self.getTransformedPoint()
|
||||
if data is None:
|
||||
return
|
||||
|
||||
if self.opts['useCache'] and self._exportOpts is False:
|
||||
|
||||
if self.target == None:
|
||||
list(imap(QtCore.QRectF.moveTo, data['rectTarg'], pts[0,:], pts[1,:]))
|
||||
self.target=data['rectTarg']
|
||||
if USE_PYSIDE:
|
||||
list(imap(p.drawPixmap, self.target, repeat(atlas), data['rectSrc']))
|
||||
else:
|
||||
p.drawPixmapFragments(self.target.tolist(), data['rectSrc'].tolist(), atlas)
|
||||
else:
|
||||
p.setRenderHint(p.Antialiasing, aa)
|
||||
|
||||
for i in range(len(self.data)):
|
||||
rec = self.data[i]
|
||||
frag = self.fragments[i]
|
||||
rec = data[i]
|
||||
p.resetTransform()
|
||||
p.translate(frag.x, frag.y)
|
||||
p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width'])
|
||||
drawSymbol(p, *self.getSpotOpts(rec, scale))
|
||||
else:
|
||||
if self.picture is None:
|
||||
@ -891,7 +924,7 @@ class SpotItem(object):
|
||||
self._data['data'] = data
|
||||
|
||||
def updateItem(self):
|
||||
self._data['fragCoords'] = None
|
||||
self._data['rectSrc'] = None
|
||||
self._plot.updateSpots(self._data.reshape(1))
|
||||
self._plot.invalidate()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user