diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 805cf09f..c11c782a 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -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) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 77b7c590..588de0cd 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -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 diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index a2c08382..de7da7ed 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -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: diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 0ddd02a8..b483f0e4 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -277,11 +277,11 @@ class AxisItem(GraphicsWidget): if pen == None, the default will be used (see :func:`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() diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 926c9045..f671c5e5 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -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,28 +689,42 @@ 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()