From 63bf2b32701b2887533f1051f7e81d95b3b77c51 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Fri, 20 Sep 2013 15:46:10 +0800 Subject: [PATCH 01/37] optimize ScatterPlotItem with pxMode=True --- pyqtgraph/__init__.py | 4 +- pyqtgraph/functions.py | 40 ++++++++------ pyqtgraph/graphicsItems/AxisItem.py | 7 ++- pyqtgraph/graphicsItems/ScatterPlotItem.py | 62 ++++++++++++++-------- 4 files changed, 67 insertions(+), 46 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 12a4f90f..c6b411a1 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 14e4e076..33069d23 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') @@ -23,7 +27,6 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' -from .Qt import QtGui, QtCore, USE_PYSIDE import pyqtgraph as pg import numpy as np import decimal, re @@ -169,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) @@ -200,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] @@ -283,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 d82f5d41..3f2f6fcd 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -266,8 +266,7 @@ class AxisItem(GraphicsWidget): self.picture = None def pen(self): - if self._pen is None: - return fn.mkPen(pg.getConfigOption('foreground')) + #return self._pen return pg.mkPen(self._pen) def setPen(self, pen): @@ -276,11 +275,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 = pg.getConfigOption('foreground') - self.labelStyle['color'] = '#' + pg.colorStr(pg.mkPen(pen).color())[:6] + self._pen = pg.mkPen(pen) + self.labelStyle['color'] = '#' + pg.colorStr(self._pen.color())[:6] self.setLabel() self.update() diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 97f5aa8f..3cab5ffe 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -98,31 +98,41 @@ class SymbolAtlas(object): # 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.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) + keyi = None + coordi = 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] + key = (rec[3], rec[2], id(rec[4]), id(rec[5])) + if key == keyi: + coords[i]=coordi + else: + try: + coords[i] = self.symbolMap[key] + except KeyError: + newCoords = SymbolAtlas.SymbolCoords() + self.symbolMap[key] = newCoords + self.symbolPen[key] = rec['pen'] + self.symbolBrush[key] = rec['brush'] + 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] = newCoords + keyi = key + coordi = newCoords return coords def buildAtlas(self): @@ -133,8 +143,8 @@ class SymbolAtlas(object): 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]) + 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) @@ -181,6 +191,7 @@ class SymbolAtlas(object): 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: @@ -237,8 +248,8 @@ class ScatterPlotItem(GraphicsObject): 'antialias': pg.getConfigOption('antialias'), } - 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) prof.mark('1') @@ -533,10 +544,8 @@ 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) if np.any(mask): @@ -549,6 +558,13 @@ class ScatterPlotItem(GraphicsObject): #if rec['fragCoords'] is None: #invalidate = True #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) + self.fragmentAtlas.getAtlas() + self._maxSpotPxWidth=self.fragmentAtlas.max_width + else: + self._maxSpotWidth = 0 + self._maxSpotPxWidth = 0 + self.measureSpotSizes(dataSet) + if invalidate: self.invalidate() @@ -666,7 +682,7 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None - + def generateFragments(self): tr = self.deviceTransform() if tr is None: @@ -711,7 +727,7 @@ class ScatterPlotItem(GraphicsObject): #self.lastAtlas = arr if self.fragments is None: - self.updateSpots() + #self.updateSpots() self.generateFragments() p.resetTransform() From 3a9258e35edd8032e4d2dd41cd128cde2658ffcf Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Fri, 20 Sep 2013 16:46:33 +0800 Subject: [PATCH 02/37] Correct comment in examples/ScatterPlot.py --- examples/ScatterPlot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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) From 26b84693a87163264c4d4fee36842ce1eeb68caf Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Sun, 22 Sep 2013 23:10:18 +0800 Subject: [PATCH 03/37] Modify for loop into map in ScatterPlotItem.py --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 3cab5ffe..64779b1f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -3,6 +3,7 @@ from pyqtgraph.Point import Point import pyqtgraph.functions as fn from .GraphicsItem import GraphicsItem from .GraphicsObject import GraphicsObject +from itertools import starmap import numpy as np import scipy.stats import weakref @@ -149,7 +150,7 @@ class SymbolAtlas(object): 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) = self.symbolMap[key] arr = self.atlasData[x:x+w, y:y+w] rendered[key] = arr w = arr.shape[0] @@ -180,14 +181,14 @@ class SymbolAtlas(object): x = 0 rowheight = h self.atlasRows.append([y, rowheight, 0]) - self.symbolMap[key][:] = x, y, w, h + self.symbolMap[key][:] = 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.symbolMap[key] self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlas = None self.atlasValid = True @@ -694,12 +695,15 @@ class ScatterPlotItem(GraphicsObject): 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)) + #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)) + rect = starmap(QtCore.QRectF, self.data['fragCoords']) + pos = map(QtCore.QPointF, pts[0,:], pts[1,:]) + self.fragments = map(QtGui.QPainter.PixmapFragment.create, pos, rect) def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) From f5ee45ac28da20fa52d039a789ca3e402986d5ce Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Mon, 23 Sep 2013 00:45:55 +0800 Subject: [PATCH 04/37] Improve ScatterPlotItem Slightly faster and more memory efficient, correct python 3 bug --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 64779b1f..76390ba9 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -4,6 +4,10 @@ import pyqtgraph.functions as fn from .GraphicsItem import GraphicsItem from .GraphicsObject import GraphicsObject from itertools import starmap +try: + from itertools import imap +except ImportError: + imap = map import numpy as np import scipy.stats import weakref @@ -702,8 +706,8 @@ class ScatterPlotItem(GraphicsObject): # rect = QtCore.QRectF(y, x, h, w) # self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) rect = starmap(QtCore.QRectF, self.data['fragCoords']) - pos = map(QtCore.QPointF, pts[0,:], pts[1,:]) - self.fragments = map(QtGui.QPainter.PixmapFragment.create, pos, rect) + pos = imap(QtCore.QPointF, pts[0,:], pts[1,:]) + self.fragments = list(imap(QtGui.QPainter.PixmapFragment.create, pos, rect)) def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) From c3576b1c09f2204d64861e964a71e05a5cdaffb3 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Mon, 23 Sep 2013 16:45:43 +0800 Subject: [PATCH 05/37] Some few more optimization to ScatterPlotItem --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 91 +++++++++++++--------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 76390ba9..8dcdcdf0 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -92,9 +92,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 @@ -102,9 +99,10 @@ 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.symbolRectTarg = weakref.WeakValueDictionary() self.atlasData = None # numpy array of atlas image self.atlas = None # atlas as QPixmap @@ -115,30 +113,34 @@ class SymbolAtlas(object): """ 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) + rectTarg = np.empty(len(opts), dtype=object) keyi = None - coordi = None + rectSrci = None + rectTargi = None for i, rec in enumerate(opts): key = (rec[3], rec[2], id(rec[4]), id(rec[5])) if key == keyi: - coords[i]=coordi + rectSrc[i] = rectSrci + rectTarg[i] = rectTargi else: try: - coords[i] = self.symbolMap[key] + rectSrc[i] = self.symbolRectSrc[key] + rectTarg[i] = self.symbolRectTarg[key] except KeyError: - newCoords = SymbolAtlas.SymbolCoords() - self.symbolMap[key] = newCoords + newRectSrc = QtCore.QRectF() + newRectTarg = QtCore.QRectF() self.symbolPen[key] = rec['pen'] self.symbolBrush[key] = rec['brush'] + self.symbolRectSrc[key] = newRectSrc + self.symbolRectTarg[key] = newRectTarg 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] = newCoords + rectSrc[i] = self.symbolRectSrc[key] + rectTarg[i] = self.symbolRectTarg[key] keyi = key - coordi = newCoords - return coords + rectSrci = self.symbolRectSrc[key] + rectTargi = self.symbolRectTarg[key] + return rectSrc, rectTarg def buildAtlas(self): # get rendered array for all symbols, keep track of avg/max width @@ -146,15 +148,15 @@ class SymbolAtlas(object): avgWidth = 0.0 maxWidth = 0 images = [] - for key, coords in self.symbolMap.items(): - if len(coords) == 0: + 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: - (y,x,h,w) = 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] @@ -185,14 +187,15 @@ class SymbolAtlas(object): x = 0 rowheight = h self.atlasRows.append([y, rowheight, 0]) - self.symbolMap[key][:] = y, x, h, w + 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: - y, x, h, w = self.symbolMap[key] + y, x, h, w = self.symbolRectSrc[key].getRect() + self.symbolRectTarg[key].setRect(-h/2, -w/2, h, w) self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlas = None self.atlasValid = True @@ -241,9 +244,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.tar = 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)]) 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 @@ -412,6 +416,7 @@ class ScatterPlotItem(GraphicsObject): ## clear any cached drawing state self.picture = None self.fragments = None + self.tar = None self.update() def getData(self): @@ -446,7 +451,7 @@ class ScatterPlotItem(GraphicsObject): else: self.opts['pen'] = fn.mkPen(*args, **kargs) - dataSet['fragCoords'] = None + dataSet['rectSrc'] = None if update: self.updateSpots(dataSet) @@ -471,7 +476,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) @@ -494,7 +499,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['symbol'] = symbol self._spotPixmap = None - dataSet['fragCoords'] = None + dataSet['rectSrc'] = None if update: self.updateSpots(dataSet) @@ -517,7 +522,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['size'] = size self._spotPixmap = None - dataSet['fragCoords'] = None + dataSet['rectSrc'] = None if update: self.updateSpots(dataSet) @@ -552,12 +557,13 @@ class ScatterPlotItem(GraphicsObject): invalidate = False 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, rectTarg = self.fragmentAtlas.getSymbolCoords(opts) + dataSet['rectSrc'][mask] = rectSrc + dataSet['rectTarg'][mask] = rectTarg #for rec in dataSet: #if rec['fragCoords'] is None: @@ -687,6 +693,7 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None + self.tar = None def generateFragments(self): tr = self.deviceTransform() @@ -705,9 +712,8 @@ class ScatterPlotItem(GraphicsObject): # x,y,w,h = rec['fragCoords'] # rect = QtCore.QRectF(y, x, h, w) # self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) - rect = starmap(QtCore.QRectF, self.data['fragCoords']) pos = imap(QtCore.QPointF, pts[0,:], pts[1,:]) - self.fragments = list(imap(QtGui.QPainter.PixmapFragment.create, pos, rect)) + self.fragments = list(imap(QtGui.QPainter.PixmapFragment.create, pos, self.data['rectSrc'])) def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) @@ -734,15 +740,28 @@ class ScatterPlotItem(GraphicsObject): #print "Atlas changed:", arr #self.lastAtlas = arr - if self.fragments is None: + #if self.fragments is None: #self.updateSpots() - self.generateFragments() + #self.generateFragments() p.resetTransform() if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False: - p.drawPixmapFragments(self.fragments, atlas) + 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) + pts = np.clip(pts, -2**30, 2**30) + if self.tar == None: + self.tar = list(imap(QtCore.QRectF.translated, self.data['rectTarg'], pts[0,:], pts[1,:])) + p.drawPixmapFragments(self.tar, self.data['rectSrc'].tolist(), atlas) + #p.drawPixmapFragments(self.fragments, atlas) else: + if self.fragments is None: + self.generateFragments() p.setRenderHint(p.Antialiasing, aa) for i in range(len(self.data)): @@ -911,7 +930,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() From bd43a7508afb60b29a0047504279d68a28817f20 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Mon, 23 Sep 2013 17:47:33 +0800 Subject: [PATCH 06/37] Rename self.tar to self.target --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 8dcdcdf0..98643582 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -244,7 +244,7 @@ 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.tar = None + 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), ('item', object), ('rectSrc', object), ('rectTarg', object)]) @@ -416,7 +416,7 @@ class ScatterPlotItem(GraphicsObject): ## clear any cached drawing state self.picture = None self.fragments = None - self.tar = None + self.target = None self.update() def getData(self): @@ -693,7 +693,7 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] self.fragments = None - self.tar = None + self.target = None def generateFragments(self): tr = self.deviceTransform() @@ -755,9 +755,9 @@ class ScatterPlotItem(GraphicsObject): pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) pts = np.clip(pts, -2**30, 2**30) - if self.tar == None: - self.tar = list(imap(QtCore.QRectF.translated, self.data['rectTarg'], pts[0,:], pts[1,:])) - p.drawPixmapFragments(self.tar, self.data['rectSrc'].tolist(), atlas) + if self.target == None: + self.target = list(imap(QtCore.QRectF.translated, self.data['rectTarg'], pts[0,:], pts[1,:])) + p.drawPixmapFragments(self.target, self.data['rectSrc'].tolist(), atlas) #p.drawPixmapFragments(self.fragments, atlas) else: if self.fragments is None: From 73a079a64922e5744be1f3078ab0c304eb96c22b Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 24 Sep 2013 16:12:29 +0800 Subject: [PATCH 07/37] Improve ScatterPlotItem.py Add optimization for PySide, Plot only visible symbole, cache rectTarg --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 67 ++++++++++++++-------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 98643582..71e94e57 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -3,7 +3,7 @@ from pyqtgraph.Point import Point import pyqtgraph.functions as fn from .GraphicsItem import GraphicsItem from .GraphicsObject import GraphicsObject -from itertools import starmap +from itertools import starmap, repeat try: from itertools import imap except ImportError: @@ -102,7 +102,6 @@ class SymbolAtlas(object): self.symbolPen = weakref.WeakValueDictionary() self.symbolBrush = weakref.WeakValueDictionary() self.symbolRectSrc = weakref.WeakValueDictionary() - self.symbolRectTarg = weakref.WeakValueDictionary() self.atlasData = None # numpy array of atlas image self.atlas = None # atlas as QPixmap @@ -114,33 +113,25 @@ class SymbolAtlas(object): Given a list of spot records, return an object representing the coordinates of that symbol within the atlas """ rectSrc = np.empty(len(opts), dtype=object) - rectTarg = np.empty(len(opts), dtype=object) keyi = None rectSrci = None - rectTargi = None for i, rec in enumerate(opts): key = (rec[3], rec[2], id(rec[4]), id(rec[5])) if key == keyi: rectSrc[i] = rectSrci - rectTarg[i] = rectTargi else: try: rectSrc[i] = self.symbolRectSrc[key] - rectTarg[i] = self.symbolRectTarg[key] except KeyError: newRectSrc = QtCore.QRectF() - newRectTarg = QtCore.QRectF() self.symbolPen[key] = rec['pen'] self.symbolBrush[key] = rec['brush'] self.symbolRectSrc[key] = newRectSrc - self.symbolRectTarg[key] = newRectTarg self.atlasValid = False rectSrc[i] = self.symbolRectSrc[key] - rectTarg[i] = self.symbolRectTarg[key] keyi = key rectSrci = self.symbolRectSrc[key] - rectTargi = self.symbolRectTarg[key] - return rectSrc, rectTarg + return rectSrc def buildAtlas(self): # get rendered array for all symbols, keep track of avg/max width @@ -195,7 +186,6 @@ class SymbolAtlas(object): self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte) for key in symbols: y, x, h, w = self.symbolRectSrc[key].getRect() - self.symbolRectTarg[key].setRect(-h/2, -w/2, h, w) self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlas = None self.atlasValid = True @@ -247,7 +237,7 @@ class ScatterPlotItem(GraphicsObject): 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), ('item', object), ('rectSrc', object), ('rectTarg', 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 @@ -561,15 +551,17 @@ class ScatterPlotItem(GraphicsObject): if np.any(mask): invalidate = True opts = self.getSpotOpts(dataSet[mask]) - rectSrc, rectTarg = self.fragmentAtlas.getSymbolCoords(opts) + rectSrc = self.fragmentAtlas.getSymbolCoords(opts) dataSet['rectSrc'][mask] = rectSrc - dataSet['rectTarg'][mask] = rectTarg + #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 @@ -699,9 +691,15 @@ class ScatterPlotItem(GraphicsObject): 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'] + mask = np.logical_and( + np.logical_and(self.data['x'] - self.data['width'] > range[0][0], + self.data['x'] + self.data['width'] < range[0][1]), + np.logical_and(self.data['y'] - self.data['width'] > range[1][0], + self.data['y'] + self.data['width'] < range[1][1])) ## remove out of view points + data = self.data[mask] + pts = np.empty((2,len(data['x']))) + pts[0] = data['x'] + pts[1] = data['y'] pts = fn.transformCoordinates(tr, pts) self.fragments = [] pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. @@ -746,18 +744,39 @@ class ScatterPlotItem(GraphicsObject): p.resetTransform() - if not USE_PYSIDE and self.opts['useCache'] and self._exportOpts is False: + if self.opts['useCache'] and self._exportOpts is False: 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'] + 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) if self.target == None: - self.target = list(imap(QtCore.QRectF.translated, self.data['rectTarg'], pts[0,:], pts[1,:])) - p.drawPixmapFragments(self.target, self.data['rectSrc'].tolist(), atlas) + 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) #p.drawPixmapFragments(self.fragments, atlas) else: if self.fragments is None: From 36979b67ea3b99bbb5b7d08e6a9eb4fd0a3d0246 Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 24 Sep 2013 17:06:51 +0800 Subject: [PATCH 08/37] Clean ScatterPlotItem --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 103 ++++++++------------- 1 file changed, 37 insertions(+), 66 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 71e94e57..2d76b104 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -687,35 +687,40 @@ class ScatterPlotItem(GraphicsObject): self.fragments = None self.target = None - def generateFragments(self): + def setExportMode(self, *args, **kwds): + GraphicsObject.setExportMode(self, *args, **kwds) + self.invalidate() + + + def getTransformedPoint(self): tr = self.deviceTransform() if tr is None: - return - mask = np.logical_and( - np.logical_and(self.data['x'] - self.data['width'] > range[0][0], - self.data['x'] + self.data['width'] < range[0][1]), - np.logical_and(self.data['y'] - self.data['width'] > range[1][0], - self.data['y'] + self.data['width'] < range[1][1])) ## remove out of view points - data = self.data[mask] + 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) - 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)) - pos = imap(QtCore.QPointF, pts[0,:], pts[1,:]) - self.fragments = list(imap(QtGui.QPainter.PixmapFragment.create, pos, self.data['rectSrc'])) - - def setExportMode(self, *args, **kwds): - GraphicsObject.setExportMode(self, *args, **kwds) - self.invalidate() + pts -= data['width'] + pts = np.clip(pts, -2**30, 2**30) + return data, pts @pg.debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): @@ -732,44 +737,14 @@ 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() + data, pts = self.getTransformedPoint() + if data is None: + return + if self.opts['useCache'] and self._exportOpts is False: - tr = self.deviceTransform() - if tr is None: - return - 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) + if self.target == None: list(imap(QtCore.QRectF.moveTo, data['rectTarg'], pts[0,:], pts[1,:])) self.target=data['rectTarg'] @@ -777,17 +752,13 @@ class ScatterPlotItem(GraphicsObject): list(imap(p.drawPixmap, self.target, repeat(atlas), data['rectSrc'])) else: p.drawPixmapFragments(self.target.tolist(), data['rectSrc'].tolist(), atlas) - #p.drawPixmapFragments(self.fragments, atlas) else: - if self.fragments is None: - self.generateFragments() 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], pts[1,i]) drawSymbol(p, *self.getSpotOpts(rec, scale)) else: if self.picture is None: @@ -798,7 +769,7 @@ class ScatterPlotItem(GraphicsObject): rec = rec.copy() rec['size'] *= scale p2.resetTransform() - p2.translate(rec['x'], rec['y']) + p2.translate(rec['x']+rec['width'], rec['y']+rec['width']) drawSymbol(p2, *self.getSpotOpts(rec, scale)) p2.end() From d22403f84f9c53547d74a3bfdd74973209a2e91e Mon Sep 17 00:00:00 2001 From: Guillaume Poulin Date: Tue, 24 Sep 2013 17:13:13 +0800 Subject: [PATCH 09/37] Correct symbols position --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 2d76b104..1eedf0e7 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -758,7 +758,7 @@ class ScatterPlotItem(GraphicsObject): for i in range(len(self.data)): rec = data[i] p.resetTransform() - p.translate(pts[0,i], pts[1,i]) + p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']) drawSymbol(p, *self.getSpotOpts(rec, scale)) else: if self.picture is None: @@ -769,7 +769,7 @@ class ScatterPlotItem(GraphicsObject): rec = rec.copy() rec['size'] *= scale p2.resetTransform() - p2.translate(rec['x']+rec['width'], rec['y']+rec['width']) + p2.translate(rec['x'], rec['y']) drawSymbol(p2, *self.getSpotOpts(rec, scale)) p2.end() From 9de301155683f6a55bbbecad23ebb04c50748c68 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Dec 2013 12:05:27 -0500 Subject: [PATCH 10/37] minor setup corrections --- MANIFEST.in | 2 +- tools/debian/control | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 02d67f6f..f4158fac 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,5 @@ recursive-include examples *.py *.ui recursive-include doc *.rst *.py *.svg *.png *.jpg recursive-include doc/build/html * recursive-include tools * -include doc/Makefile doc/make.bat README.txt LICENSE.txt +include doc/Makefile doc/make.bat README.md LICENSE.txt CHANGELOG diff --git a/tools/debian/control b/tools/debian/control index 7ab6f28a..5ea2d4f2 100644 --- a/tools/debian/control +++ b/tools/debian/control @@ -7,7 +7,7 @@ Build-Depends: debhelper (>= 8) Package: python-pyqtgraph Architecture: all -Homepage: http://luke.campagnola.me/code/pyqtgraph +Homepage: http://www.pyqtgraph.org Depends: python (>= 2.6), python-support (>= 0.90), python-qt4 | python-pyside, python-scipy, python-numpy, ${misc:Depends} Suggests: python-opengl, python-qt4-gl Description: Scientific Graphics and GUI Library for Python From 2c2135a49f68eb5135fc72ee78675dc7cbbe8cd0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Dec 2013 21:06:31 -0500 Subject: [PATCH 11/37] Major updates to ComboBox: - Essentially a graphical interface to dict; all items have text and value - Assigns previously-selected text after list is cleared and repopulated - Get, set current value --- pyqtgraph/widgets/ComboBox.py | 201 +++++++++++++++++++++-- pyqtgraph/widgets/tests/test_combobox.py | 44 +++++ 2 files changed, 230 insertions(+), 15 deletions(-) create mode 100644 pyqtgraph/widgets/tests/test_combobox.py diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index 72ac384f..66ea4205 100644 --- a/pyqtgraph/widgets/ComboBox.py +++ b/pyqtgraph/widgets/ComboBox.py @@ -1,41 +1,212 @@ from ..Qt import QtGui, QtCore from ..SignalProxy import SignalProxy - +from ..ordereddict import OrderedDict +from ..python2_3 import asUnicode class ComboBox(QtGui.QComboBox): """Extends QComboBox to add extra functionality. - - updateList() - updates the items in the comboBox while blocking signals, remembers and resets to the previous values if it's still in the list + + * Handles dict mappings -- user selects a text key, and the ComboBox indicates + the selected value. + * Requires item strings to be unique + * Remembers selected value if list is cleared and subsequently repopulated + * setItems() replaces the items in the ComboBox and blocks signals if the + value ultimately does not change. """ def __init__(self, parent=None, items=None, default=None): QtGui.QComboBox.__init__(self, parent) + self.currentIndexChanged.connect(self.indexChanged) + self._ignoreIndexChange = False - #self.value = default + self._chosenText = None + self._items = OrderedDict() if items is not None: - self.addItems(items) + self.setItems(items) if default is not None: self.setValue(default) def setValue(self, value): - ind = self.findText(value) + """Set the selected item to the first one having the given value.""" + text = None + for k,v in self._items.items(): + if v == value: + text = k + break + if text is None: + raise ValueError(value) + + self.setText(text) + + def setText(self, text): + """Set the selected item to the first one having the given text.""" + ind = self.findText(text) if ind == -1: - return + raise ValueError(text) #self.value = value - self.setCurrentIndex(ind) - - def updateList(self, items): - prevVal = str(self.currentText()) - try: + self.setCurrentIndex(ind) + + def value(self): + """ + If items were given as a list of strings, then return the currently + selected text. If items were given as a dict, then return the value + corresponding to the currently selected key. If the combo list is empty, + return None. + """ + if self.count() == 0: + return None + text = asUnicode(self.currentText()) + return self._items[text] + + def ignoreIndexChange(func): + # Decorator that prevents updates to self._chosenText + def fn(self, *args, **kwds): + prev = self._ignoreIndexChange + self._ignoreIndexChange = True + try: + ret = func(self, *args, **kwds) + finally: + self._ignoreIndexChange = prev + return ret + return fn + + def blockIfUnchanged(func): + # decorator that blocks signal emission during complex operations + # and emits currentIndexChanged only if the value has actually + # changed at the end. + def fn(self, *args, **kwds): + prevVal = self.value() + blocked = self.signalsBlocked() self.blockSignals(True) + try: + ret = func(self, *args, **kwds) + finally: + self.blockSignals(blocked) + + # only emit if the value has changed + if self.value() != prevVal: + self.currentIndexChanged.emit(self.currentIndex()) + + return ret + return fn + + @ignoreIndexChange + @blockIfUnchanged + def setItems(self, items): + """ + *items* may be a list or a dict. + If a dict is given, then the keys are used to populate the combo box + and the values will be used for both value() and setValue(). + """ + prevVal = self.value() + + self.blockSignals(True) + try: self.clear() self.addItems(items) - self.setValue(prevVal) - finally: self.blockSignals(False) - if str(self.currentText()) != prevVal: + # only emit if we were not able to re-set the original value + if self.value() != prevVal: self.currentIndexChanged.emit(self.currentIndex()) - \ No newline at end of file + + def items(self): + return self.items.copy() + + def updateList(self, items): + # for backward compatibility + return self.setItems(items) + + def indexChanged(self, index): + # current index has changed; need to remember new 'chosen text' + if self._ignoreIndexChange: + return + self._chosenText = asUnicode(self.currentText()) + + def setCurrentIndex(self, index): + QtGui.QComboBox.setCurrentIndex(self, index) + + def itemsChanged(self): + # try to set the value to the last one selected, if it is available. + if self._chosenText is not None: + try: + self.setText(self._chosenText) + except ValueError: + pass + + @ignoreIndexChange + def insertItem(self, *args): + raise NotImplementedError() + #QtGui.QComboBox.insertItem(self, *args) + #self.itemsChanged() + + @ignoreIndexChange + def insertItems(self, *args): + raise NotImplementedError() + #QtGui.QComboBox.insertItems(self, *args) + #self.itemsChanged() + + @ignoreIndexChange + def addItem(self, *args, **kwds): + # Need to handle two different function signatures for QComboBox.addItem + try: + if isinstance(args[0], basestring): + text = args[0] + if len(args) == 2: + value = args[1] + else: + value = kwds.get('value', text) + else: + text = args[1] + if len(args) == 3: + value = args[2] + else: + value = kwds.get('value', text) + + except IndexError: + raise TypeError("First or second argument of addItem must be a string.") + + if text in self._items: + raise Exception('ComboBox already has item named "%s".' % text) + + self._items[text] = value + QtGui.QComboBox.addItem(self, *args) + self.itemsChanged() + + def setItemValue(self, name, value): + if name not in self._items: + self.addItem(name, value) + else: + self._items[name] = value + + @ignoreIndexChange + @blockIfUnchanged + def addItems(self, items): + if isinstance(items, list): + texts = items + items = dict([(x, x) for x in items]) + elif isinstance(items, dict): + texts = items.keys() + else: + raise TypeError("items argument must be list or dict.") + + for t in texts: + if t in self._items: + raise Exception('ComboBox already has item named "%s".' % t) + + + for k,v in items.items(): + self._items[k] = v + QtGui.QComboBox.addItems(self, texts) + + self.itemsChanged() + + @ignoreIndexChange + def clear(self): + self._items = OrderedDict() + QtGui.QComboBox.clear(self) + self.itemsChanged() + diff --git a/pyqtgraph/widgets/tests/test_combobox.py b/pyqtgraph/widgets/tests/test_combobox.py new file mode 100644 index 00000000..300489e0 --- /dev/null +++ b/pyqtgraph/widgets/tests/test_combobox.py @@ -0,0 +1,44 @@ +import pyqtgraph as pg +pg.mkQApp() + +def test_combobox(): + cb = pg.ComboBox() + items = {'a': 1, 'b': 2, 'c': 3} + cb.setItems(items) + cb.setValue(2) + assert str(cb.currentText()) == 'b' + assert cb.value() == 2 + + # Clear item list; value should be None + cb.clear() + assert cb.value() == None + + # Reset item list; value should be set automatically + cb.setItems(items) + assert cb.value() == 2 + + # Clear item list; repopulate with same names and new values + items = {'a': 4, 'b': 5, 'c': 6} + cb.clear() + cb.setItems(items) + assert cb.value() == 5 + + # Set list instead of dict + cb.setItems(items.keys()) + assert str(cb.currentText()) == 'b' + + cb.setValue('c') + assert cb.value() == str(cb.currentText()) + assert cb.value() == 'c' + + cb.setItemValue('c', 7) + assert cb.value() == 7 + + +if __name__ == '__main__': + cb = pg.ComboBox() + cb.show() + cb.setItems({'': None, 'a': 1, 'b': 2, 'c': 3}) + def fn(ind): + print "New value:", cb.value() + cb.currentIndexChanged.connect(fn) \ No newline at end of file From 4886270b53cc21649a29810cc897674371307ea0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Dec 2013 21:07:03 -0500 Subject: [PATCH 12/37] PlotNode control widget now displays combo box to let user select plots to connect to Flowchart example updated to use this feature. --- examples/Flowchart.py | 4 ++ pyqtgraph/flowchart/library/Display.py | 68 +++++++++++++++----- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 23 ------- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/examples/Flowchart.py b/examples/Flowchart.py index 09ea1f93..86c2564b 100644 --- a/examples/Flowchart.py +++ b/examples/Flowchart.py @@ -58,11 +58,15 @@ fc.setInput(dataIn=data) ## populate the flowchart with a basic set of processing nodes. ## (usually we let the user do this) +plotList = {'Top Plot': pw1, 'Bottom Plot': pw2} + pw1Node = fc.createNode('PlotWidget', pos=(0, -150)) +pw1Node.setPlotList(plotList) pw1Node.setPlot(pw1) pw2Node = fc.createNode('PlotWidget', pos=(150, -150)) pw2Node.setPlot(pw2) +pw2Node.setPlotList(plotList) fNode = fc.createNode('GaussianFilter', pos=(0, 0)) fNode.ctrls['sigma'].setValue(5) diff --git a/pyqtgraph/flowchart/library/Display.py b/pyqtgraph/flowchart/library/Display.py index 2c352fb2..642e6491 100644 --- a/pyqtgraph/flowchart/library/Display.py +++ b/pyqtgraph/flowchart/library/Display.py @@ -4,7 +4,7 @@ import weakref from ...Qt import QtCore, QtGui from ...graphicsItems.ScatterPlotItem import ScatterPlotItem from ...graphicsItems.PlotCurveItem import PlotCurveItem -from ... import PlotDataItem +from ... import PlotDataItem, ComboBox from .common import * import numpy as np @@ -16,7 +16,9 @@ class PlotWidgetNode(Node): def __init__(self, name): Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}}) - self.plot = None + self.plot = None # currently selected plot + self.plots = {} # list of available plots user may select from + self.ui = None self.items = {} def disconnected(self, localTerm, remoteTerm): @@ -26,16 +28,27 @@ class PlotWidgetNode(Node): def setPlot(self, plot): #print "======set plot" + if plot == self.plot: + return + + # clear data from previous plot + if self.plot is not None: + for vid in list(self.items.keys()): + self.plot.removeItem(self.items[vid]) + del self.items[vid] + self.plot = plot + self.updateUi() + self.update() self.sigPlotChanged.emit(self) def getPlot(self): return self.plot def process(self, In, display=True): - if display: - #self.plot.clearPlots() + if display and self.plot is not None: items = set() + # Add all new input items to selected plot for name, vals in In.items(): if vals is None: continue @@ -45,14 +58,13 @@ class PlotWidgetNode(Node): for val in vals: vid = id(val) if vid in self.items and self.items[vid].scene() is self.plot.scene(): + # Item is already added to the correct scene + # possible bug: what if two plots occupy the same scene? (should + # rarely be a problem because items are removed from a plot before + # switching). items.add(vid) else: - #if isinstance(val, PlotCurveItem): - #self.plot.addItem(val) - #item = val - #if isinstance(val, ScatterPlotItem): - #self.plot.addItem(val) - #item = val + # Add the item to the plot, or generate a new item if needed. if isinstance(val, QtGui.QGraphicsItem): self.plot.addItem(val) item = val @@ -60,22 +72,48 @@ class PlotWidgetNode(Node): item = self.plot.plot(val) self.items[vid] = item items.add(vid) + + # Any left-over items that did not appear in the input must be removed for vid in list(self.items.keys()): if vid not in items: - #print "remove", self.items[vid] self.plot.removeItem(self.items[vid]) del self.items[vid] def processBypassed(self, args): + if self.plot is None: + return for item in list(self.items.values()): self.plot.removeItem(item) self.items = {} - #def setInput(self, **args): - #for k in args: - #self.plot.plot(args[k]) + def ctrlWidget(self): + if self.ui is None: + self.ui = ComboBox() + self.ui.currentIndexChanged.connect(self.plotSelected) + self.updateUi() + return self.ui - + def plotSelected(self, index): + self.setPlot(self.ui.value()) + + def setPlotList(self, plots): + """ + Specify the set of plots (PlotWidget or PlotItem) that the user may + select from. + + *plots* must be a dictionary of {name: plot} pairs. + """ + self.plots = plots + self.updateUi() + + def updateUi(self): + # sets list and automatically preserves previous selection + self.ui.setItems(self.plots) + try: + self.ui.setValue(self.plot) + except ValueError: + pass + class CanvasNode(Node): """Connection to a Canvas widget.""" diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index baff1aa9..575a1599 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -95,7 +95,6 @@ class PlotItem(GraphicsWidget): lastFileDir = None - managers = {} def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): """ @@ -369,28 +368,6 @@ class PlotItem(GraphicsWidget): self.scene().removeItem(self.vb) self.vb = None - ## causes invalid index errors: - #for i in range(self.layout.count()): - #self.layout.removeAt(i) - - #for p in self.proxies: - #try: - #p.setWidget(None) - #except RuntimeError: - #break - #self.scene().removeItem(p) - #self.proxies = [] - - #self.menuAction.releaseWidget(self.menuAction.defaultWidget()) - #self.menuAction.setParent(None) - #self.menuAction = None - - #if self.manager is not None: - #self.manager.sigWidgetListChanged.disconnect(self.updatePlotList) - #self.manager.removeWidget(self.name) - #else: - #print "no manager" - def registerPlot(self, name): ## for backward compatibility self.vb.register(name) From 2aac1faa176552be07c23b069c1196e1b936ae9e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Dec 2013 22:32:05 -0500 Subject: [PATCH 13/37] fixes for python3 --- pyqtgraph/widgets/ComboBox.py | 8 ++++---- pyqtgraph/widgets/tests/test_combobox.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index 66ea4205..f9983c97 100644 --- a/pyqtgraph/widgets/ComboBox.py +++ b/pyqtgraph/widgets/ComboBox.py @@ -1,6 +1,6 @@ from ..Qt import QtGui, QtCore from ..SignalProxy import SignalProxy -from ..ordereddict import OrderedDict +from ..pgcollections import OrderedDict from ..python2_3 import asUnicode class ComboBox(QtGui.QComboBox): @@ -189,9 +189,9 @@ class ComboBox(QtGui.QComboBox): texts = items items = dict([(x, x) for x in items]) elif isinstance(items, dict): - texts = items.keys() + texts = list(items.keys()) else: - raise TypeError("items argument must be list or dict.") + raise TypeError("items argument must be list or dict (got %s)." % type(items)) for t in texts: if t in self._items: @@ -200,7 +200,7 @@ class ComboBox(QtGui.QComboBox): for k,v in items.items(): self._items[k] = v - QtGui.QComboBox.addItems(self, texts) + QtGui.QComboBox.addItems(self, list(texts)) self.itemsChanged() diff --git a/pyqtgraph/widgets/tests/test_combobox.py b/pyqtgraph/widgets/tests/test_combobox.py index 300489e0..f511331c 100644 --- a/pyqtgraph/widgets/tests/test_combobox.py +++ b/pyqtgraph/widgets/tests/test_combobox.py @@ -24,7 +24,7 @@ def test_combobox(): assert cb.value() == 5 # Set list instead of dict - cb.setItems(items.keys()) + cb.setItems(list(items.keys())) assert str(cb.currentText()) == 'b' cb.setValue('c') @@ -40,5 +40,5 @@ if __name__ == '__main__': cb.show() cb.setItems({'': None, 'a': 1, 'b': 2, 'c': 3}) def fn(ind): - print "New value:", cb.value() + print("New value: %s" % cb.value()) cb.currentIndexChanged.connect(fn) \ No newline at end of file From a199b75c660300f2b6578d8cd3a78a39a56658f8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Dec 2013 22:48:44 -0500 Subject: [PATCH 14/37] Added Flowchart.sigChartChanged --- CHANGELOG | 6 ++++++ pyqtgraph/flowchart/Flowchart.py | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 64a030d6..66b01d1c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,12 +10,18 @@ pyqtgraph-0.9.9 [unreleased] commit will result in a more descriptive version string. - Speed improvements in functions.makeARGB - ImageItem is faster by avoiding makeQImage(transpose=True) + - ComboBox will raise error when adding multiple items of the same name New Features: - New HDF5 example for working with very large datasets - Added Qt.loadUiType function for PySide - Simplified Profilers; can be activated with environmental variables - Added Dock.raiseDock() method + - ComboBox updates: + - Essentially a graphical interface to dict; all items have text and value + - Assigns previously-selected text after list is cleared and repopulated + - Get, set current value + - Added Flowchart.sigChartChanged Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 8d1ea4ce..27586040 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -58,14 +58,15 @@ def toposort(deps, nodes=None, seen=None, stack=None, depth=0): class Flowchart(Node): - sigFileLoaded = QtCore.Signal(object) sigFileSaved = QtCore.Signal(object) #sigOutputChanged = QtCore.Signal() ## inherited from Node sigChartLoaded = QtCore.Signal() - sigStateChanged = QtCore.Signal() + sigStateChanged = QtCore.Signal() # called when output is expected to have changed + sigChartChanged = QtCore.Signal(object, object, object) # called when nodes are added, removed, or renamed. + # (self, action, node) def __init__(self, terminals=None, name=None, filePath=None, library=None): self.library = library or LIBRARY @@ -218,6 +219,7 @@ class Flowchart(Node): node.sigClosed.connect(self.nodeClosed) node.sigRenamed.connect(self.nodeRenamed) node.sigOutputChanged.connect(self.nodeOutputChanged) + self.sigChartChanged.emit(self, 'add', node) def removeNode(self, node): node.close() @@ -237,11 +239,13 @@ class Flowchart(Node): node.sigOutputChanged.disconnect(self.nodeOutputChanged) except TypeError: pass + self.sigChartChanged.emit(self, 'remove', node) def nodeRenamed(self, node, oldName): del self._nodes[oldName] self._nodes[node.name()] = node self.widget().nodeRenamed(node, oldName) + self.sigChartChanged.emit(self, 'rename', node) def arrangeNodes(self): pass From 21c1686221a09e8a49c5f6cef648ce4a915c481d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 29 Dec 2013 09:17:23 -0500 Subject: [PATCH 15/37] Fixed AxisItem to support unicode in tick strings --- CHANGELOG | 1 + README.md | 6 ++++-- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 66b01d1c..fd48d5b2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,6 +34,7 @@ pyqtgraph-0.9.9 [unreleased] - Gave .name() methods to PlotDataItem, PlotCurveItem, and ScatterPlotItem - fixed ImageItem handling of rgb images - fixed makeARGB re-ordering of color channels + - fixed unicode usage in AxisItem tick strings pyqtgraph-0.9.8 2013-11-24 diff --git a/README.md b/README.md index b5f83be2..47377410 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Contributors * Felix Schill * Guillaume Poulin * Antony Lee + * Mattias Põldaru Requirements ------------ @@ -43,7 +44,8 @@ Installation Methods -------------------- * To use with a specific project, simply copy the pyqtgraph subdirectory - anywhere that is importable from your project + anywhere that is importable from your project. PyQtGraph may also be + used as a git subtree by cloning the git-core repository from github. * To install system-wide from source distribution: `$ python setup.py install` * For instalation packages, see the website (pyqtgraph.org) @@ -62,4 +64,4 @@ Some (incomplete) documentation exists at this time. `$ make html` Please feel free to pester Luke or post to the forum if you need a specific - section of documentation. + section of documentation to be expanded. diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 1d0b36b6..425fdd93 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -795,7 +795,7 @@ class AxisItem(GraphicsWidget): if s is None: rects.append(None) else: - br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s)) + br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, asUnicode(s)) ## boundingRect is usually just a bit too large ## (but this probably depends on per-font metrics?) br.setHeight(br.height() * 0.8) @@ -830,7 +830,7 @@ class AxisItem(GraphicsWidget): vstr = strings[j] if vstr is None: ## this tick was ignored because it is out of bounds continue - vstr = str(vstr) + vstr = asUnicode(vstr) x = tickPositions[i][j] #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) textRect = rects[j] From 95dd56bdb6407b81da00c01727cd9bb808562d21 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 1 Jan 2014 20:22:13 -0500 Subject: [PATCH 16/37] Bugfixes: - fixed PlotCurveItem generating exceptions when data has length=0 - fixed ImageView.setImage only working once --- CHANGELOG | 2 ++ pyqtgraph/graphicsItems/PlotCurveItem.py | 8 +++++--- pyqtgraph/imageview/ImageView.py | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fd48d5b2..41f63e9d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,8 @@ pyqtgraph-0.9.9 [unreleased] - fixed ImageItem handling of rgb images - fixed makeARGB re-ordering of color channels - fixed unicode usage in AxisItem tick strings + - fixed PlotCurveItem generating exceptions when data has length=0 + - fixed ImageView.setImage only working once pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index b2beaa99..ea337100 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -393,16 +393,18 @@ class PlotCurveItem(GraphicsObject): if self.path is None: x,y = self.getData() if x is None or len(x) == 0 or y is None or len(y) == 0: - return QtGui.QPainterPath() - self.path = self.generatePath(*self.getData()) + self.path = QtGui.QPainterPath() + else: + self.path = self.generatePath(*self.getData()) self.fillPath = None self._mouseShape = None + return self.path @debug.warnOnException ## raising an exception here causes crash def paint(self, p, opt, widget): profiler = debug.Profiler() - if self.xData is None: + if self.xData is None or len(self.xData) == 0: return if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget): diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index d4458a0e..c50a54c0 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -198,6 +198,7 @@ class ImageView(QtGui.QWidget): if not isinstance(img, np.ndarray): raise Exception("Image must be specified as ndarray.") self.image = img + self.imageDisp = None if xvals is not None: self.tVals = xvals From eda59be46d92dcaadf63ec1d2fe6c9f1c98a9d89 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 4 Jan 2014 01:03:58 -0500 Subject: [PATCH 17/37] corrected PolyLineROI.setPen() to modify individual segments as well. --- CHANGELOG | 1 + pyqtgraph/graphicsItems/ROI.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 41f63e9d..e9ab5d7d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -37,6 +37,7 @@ pyqtgraph-0.9.9 [unreleased] - fixed unicode usage in AxisItem tick strings - fixed PlotCurveItem generating exceptions when data has length=0 - fixed ImageView.setImage only working once + - PolyLineROI.setPen() now changes the pen of its segments as well pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 0dee2fd4..b99465b5 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1623,9 +1623,9 @@ class PolyLineROI(ROI): if pos is None: pos = [0,0] - ROI.__init__(self, pos, size=[1,1], **args) self.closed = closed self.segments = [] + ROI.__init__(self, pos, size=[1,1], **args) for p in positions: self.addFreeHandle(p) @@ -1750,6 +1750,10 @@ class PolyLineROI(ROI): shape[axes[1]] = sliced.shape[axes[1]] return sliced * mask.reshape(shape) + def setPen(self, *args, **kwds): + ROI.setPen(self, *args, **kwds) + for seg in self.segments: + seg.setPen(*args, **kwds) class LineSegmentROI(ROI): """ From 33e4a9e21307cc2c2235a838f002a1c961de66a7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 5 Jan 2014 14:40:56 -0500 Subject: [PATCH 18/37] Fix: prevent divide-by-zero in AxisItem --- CHANGELOG | 2 +- pyqtgraph/graphicsItems/AxisItem.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e9ab5d7d..1457e9f6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,7 +38,7 @@ pyqtgraph-0.9.9 [unreleased] - fixed PlotCurveItem generating exceptions when data has length=0 - fixed ImageView.setImage only working once - PolyLineROI.setPen() now changes the pen of its segments as well - + - Prevent divide-by-zero in AxisItem pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 425fdd93..0ddd02a8 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -458,8 +458,7 @@ class AxisItem(GraphicsWidget): return [] ## decide optimal minor tick spacing in pixels (this is just aesthetics) - pixelSpacing = size / np.log(size) - optimalTickCount = max(2., size / pixelSpacing) + optimalTickCount = max(2., np.log(size)) ## optimal minor tick spacing optimalSpacing = dif / optimalTickCount From 7a45b9a0e2275a038db2945e1ac2c2e426e94344 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 12 Jan 2014 10:35:31 -0500 Subject: [PATCH 19/37] Reorganized setup.py code Added "deb" setup command --- .gitignore | 3 + setup.py | 230 ++++++++++++++++++++----------------- tools/generateChangelog.py | 122 ++++++++++---------- tools/setupHelpers.py | 114 ++++++++++++++++++ 4 files changed, 304 insertions(+), 165 deletions(-) create mode 100644 tools/setupHelpers.py diff --git a/.gitignore b/.gitignore index 28ed45aa..b8e7af73 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ __pycache__ build *.pyc *.swp +MANIFEST +deb_build +dist diff --git a/setup.py b/setup.py index 826bb57c..761a090d 100644 --- a/setup.py +++ b/setup.py @@ -1,105 +1,4 @@ -from distutils.core import setup -import distutils.dir_util -import os, sys, re -from subprocess import check_output - -## generate list of all sub-packages -path = os.path.abspath(os.path.dirname(__file__)) -n = len(path.split(os.path.sep)) -subdirs = [i[0].split(os.path.sep)[n:] for i in os.walk(os.path.join(path, 'pyqtgraph')) if '__init__.py' in i[2]] -all_packages = ['.'.join(p) for p in subdirs] + ['pyqtgraph.examples'] - - -## Make sure build directory is clean before installing -buildPath = os.path.join(path, 'build') -if os.path.isdir(buildPath): - distutils.dir_util.remove_tree(buildPath) - - -## Determine current version string -initfile = os.path.join(path, 'pyqtgraph', '__init__.py') -init = open(initfile).read() -m = re.search(r'__version__ = (\S+)\n', init) -if m is None or len(m.groups()) != 1: - raise Exception("Cannot determine __version__ from init file: '%s'!" % initfile) -version = m.group(1).strip('\'\"') -initVersion = version - -# If this is a git checkout, try to generate a more decriptive version string -try: - if os.path.isdir(os.path.join(path, '.git')): - def gitCommit(name): - commit = check_output(['git', 'show', name], universal_newlines=True).split('\n')[0] - assert commit[:7] == 'commit ' - return commit[7:] - - # Find last tag matching "pyqtgraph-.*" - tagNames = check_output(['git', 'tag'], universal_newlines=True).strip().split('\n') - while True: - if len(tagNames) == 0: - raise Exception("Could not determine last tagged version.") - lastTagName = tagNames.pop() - if re.match(r'pyqtgraph-.*', lastTagName): - break - - # is this commit an unchanged checkout of the last tagged version? - lastTag = gitCommit(lastTagName) - head = gitCommit('HEAD') - if head != lastTag: - branch = re.search(r'\* (.*)', check_output(['git', 'branch'], universal_newlines=True)).group(1) - version = version + "-%s-%s" % (branch, head[:10]) - - # any uncommitted modifications? - modified = False - status = check_output(['git', 'status', '-s'], universal_newlines=True).strip().split('\n') - for line in status: - if line[:2] != '??': - modified = True - break - - if modified: - version = version + '+' - sys.stderr.write("Detected git commit; will use version string: '%s'\n" % version) -except: - version = initVersion - sys.stderr.write("This appears to be a git checkout, but an error occurred " - "while attempting to determine a version string for the " - "current commit.\nUsing the unmodified version string " - "instead: '%s'\n" % version) - sys.excepthook(*sys.exc_info()) - - -import distutils.command.build - -class Build(distutils.command.build.build): - def run(self): - ret = distutils.command.build.build.run(self) - - # If the version in __init__ is different from the automatically-generated - # version string, then we will update __init__ in the build directory - global path, version, initVersion - if initVersion == version: - return ret - - initfile = os.path.join(path, self.build_lib, 'pyqtgraph', '__init__.py') - if not os.path.isfile(initfile): - sys.stderr.write("Warning: setup detected a git install and attempted " - "to generate a descriptive version string; however, " - "the expected build file at %s was not found. " - "Installation will use the original version string " - "%s instead.\n" % (initfile, initVersion) - ) - else: - data = open(initfile, 'r').read() - open(initfile, 'w').write(re.sub(r"__version__ = .*", "__version__ = '%s'" % version, data)) - return ret - - -setup(name='pyqtgraph', - version=version, - cmdclass={'build': Build}, - description='Scientific Graphics and GUI Library for Python', - long_description="""\ +DESCRIPTION = """\ PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and numpy. @@ -107,14 +6,16 @@ It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching, Qt's GraphicsView framework for 2D display, and OpenGL for 3D display. -""", +""" + +setupOpts = dict( + name='pyqtgraph', + description='Scientific Graphics and GUI Library for Python', + long_description=DESCRIPTION, license='MIT', url='http://www.pyqtgraph.org', author='Luke Campagnola', author_email='luke.campagnola@gmail.com', - packages=all_packages, - package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source - #package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']}, classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 2", @@ -130,9 +31,126 @@ heavy leverage of numpy for number crunching, Qt's GraphicsView framework for "Topic :: Scientific/Engineering :: Visualization", "Topic :: Software Development :: User Interfaces", ], +) + + +from distutils.core import setup +import distutils.dir_util +import os, sys, re + +path = os.path.split(__file__)[0] +sys.path.insert(0, os.path.join(path, 'tools')) +import setupHelpers as helpers + +## generate list of all sub-packages +allPackages = helpers.listAllPackages(pkgroot='pyqtgraph') + ['pyqtgraph.examples'] + +## Decide what version string to use in the build +version, forcedVersion, gitVersion, initVersion = helpers.getVersionStrings(pkg='pyqtgraph') + + +import distutils.command.build + +class Build(distutils.command.build.build): + """ + * Clear build path before building + * Set version string in __init__ after building + """ + def run(self): + global path, version, initVersion, forcedVersion + global buildVersion + + ## Make sure build directory is clean + buildPath = os.path.join(path, self.build_lib) + if os.path.isdir(buildPath): + distutils.dir_util.remove_tree(buildPath) + + ret = distutils.command.build.build.run(self) + + # If the version in __init__ is different from the automatically-generated + # version string, then we will update __init__ in the build directory + if initVersion == version: + return ret + + try: + initfile = os.path.join(buildPath, 'pyqtgraph', '__init__.py') + data = open(initfile, 'r').read() + open(initfile, 'w').write(re.sub(r"__version__ = .*", "__version__ = '%s'" % version, data)) + buildVersion = version + except: + if forcedVersion: + raise + buildVersion = initVersion + sys.stderr.write("Warning: Error occurred while setting version string in build path. " + "Installation will use the original version string " + "%s instead.\n" % (initVersion) + ) + sys.excepthook(*sys.exc_info()) + return ret + +from distutils.core import Command +import shutil, subprocess + +class DebCommand(Command): + description = "build .deb package" + user_options = [] + def initialize_options(self): + self.cwd = None + def finalize_options(self): + self.cwd = os.getcwd() + def run(self): + assert os.getcwd() == self.cwd, 'Must be in package root: %s' % self.cwd + global version + pkgName = "python-pyqtgraph-" + version + debDir = "deb_build" + if os.path.isdir(debDir): + raise Exception('DEB build dir already exists: "%s"' % debDir) + sdist = "dist/pyqtgraph-%s.tar.gz" % version + if not os.path.isfile(sdist): + raise Exception("No source distribution; run `setup.py sdist` first.") + + # copy sdist to build directory and extract + os.mkdir(debDir) + renamedSdist = 'python-pyqtgraph_%s.orig.tar.gz' % version + shutil.copy(sdist, os.path.join(debDir, renamedSdist)) + if os.system("cd %s; tar -xzf %s" % (debDir, renamedSdist)) != 0: + raise Exception("Error extracting source distribution.") + buildDir = '%s/pyqtgraph-%s' % (debDir, version) + + # copy debian control structure + shutil.copytree('tools/debian', buildDir+'/debian') + + # Write changelog + #chlog = subprocess.check_output([sys.executable, 'tools/generateChangelog.py', 'CHANGELOG']) + #open('%s/pyqtgraph-%s/debian/changelog', 'w').write(chlog) + if os.system('python tools/generateChangelog.py CHANGELOG %s > %s/debian/changelog' % (version, buildDir)) != 0: + raise Exception("Error writing debian/changelog") + + # build package + if os.system('cd %s; debuild -us -uc' % buildDir) != 0: + raise Exception("Error during debuild.") + +class TestCommand(Command): + description = "" + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + global cmd + cmd = self + +setup( + version=version, + cmdclass={'build': Build, 'deb': DebCommand, 'test': TestCommand}, + packages=allPackages, + package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source + #package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']}, install_requires = [ 'numpy', 'scipy', ], + **setupOpts ) diff --git a/tools/generateChangelog.py b/tools/generateChangelog.py index 0c8bf3e6..32c9ad2c 100644 --- a/tools/generateChangelog.py +++ b/tools/generateChangelog.py @@ -1,66 +1,70 @@ -from subprocess import check_output -import re, time +import re, time, sys +if len(sys.argv) < 3: + sys.stderr.write("Must specify changelog file and latest release!\n") + sys.exit(-1) -def run(cmd): - return check_output(cmd, shell=True) +### Convert CHANGELOG format like: +""" +pyqtgraph-0.9.1 2012-12-29 -tags = run('bzr tags') -versions = [] -for tag in tags.split('\n'): - if tag.strip() == '': - continue - ver, rev = re.split(r'\s+', tag) - if ver.startswith('pyqtgraph-'): - versions.append(ver) + - change + - change +""" -for i in range(len(versions)-1)[::-1]: - log = run('bzr log -r tag:%s..tag:%s' % (versions[i], versions[i+1])) - changes = [] - times = [] - inmsg = False - for line in log.split('\n'): - if line.startswith('message:'): - inmsg = True - continue - elif line.startswith('-----------------------'): - inmsg = False - continue - - if inmsg: - changes.append(line) - else: - m = re.match(r'timestamp:\s+(.*)$', line) - if m is not None: - times.append(m.groups()[0]) - - citime = time.strptime(times[0][:-6], '%a %Y-%m-%d %H:%M:%S') - - print "python-pyqtgraph (%s-1) UNRELEASED; urgency=low" % versions[i+1].split('-')[1] - print "" - for line in changes: - for n in range(len(line)): - if line[n] != ' ': - n += 1 - break - - words = line.split(' ') - nextline = '' - for w in words: - if len(w) + len(nextline) > 79: - print nextline - nextline = (' '*n) + w - else: - nextline += ' ' + w - print nextline - #print '\n'.join(changes) - print "" - print " -- Luke %s -0%d00" % (time.strftime('%a, %d %b %Y %H:%M:%S', citime), time.timezone/3600) - #print " -- Luke %s -0%d00" % (times[0], time.timezone/3600) - print "" - -print """python-pyqtgraph (0.9.0-1) UNRELEASED; urgency=low +### to debian changelog format: +""" +python-pyqtgraph (0.9.1-1) UNRELEASED; urgency=low * Initial release. - -- Luke Thu, 27 Dec 2012 02:46:26 -0500""" + -- Luke Sat, 29 Dec 2012 01:07:23 -0500 +""" + + + +releases = [] +current_version = None +current_log = None +current_date = None +for line in open(sys.argv[1]).readlines(): + match = re.match(r'pyqtgraph-(\d+\.\d+\.\d+(\.\d+)?)\s*(\d+-\d+-\d+)\s*$', line) + if match is None: + if current_log is not None: + current_log.append(line) + else: + if current_log is not None: + releases.append((current_version, current_log, current_date)) + current_version, current_date = match.groups()[0], match.groups()[2] + #sys.stderr.write("Found release %s\n" % current_version) + current_log = [] + +if releases[0][0] != sys.argv[2]: + sys.stderr.write("Latest release in changelog (%s) does not match current release (%s)\n" % (releases[0][0], sys.argv[2])) + sys.exit(-1) + +for release, changes, date in releases: + date = time.strptime(date, '%Y-%m-%d') + changeset = [ + "python-pyqtgraph (%s-1) UNRELEASED; urgency=low\n" % release, + "\n"] + changes + [ + " -- Luke %s -0%d00\n" % (time.strftime('%a, %d %b %Y %H:%M:%S', date), time.timezone/3600), + "\n" ] + + # remove consecutive blank lines except between releases + clean = "" + lastBlank = True + for line in changeset: + if line.strip() == '': + if lastBlank: + continue + else: + clean += line + lastBlank = True + else: + clean += line + lastBlank = False + + print clean + print "" + diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py new file mode 100644 index 00000000..216e6cc2 --- /dev/null +++ b/tools/setupHelpers.py @@ -0,0 +1,114 @@ +import os, sys, re +from subprocess import check_output + +def listAllPackages(pkgroot): + path = os.getcwd() + n = len(path.split(os.path.sep)) + subdirs = [i[0].split(os.path.sep)[n:] for i in os.walk(os.path.join(path, pkgroot)) if '__init__.py' in i[2]] + return ['.'.join(p) for p in subdirs] + + +def getInitVersion(pkgroot): + """Return the version string defined in __init__.py""" + path = os.getcwd() + initfile = os.path.join(path, pkgroot, '__init__.py') + init = open(initfile).read() + m = re.search(r'__version__ = (\S+)\n', init) + if m is None or len(m.groups()) != 1: + raise Exception("Cannot determine __version__ from init file: '%s'!" % initfile) + version = m.group(1).strip('\'\"') + return version + +def gitCommit(name): + """Return the commit ID for the given name.""" + commit = check_output(['git', 'show', name], universal_newlines=True).split('\n')[0] + assert commit[:7] == 'commit ' + return commit[7:] + +def getGitVersion(tagPrefix): + """Return a version string with information about this git checkout. + If the checkout is an unmodified, tagged commit, then return the tag version. + If this is not a tagged commit, return version-branch_name-commit_id. + If this checkout has been modified, append "+" to the version. + """ + path = os.getcwd() + if not os.path.isdir(os.path.join(path, '.git')): + return None + + # Find last tag matching "tagPrefix.*" + tagNames = check_output(['git', 'tag'], universal_newlines=True).strip().split('\n') + while True: + if len(tagNames) == 0: + raise Exception("Could not determine last tagged version.") + lastTagName = tagNames.pop() + if re.match(tagPrefix+r'\d+\.\d+.*', lastTagName): + break + gitVersion = lastTagName.replace(tagPrefix, '') + + # is this commit an unchanged checkout of the last tagged version? + lastTag = gitCommit(lastTagName) + head = gitCommit('HEAD') + if head != lastTag: + branch = re.search(r'\* (.*)', check_output(['git', 'branch'], universal_newlines=True)).group(1) + gitVersion = gitVersion + "-%s-%s" % (branch, head[:10]) + + # any uncommitted modifications? + modified = False + status = check_output(['git', 'status', '-s'], universal_newlines=True).strip().split('\n') + for line in status: + if line[:2] != '??': + modified = True + break + + if modified: + gitVersion = gitVersion + '+' + + return gitVersion + +def getVersionStrings(pkg): + """ + Returns 4 version strings: + + * the version string to use for this build, + * version string requested with --force-version (or None) + * version string that describes the current git checkout (or None). + * version string in the pkg/__init__.py, + + The first return value is (forceVersion or gitVersion or initVersion). + """ + + ## Determine current version string from __init__.py + initVersion = getInitVersion(pkgroot='pyqtgraph') + + ## If this is a git checkout, try to generate a more descriptive version string + try: + gitVersion = getGitVersion(tagPrefix='pyqtgraph-') + except: + gitVersion = None + sys.stderr.write("This appears to be a git checkout, but an error occurred " + "while attempting to determine a version string for the " + "current commit.\n") + sys.excepthook(*sys.exc_info()) + + # See whether a --force-version flag was given + forcedVersion = None + for i,arg in enumerate(sys.argv): + if arg.startswith('--force-version'): + if arg == '--force-version': + forcedVersion = sys.argv[i+1] + sys.argv.pop(i) + sys.argv.pop(i) + elif arg.startswith('--force-version='): + forcedVersion = sys.argv[i].replace('--force-version=', '') + sys.argv.pop(i) + + ## Finally decide on a version string to use: + if forcedVersion is not None: + version = forcedVersion + elif gitVersion is not None: + version = gitVersion + sys.stderr.write("Detected git commit; will use version string: '%s'\n" % version) + else: + version = initVersion + + return version, forcedVersion, gitVersion, initVersion \ No newline at end of file From 9a131f763b698936818c3069b65d6f036c362018 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 12 Jan 2014 11:50:52 -0500 Subject: [PATCH 20/37] more reorganization to make setup-helpers code more generic --- setup.py | 55 +--------------- tools/debian/control | 2 +- tools/generateChangelog.py | 128 ++++++++++++++++++++----------------- tools/setupHelpers.py | 70 +++++++++++++++++++- 4 files changed, 141 insertions(+), 114 deletions(-) diff --git a/setup.py b/setup.py index 761a090d..16d66f61 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ class Build(distutils.command.build.build): def run(self): global path, version, initVersion, forcedVersion global buildVersion - + ## Make sure build directory is clean buildPath = os.path.join(path, self.build_lib) if os.path.isdir(buildPath): @@ -88,62 +88,11 @@ class Build(distutils.command.build.build): sys.excepthook(*sys.exc_info()) return ret -from distutils.core import Command -import shutil, subprocess -class DebCommand(Command): - description = "build .deb package" - user_options = [] - def initialize_options(self): - self.cwd = None - def finalize_options(self): - self.cwd = os.getcwd() - def run(self): - assert os.getcwd() == self.cwd, 'Must be in package root: %s' % self.cwd - global version - pkgName = "python-pyqtgraph-" + version - debDir = "deb_build" - if os.path.isdir(debDir): - raise Exception('DEB build dir already exists: "%s"' % debDir) - sdist = "dist/pyqtgraph-%s.tar.gz" % version - if not os.path.isfile(sdist): - raise Exception("No source distribution; run `setup.py sdist` first.") - - # copy sdist to build directory and extract - os.mkdir(debDir) - renamedSdist = 'python-pyqtgraph_%s.orig.tar.gz' % version - shutil.copy(sdist, os.path.join(debDir, renamedSdist)) - if os.system("cd %s; tar -xzf %s" % (debDir, renamedSdist)) != 0: - raise Exception("Error extracting source distribution.") - buildDir = '%s/pyqtgraph-%s' % (debDir, version) - - # copy debian control structure - shutil.copytree('tools/debian', buildDir+'/debian') - - # Write changelog - #chlog = subprocess.check_output([sys.executable, 'tools/generateChangelog.py', 'CHANGELOG']) - #open('%s/pyqtgraph-%s/debian/changelog', 'w').write(chlog) - if os.system('python tools/generateChangelog.py CHANGELOG %s > %s/debian/changelog' % (version, buildDir)) != 0: - raise Exception("Error writing debian/changelog") - - # build package - if os.system('cd %s; debuild -us -uc' % buildDir) != 0: - raise Exception("Error during debuild.") - -class TestCommand(Command): - description = "" - user_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - global cmd - cmd = self setup( version=version, - cmdclass={'build': Build, 'deb': DebCommand, 'test': TestCommand}, + cmdclass={'build': Build, 'deb': helpers.DebCommand, 'test': helpers.TestCommand}, packages=allPackages, package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source #package_data={'pyqtgraph': ['graphicsItems/PlotItem/*.png']}, diff --git a/tools/debian/control b/tools/debian/control index 5ea2d4f2..9d0519cc 100644 --- a/tools/debian/control +++ b/tools/debian/control @@ -2,7 +2,7 @@ Source: python-pyqtgraph Maintainer: Luke Campagnola Section: python Priority: optional -Standards-Version: 3.9.3 +Standards-Version: 3.9.4 Build-Depends: debhelper (>= 8) Package: python-pyqtgraph diff --git a/tools/generateChangelog.py b/tools/generateChangelog.py index 32c9ad2c..10601b35 100644 --- a/tools/generateChangelog.py +++ b/tools/generateChangelog.py @@ -1,70 +1,80 @@ import re, time, sys -if len(sys.argv) < 3: - sys.stderr.write("Must specify changelog file and latest release!\n") - sys.exit(-1) - -### Convert CHANGELOG format like: -""" -pyqtgraph-0.9.1 2012-12-29 - - - change - - change -""" - -### to debian changelog format: -""" -python-pyqtgraph (0.9.1-1) UNRELEASED; urgency=low - - * Initial release. - - -- Luke Sat, 29 Dec 2012 01:07:23 -0500 -""" +def generateDebianChangelog(package, logFile, version, maintainer): + """ + ------- Convert CHANGELOG format like: + pyqtgraph-0.9.1 2012-12-29 -releases = [] -current_version = None -current_log = None -current_date = None -for line in open(sys.argv[1]).readlines(): - match = re.match(r'pyqtgraph-(\d+\.\d+\.\d+(\.\d+)?)\s*(\d+-\d+-\d+)\s*$', line) - if match is None: - if current_log is not None: - current_log.append(line) - else: - if current_log is not None: - releases.append((current_version, current_log, current_date)) - current_version, current_date = match.groups()[0], match.groups()[2] - #sys.stderr.write("Found release %s\n" % current_version) - current_log = [] + - change + - change -if releases[0][0] != sys.argv[2]: - sys.stderr.write("Latest release in changelog (%s) does not match current release (%s)\n" % (releases[0][0], sys.argv[2])) - sys.exit(-1) -for release, changes, date in releases: - date = time.strptime(date, '%Y-%m-%d') - changeset = [ - "python-pyqtgraph (%s-1) UNRELEASED; urgency=low\n" % release, - "\n"] + changes + [ - " -- Luke %s -0%d00\n" % (time.strftime('%a, %d %b %Y %H:%M:%S', date), time.timezone/3600), - "\n" ] + -------- to debian changelog format: + python-pyqtgraph (0.9.1-1) UNRELEASED; urgency=low - # remove consecutive blank lines except between releases - clean = "" - lastBlank = True - for line in changeset: - if line.strip() == '': - if lastBlank: - continue + * Initial release. + + -- Luke Sat, 29 Dec 2012 01:07:23 -0500 + + + *package* is the name of the python package. + *logFile* is the CHANGELOG file to read; must have the format described above. + *version* will be used to check that the most recent log entry corresponds + to the current package version. + *maintainer* should be string like "Luke ". + """ + releases = [] + current_version = None + current_log = None + current_date = None + for line in open(logFile).readlines(): + match = re.match(package+r'-(\d+\.\d+\.\d+(\.\d+)?)\s*(\d+-\d+-\d+)\s*$', line) + if match is None: + if current_log is not None: + current_log.append(line) + else: + if current_log is not None: + releases.append((current_version, current_log, current_date)) + current_version, current_date = match.groups()[0], match.groups()[2] + #sys.stderr.write("Found release %s\n" % current_version) + current_log = [] + + if releases[0][0] != version: + raise Exception("Latest release in changelog (%s) does not match current release (%s)\n" % (releases[0][0], version)) + + output = [] + for release, changes, date in releases: + date = time.strptime(date, '%Y-%m-%d') + changeset = [ + "python-%s (%s-1) UNRELEASED; urgency=low\n" % (package, release), + "\n"] + changes + [ + " -- %s %s -0%d00\n" % (maintainer, time.strftime('%a, %d %b %Y %H:%M:%S', date), time.timezone/3600), + "\n" ] + + # remove consecutive blank lines except between releases + clean = "" + lastBlank = True + for line in changeset: + if line.strip() == '': + if lastBlank: + continue + else: + clean += line + lastBlank = True else: clean += line - lastBlank = True - else: - clean += line - lastBlank = False - - print clean - print "" + lastBlank = False + + output.append(clean) + output.append("") + return "\n".join(output) + "\n" +if __name__ == '__main__': + if len(sys.argv) < 5: + sys.stderr.write('Usage: generateChangelog.py package_name log_file version "Maintainer "\n') + sys.exit(-1) + + print generateDebianChangelog(*sys.argv[1:]) + diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index 216e6cc2..f1845ce4 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -111,4 +111,72 @@ def getVersionStrings(pkg): else: version = initVersion - return version, forcedVersion, gitVersion, initVersion \ No newline at end of file + return version, forcedVersion, gitVersion, initVersion + + + +from distutils.core import Command +import shutil, subprocess +from generateChangelog import generateDebianChangelog + +class DebCommand(Command): + description = "build .deb package using `debuild -us -uc`" + maintainer = "Luke " + debTemplate = "tools/debian" + debDir = "deb_build" + + user_options = [] + + def initialize_options(self): + self.cwd = None + + def finalize_options(self): + self.cwd = os.getcwd() + + def run(self): + version = self.distribution.get_version() + pkgName = self.distribution.get_name() + debName = "python-" + pkgName + debDir = self.debDir + + assert os.getcwd() == self.cwd, 'Must be in package root: %s' % self.cwd + + if os.path.isdir(debDir): + raise Exception('DEB build dir already exists: "%s"' % debDir) + sdist = "dist/%s-%s.tar.gz" % (pkgName, version) + if not os.path.isfile(sdist): + raise Exception("No source distribution; run `setup.py sdist` first.") + + # copy sdist to build directory and extract + os.mkdir(debDir) + renamedSdist = '%s_%s.orig.tar.gz' % (debName, version) + shutil.copy(sdist, os.path.join(debDir, renamedSdist)) + if os.system("cd %s; tar -xzf %s" % (debDir, renamedSdist)) != 0: + raise Exception("Error extracting source distribution.") + buildDir = '%s/%s-%s' % (debDir, pkgName, version) + + # copy debian control structure + shutil.copytree(self.debTemplate, buildDir+'/debian') + + # Write new changelog + chlog = generateDebianChangelog(pkgName, 'CHANGELOG', version, self.maintainer) + open(buildDir+'/debian/changelog', 'w').write(chlog) + + # build package + if os.system('cd %s; debuild -us -uc' % buildDir) != 0: + raise Exception("Error during debuild.") + + +class TestCommand(Command): + description = "" + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + global cmd + cmd = self + print self.distribution.name + print self.distribution.version + \ No newline at end of file From c8739e54257877eaa9b7a997540837ab90d93a7e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 12 Jan 2014 23:59:53 -0500 Subject: [PATCH 21/37] Updated build system to use pybuild - now generates python-pyqtgraph, python3-pyqtgraph, and python-pyqtgraph-doc packages - `python setup.py sdist deb` works --- MANIFEST.in | 1 + tools/debian/changelog | 5 ---- tools/debian/control | 39 +++++++++++++++++++++++--- tools/debian/files | 1 - tools/debian/postrm | 3 -- tools/debian/python-pyqtgraph.install | 1 + tools/debian/python3-pyqtgraph.install | 1 + tools/debian/rules | 13 ++++++++- tools/debian/watch | 3 ++ tools/generateChangelog.py | 2 +- tools/setupHelpers.py | 14 +++++---- 11 files changed, 63 insertions(+), 20 deletions(-) delete mode 100644 tools/debian/changelog delete mode 100644 tools/debian/files delete mode 100755 tools/debian/postrm create mode 100644 tools/debian/python-pyqtgraph.install create mode 100644 tools/debian/python3-pyqtgraph.install create mode 100644 tools/debian/watch diff --git a/MANIFEST.in b/MANIFEST.in index f4158fac..c6667d04 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,4 +5,5 @@ recursive-include doc *.rst *.py *.svg *.png *.jpg recursive-include doc/build/html * recursive-include tools * include doc/Makefile doc/make.bat README.md LICENSE.txt CHANGELOG +global-exclude *.pyc diff --git a/tools/debian/changelog b/tools/debian/changelog deleted file mode 100644 index 1edf45f3..00000000 --- a/tools/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -python-pyqtgraph (0.9.1-1) UNRELEASED; urgency=low - - * Initial release. - - -- Luke Sat, 29 Dec 2012 01:07:23 -0500 diff --git a/tools/debian/control b/tools/debian/control index 9d0519cc..a516920d 100644 --- a/tools/debian/control +++ b/tools/debian/control @@ -3,16 +3,47 @@ Maintainer: Luke Campagnola Section: python Priority: optional Standards-Version: 3.9.4 -Build-Depends: debhelper (>= 8) +Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools, python3-all, python3-setuptools, python-docutils, python-sphinx (>= 1.0.7+dfsg-1~) +X-Python-Version: >= 2.6 +X-Python3-Version: >= 3.2 Package: python-pyqtgraph Architecture: all Homepage: http://www.pyqtgraph.org -Depends: python (>= 2.6), python-support (>= 0.90), python-qt4 | python-pyside, python-scipy, python-numpy, ${misc:Depends} -Suggests: python-opengl, python-qt4-gl -Description: Scientific Graphics and GUI Library for Python +Depends: python-qt4 | python-pyside, python-scipy, python-numpy, ${python:Depends}, ${misc:Depends} +Suggests: python-pyqtgraph-doc, python-opengl, python-qt4-gl +Description: Scientific Graphics and GUI Library (Python 2) PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. It is intended for use in mathematics / scientific / engineering applications. Despite being written entirely in python, the library is very fast due to its heavy leverage of numpy for number crunching and Qt's GraphicsView framework for fast display. + . + This is the Python 2 version of the package. + +Package: python3-pyqtgraph +Architecture: all +Homepage: http://www.pyqtgraph.org +Depends: python-qt4 | python-pyside, python-scipy, python-numpy, ${python3:Depends}, ${misc:Depends} +Suggests: python-pyqtgraph-doc, python-opengl, python-qt4-gl +Description: Scientific Graphics and GUI Library (Python 3) + PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. + It is intended for use in mathematics / scientific / engineering applications. + Despite being written entirely in python, the library is very fast due to its + heavy leverage of numpy for number crunching and Qt's GraphicsView framework + for fast display. + . + This is the Python 3 version of the package. + +Package: python-pyqtgraph-doc +Architecture: all +Section: doc +Depends: ${sphinxdoc:Depends}, ${misc:Depends} +Description: Scientific Graphics and GUI Library (common documentation) + PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. + It is intended for use in mathematics / scientific / engineering applications. + Despite being written entirely in python, the library is very fast due to its + heavy leverage of numpy for number crunching and Qt's GraphicsView framework + for fast display. + . + This is the common documentation package. diff --git a/tools/debian/files b/tools/debian/files deleted file mode 100644 index 4af05533..00000000 --- a/tools/debian/files +++ /dev/null @@ -1 +0,0 @@ -python-pyqtgraph_0.9.1-1_all.deb python optional diff --git a/tools/debian/postrm b/tools/debian/postrm deleted file mode 100755 index e1eae9f2..00000000 --- a/tools/debian/postrm +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -e -#DEBHELPER# -rm -rf /usr/lib/python2.7/dist-packages/pyqtgraph diff --git a/tools/debian/python-pyqtgraph.install b/tools/debian/python-pyqtgraph.install new file mode 100644 index 00000000..b2cc1360 --- /dev/null +++ b/tools/debian/python-pyqtgraph.install @@ -0,0 +1 @@ +usr/lib/python2* diff --git a/tools/debian/python3-pyqtgraph.install b/tools/debian/python3-pyqtgraph.install new file mode 100644 index 00000000..4606faae --- /dev/null +++ b/tools/debian/python3-pyqtgraph.install @@ -0,0 +1 @@ +usr/lib/python3* diff --git a/tools/debian/rules b/tools/debian/rules index 2d33f6ac..3132fbfd 100755 --- a/tools/debian/rules +++ b/tools/debian/rules @@ -1,4 +1,15 @@ #!/usr/bin/make -f +#export DH_VERBOSE=1 +export PYBUILD_NAME=pyqtgraph %: - dh $@ + dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild + +override_dh_installdocs: + PYTHONPATH=`pwd` make -C doc html + dh_installdocs -ppython-pyqtgraph-doc doc/build/html + dh_installdocs -A + +override_dh_clean: + dh_clean + find ./ -name "*.pyc" -delete \ No newline at end of file diff --git a/tools/debian/watch b/tools/debian/watch new file mode 100644 index 00000000..85ff1a55 --- /dev/null +++ b/tools/debian/watch @@ -0,0 +1,3 @@ +version=3 +opts=uversionmangle=s/(rc|dev|a|b|c)/~$1/ \ +https://pypi.python.org/packages/source/p/pyqtgraph/pyqtgraph-(.*)\.(?:tar\.gz|zip|tar\.bz2) diff --git a/tools/generateChangelog.py b/tools/generateChangelog.py index 10601b35..3dcd692d 100644 --- a/tools/generateChangelog.py +++ b/tools/generateChangelog.py @@ -76,5 +76,5 @@ if __name__ == '__main__': sys.stderr.write('Usage: generateChangelog.py package_name log_file version "Maintainer "\n') sys.exit(-1) - print generateDebianChangelog(*sys.argv[1:]) + print(generateDebianChangelog(*sys.argv[1:])) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index f1845ce4..5b17069a 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -114,14 +114,13 @@ def getVersionStrings(pkg): return version, forcedVersion, gitVersion, initVersion - from distutils.core import Command import shutil, subprocess from generateChangelog import generateDebianChangelog class DebCommand(Command): description = "build .deb package using `debuild -us -uc`" - maintainer = "Luke " + maintainer = "Luke Campagnola " debTemplate = "tools/debian" debDir = "deb_build" @@ -150,24 +149,30 @@ class DebCommand(Command): # copy sdist to build directory and extract os.mkdir(debDir) renamedSdist = '%s_%s.orig.tar.gz' % (debName, version) + print("copy %s => %s" % (sdist, os.path.join(debDir, renamedSdist))) shutil.copy(sdist, os.path.join(debDir, renamedSdist)) + print("cd %s; tar -xzf %s" % (debDir, renamedSdist)) if os.system("cd %s; tar -xzf %s" % (debDir, renamedSdist)) != 0: raise Exception("Error extracting source distribution.") buildDir = '%s/%s-%s' % (debDir, pkgName, version) # copy debian control structure + print("copytree %s => %s" % (self.debTemplate, buildDir+'/debian')) shutil.copytree(self.debTemplate, buildDir+'/debian') # Write new changelog chlog = generateDebianChangelog(pkgName, 'CHANGELOG', version, self.maintainer) + print("write changelog %s" % buildDir+'/debian/changelog') open(buildDir+'/debian/changelog', 'w').write(chlog) # build package + print('cd %s; debuild -us -uc' % buildDir) if os.system('cd %s; debuild -us -uc' % buildDir) != 0: raise Exception("Error during debuild.") class TestCommand(Command): + """Just for learning about distutils; not for running package tests.""" description = "" user_options = [] def initialize_options(self): @@ -177,6 +182,5 @@ class TestCommand(Command): def run(self): global cmd cmd = self - print self.distribution.name - print self.distribution.version - \ No newline at end of file + print(self.distribution.name) + print(self.distribution.version) From 20b9d079ce39b96616b255bec476142113d2b4b4 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 25 Dec 2013 22:03:01 -0800 Subject: [PATCH 22/37] Nicer range for value histogram of integer images. When an ImageItem's data has an integer dtype, this patch ensures that each bin of the LUT histogram contains the same number of integer values, in order to avoid "spikes" in the histogram that are merely due to some bins covering more integer values than others. This commit needs testing (it was rebased from an old commit). --- pyqtgraph/graphicsItems/ImageItem.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 120312ad..4e29b11c 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -1,3 +1,5 @@ +from __future__ import division + from ..Qt import QtGui, QtCore import numpy as np import collections @@ -295,7 +297,15 @@ class ImageItem(GraphicsObject): if self.image is None: return None,None stepData = self.image[::step, ::step] - hist = np.histogram(stepData, bins=bins) + if not np.iterable(bins): + mn = stepData.min() + mx = stepData.max() + if stepData.dtype.kind in "ui": # unsigned or signed int + # we want max - min to be a multiple of nbins + range = mn, mn + np.ceil((mx - mn) / bins) * bins + else: + range = mn, mx + hist = np.histogram(stepData, bins=bins, range=range) return hist[1][:-1], hist[0] def setPxMode(self, b): From e8dd3e6e5773c92317383e6b7cd74f9684d3c033 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 14 Jan 2014 22:22:50 -0500 Subject: [PATCH 23/37] ImageItem.getHistogram is more clever about constructing histograms: - integer dtype images now have integer-aligned bins, with bin number determined by a target value - step size is automatically chosen based on a target image size - bins and step arguments have default values 'auto' --- pyqtgraph/graphicsItems/ImageItem.py | 44 +++++++++++++++++++++------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 4e29b11c..9e40e325 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -289,23 +289,45 @@ class ImageItem(GraphicsObject): self.render() self.qimage.save(fileName, *args) - def getHistogram(self, bins=500, step=3): + def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds): """Returns x and y arrays containing the histogram values for the current image. - The step argument causes pixels to be skipped when computing the histogram to save time. + For an explanation of the return format, see numpy.histogram(). + + The *step* argument causes pixels to be skipped when computing the histogram to save time. + If *step* is 'auto', then a step is chosen such that the analyzed data has + dimensions roughly *targetImageSize* for each axis. + + The *bins* argument and any extra keyword arguments are passed to + np.histogram(). If *bins* is 'auto', then a bin number is automatically + chosen based on the image characteristics: + + * Integer images will have approximately *targetHistogramSize* bins, + with each bin having an integer width. + * All other types will have *targetHistogramSize* bins. + This method is also used when automatically computing levels. """ if self.image is None: return None,None - stepData = self.image[::step, ::step] - if not np.iterable(bins): - mn = stepData.min() - mx = stepData.max() - if stepData.dtype.kind in "ui": # unsigned or signed int - # we want max - min to be a multiple of nbins - range = mn, mn + np.ceil((mx - mn) / bins) * bins + if step == 'auto': + step = (np.ceil(self.image.shape[0] / targetSize), + np.ceil(self.image.shape[1] / targetSize)) + if np.isscalar(step): + step = (step, step) + stepData = self.image[::step[0], ::step[1]] + + if bins == 'auto': + if stepData.dtype.kind in "ui": + mn = stepData.min() + mx = stepData.max() + step = np.ceil((mx-mn) / 500.) + bins = np.arange(mn, mx+1.01*step, step, dtype=np.int) else: - range = mn, mx - hist = np.histogram(stepData, bins=bins, range=range) + bins = 500 + + kwds['bins'] = bins + hist = np.histogram(stepData, **kwds) + return hist[1][:-1], hist[0] def setPxMode(self, b): From 19293d5b67fc3520482465420a434e3ca1099c8f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 14 Jan 2014 22:29:46 -0500 Subject: [PATCH 24/37] fixed division by zero --- pyqtgraph/functions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 12588cf1..a2c08382 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -821,7 +821,10 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): minVal, maxVal = levels if minVal == maxVal: maxVal += 1e-16 - data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int) + if maxVal == minVal: + data = rescaleData(data, 1, minVal, dtype=int) + else: + data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int) profile() From 6e5e35691c154b17e14d6d232f1d8b9c22583506 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 16 Jan 2014 20:34:05 -0500 Subject: [PATCH 25/37] cleanups --- examples/ScatterPlot.py | 1 + examples/ScatterPlotSpeedTest.py | 5 +- pyqtgraph/functions.py | 2 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 82 ++++++++++------------ 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index c11c782a..72022acc 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -27,6 +27,7 @@ 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: diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py index b79c6641..4dbe57db 100644 --- a/examples/ScatterPlotSpeedTest.py +++ b/examples/ScatterPlotSpeedTest.py @@ -32,6 +32,7 @@ ui.setupUi(win) win.show() p = ui.plot +p.setRange(xRange=[-500, 500], yRange=[-500, 500]) data = np.random.normal(size=(50,500), scale=100) sizeArray = (np.random.random(500) * 20.).astype(int) @@ -45,7 +46,9 @@ def update(): size = sizeArray else: size = ui.sizeSpin.value() - curve = pg.ScatterPlotItem(x=data[ptr%50], y=data[(ptr+1)%50], pen='w', brush='b', size=size, pxMode=ui.pixelModeCheck.isChecked()) + curve = pg.ScatterPlotItem(x=data[ptr%50], y=data[(ptr+1)%50], + pen='w', brush='b', size=size, + pxMode=ui.pixelModeCheck.isChecked()) p.addItem(curve) ptr += 1 now = time() diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index de7da7ed..db01b6b9 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -180,7 +180,7 @@ def mkColor(*args): try: return Colors[c] except KeyError: - raise Exception(err) + raise Exception('No color named "%s"' % c) if len(c) == 3: r = int(c[0]*2, 16) g = int(c[1]*2, 16) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index f671c5e5..2988f613 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -92,15 +92,13 @@ class SymbolAtlas(object): """ def __init__(self): - # symbol key : [x, y, w, h] atlas coordinates + # symbol key : QRect(...) coordinates where symbol can be found in atlas. # 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.symbolPen = weakref.WeakValueDictionary() - self.symbolBrush = weakref.WeakValueDictionary() - self.symbolRectSrc = weakref.WeakValueDictionary() + self.symbolMap = weakref.WeakValueDictionary() self.atlasData = None # numpy array of atlas image self.atlas = None # atlas as QPixmap @@ -111,26 +109,26 @@ class SymbolAtlas(object): """ Given a list of spot records, return an object representing the coordinates of that symbol within the atlas """ - rectSrc = np.empty(len(opts), dtype=object) + sourceRect = np.empty(len(opts), dtype=object) keyi = None - rectSrci = None + sourceRecti = None for i, rec in enumerate(opts): - key = (rec[3], rec[2], id(rec[4]), id(rec[5])) + key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? if key == keyi: - rectSrc[i] = rectSrci + sourceRect[i] = sourceRecti else: try: - rectSrc[i] = self.symbolRectSrc[key] + sourceRect[i] = self.symbolMap[key] except KeyError: newRectSrc = QtCore.QRectF() - self.symbolPen[key] = rec['pen'] - self.symbolBrush[key] = rec['brush'] - self.symbolRectSrc[key] = newRectSrc + newRectSrc.pen = rec['pen'] + newRectSrc.brush = rec['brush'] + self.symbolMap[key] = newRectSrc self.atlasValid = False - rectSrc[i] = self.symbolRectSrc[key] + sourceRect[i] = newRectSrc keyi = key - rectSrci = self.symbolRectSrc[key] - return rectSrc + sourceRecti = newRectSrc + return sourceRect def buildAtlas(self): # get rendered array for all symbols, keep track of avg/max width @@ -138,15 +136,13 @@ class SymbolAtlas(object): avgWidth = 0.0 maxWidth = 0 images = [] - 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) + for key, sourceRect in self.symbolMap.items(): + if sourceRect.width() == 0: + img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.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: - (y,x,h,w) = rectSrc.getRect() + (y,x,h,w) = sourceRect.getRect() arr = self.atlasData[x:x+w, y:y+w] rendered[key] = arr w = arr.shape[0] @@ -177,18 +173,18 @@ class SymbolAtlas(object): x = 0 rowheight = h self.atlasRows.append([y, rowheight, 0]) - self.symbolRectSrc[key].setRect(y, x, h, w) + self.symbolMap[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: - y, x, h, w = self.symbolRectSrc[key].getRect() + y, x, h, w = self.symbolMap[key].getRect() self.atlasData[x:x+w, y:y+h] = rendered[key] self.atlas = None self.atlasValid = True - self.max_width=maxWidth + self.max_width = maxWidth def getAtlas(self): if not self.atlasValid: @@ -236,7 +232,7 @@ class ScatterPlotItem(GraphicsObject): 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), ('item', object), ('rectSrc', object), ('rectTarg', object), ('width', float)]) + self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', 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 @@ -247,8 +243,8 @@ class ScatterPlotItem(GraphicsObject): 'name': None, } - self.setPen('l', update=False) - self.setBrush('s', update=False) + self.setPen(fn.mkPen(getConfigOption('foreground')), update=False) + self.setBrush(fn.mkBrush(100,100,150), update=False) self.setSymbol('o', update=False) self.setSize(7, update=False) profiler() @@ -445,7 +441,7 @@ class ScatterPlotItem(GraphicsObject): else: self.opts['pen'] = fn.mkPen(*args, **kargs) - dataSet['rectSrc'] = None + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) @@ -470,7 +466,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['brush'] = fn.mkBrush(*args, **kargs) #self._spotPixmap = None - dataSet['rectSrc'] = None + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) @@ -493,7 +489,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['symbol'] = symbol self._spotPixmap = None - dataSet['rectSrc'] = None + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) @@ -516,7 +512,7 @@ class ScatterPlotItem(GraphicsObject): self.opts['size'] = size self._spotPixmap = None - dataSet['rectSrc'] = None + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) @@ -551,12 +547,12 @@ class ScatterPlotItem(GraphicsObject): invalidate = False if self.opts['pxMode']: - mask = np.equal(dataSet['rectSrc'], None) + mask = np.equal(dataSet['sourceRect'], None) if np.any(mask): invalidate = True opts = self.getSpotOpts(dataSet[mask]) - rectSrc = self.fragmentAtlas.getSymbolCoords(opts) - dataSet['rectSrc'][mask] = rectSrc + sourceRect = self.fragmentAtlas.getSymbolCoords(opts) + dataSet['sourceRect'][mask] = sourceRect #for rec in dataSet: @@ -564,9 +560,9 @@ class ScatterPlotItem(GraphicsObject): #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 + dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2 + dataSet['targetRect'] = 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 @@ -723,7 +719,7 @@ class ScatterPlotItem(GraphicsObject): pts[1] = data['y'] pts = fn.transformCoordinates(tr, pts) pts -= data['width'] - pts = np.clip(pts, -2**30, 2**30) + pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. return data, pts @debug.warnOnException ## raising an exception here causes crash @@ -750,12 +746,12 @@ class ScatterPlotItem(GraphicsObject): 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'] + list(imap(QtCore.QRectF.moveTo, data['targetRect'], pts[0,:], pts[1,:])) + self.target = data['targetRect'] if USE_PYSIDE: - list(imap(p.drawPixmap, self.target, repeat(atlas), data['rectSrc'])) + list(imap(p.drawPixmap, self.target, repeat(atlas), data['sourceRect'])) else: - p.drawPixmapFragments(self.target.tolist(), data['rectSrc'].tolist(), atlas) + p.drawPixmapFragments(self.target.tolist(), data['sourceRect'].tolist(), atlas) else: p.setRenderHint(p.Antialiasing, aa) @@ -924,7 +920,7 @@ class SpotItem(object): self._data['data'] = data def updateItem(self): - self._data['rectSrc'] = None + self._data['sourceRect'] = None self._plot.updateSpots(self._data.reshape(1)) self._plot.invalidate() From a381c61c0c81a11f7cf2f5b3a3a6d896b55d2791 Mon Sep 17 00:00:00 2001 From: John Lund Date: Fri, 17 Jan 2014 08:59:59 -0600 Subject: [PATCH 26/37] LegendItem bugfix drawSymbol is module-level funct, not class method --- pyqtgraph/graphicsItems/LegendItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index a1228789..ba6a6897 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -3,7 +3,7 @@ from .LabelItem import LabelItem from ..Qt import QtGui, QtCore from .. import functions as fn from ..Point import Point -from .ScatterPlotItem import ScatterPlotItem +from .ScatterPlotItem import ScatterPlotItem, drawSymbol from .PlotDataItem import PlotDataItem from .GraphicsWidgetAnchor import GraphicsWidgetAnchor __all__ = ['LegendItem'] @@ -167,7 +167,7 @@ class ItemSample(GraphicsWidget): size = opts['size'] p.translate(10,10) - path = ScatterPlotItem.drawSymbol(p, symbol, size, pen, brush) + path = drawSymbol(p, symbol, size, pen, brush) From b813ecabc3db82a733750051eec9b067b7ae1de2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 17 Jan 2014 18:31:36 -0500 Subject: [PATCH 27/37] cleanup; corrected view clipping. --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 41 ++++++++++------------ 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 2988f613..8de985fc 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -693,34 +693,31 @@ class ScatterPlotItem(GraphicsObject): def getTransformedPoint(self): + # Map point locations to device + + vb = self.getViewBox() + if vb is None: + return None, None 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 = np.empty((2,len(self.data['x']))) + pts[0] = self.data['x'] + pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) - pts -= data['width'] + pts -= self.data['width'] pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. - return data, pts + + ## Remove out of view points + viewBounds = vb.mapRectToDevice(vb.boundingRect()) + w = self.data['width'] + mask = ((pts[0] + w > viewBounds.left()) & + (pts[0] - w < viewBounds.right()) & + (pts[1] + w > viewBounds.top()) & + (pts[1] - w < viewBounds.bottom())) ## remove out of view points + print np.sum(mask) + return self.data[mask], pts[:, mask] @debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): From c9c2160856238d8060b24897c2e7397002ae3a6d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 18 Jan 2014 19:13:39 -0500 Subject: [PATCH 28/37] more cleanups added simple test --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 85 ++++++++++--------- .../graphicsItems/tests/ScatterPlotItem.py | 23 +++++ 2 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 pyqtgraph/graphicsItems/tests/ScatterPlotItem.py diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 8de985fc..1c11fcf9 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -228,8 +228,6 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.__init__(self) 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), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)]) @@ -394,6 +392,7 @@ class ScatterPlotItem(GraphicsObject): self.setPointData(kargs['data'], dataSet=newData) self.prepareGeometryChange() + self.informViewBoundsChanged() self.bounds = [None, None] self.invalidate() self.updateSpots(newData) @@ -402,13 +401,10 @@ class ScatterPlotItem(GraphicsObject): def invalidate(self): ## clear any cached drawing state self.picture = None - self.fragments = None - self.target = None self.update() def getData(self): - return self.data['x'], self.data['y'] - + return self.data['x'], self.data['y'] def setPoints(self, *args, **kargs): ##Deprecated; use setData @@ -554,14 +550,10 @@ class ScatterPlotItem(GraphicsObject): sourceRect = self.fragmentAtlas.getSymbolCoords(opts) dataSet['sourceRect'][mask] = sourceRect - - #for rec in dataSet: - #if rec['fragCoords'] is None: - #invalidate = True - #rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec)) - self.fragmentAtlas.getAtlas() + self.fragmentAtlas.getAtlas() # generate atlas so source widths are available. + dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2 - dataSet['targetRect'] = list(imap(QtCore.QRectF, repeat(0), repeat(0), dataSet['width']*2, dataSet['width']*2)) + dataSet['targetRect'] = None self._maxSpotPxWidth = self.fragmentAtlas.max_width else: self._maxSpotWidth = 0 @@ -684,40 +676,42 @@ class ScatterPlotItem(GraphicsObject): self.prepareGeometryChange() GraphicsObject.viewTransformChanged(self) self.bounds = [None, None] - self.fragments = None - self.target = None + self.data['targetRect'] = None def setExportMode(self, *args, **kwds): GraphicsObject.setExportMode(self, *args, **kwds) self.invalidate() - def getTransformedPoint(self): - # Map point locations to device - - vb = self.getViewBox() - if vb is None: - return None, None + def mapPointsToDevice(self, pts): + # Map point locations to device tr = self.deviceTransform() if tr is None: - return None, None + return None - pts = np.empty((2,len(self.data['x']))) - pts[0] = self.data['x'] - pts[1] = self.data['y'] + #pts = np.empty((2,len(self.data['x']))) + #pts[0] = self.data['x'] + #pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) pts -= self.data['width'] pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. - ## Remove out of view points + return pts + + def getViewMask(self, pts): + # Return bool mask indicating all points that are within viewbox + # pts is expressed in *device coordiantes* + vb = self.getViewBox() + if vb is None: + return None viewBounds = vb.mapRectToDevice(vb.boundingRect()) w = self.data['width'] mask = ((pts[0] + w > viewBounds.left()) & (pts[0] - w < viewBounds.right()) & (pts[1] + w > viewBounds.top()) & (pts[1] - w < viewBounds.bottom())) ## remove out of view points - print np.sum(mask) - return self.data[mask], pts[:, mask] + return mask + @debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): @@ -733,27 +727,42 @@ class ScatterPlotItem(GraphicsObject): scale = 1.0 if self.opts['pxMode'] is True: - atlas = self.fragmentAtlas.getAtlas() p.resetTransform() - data, pts = self.getTransformedPoint() - if data is None: + # Map point coordinates to device + pts = np.vstack([self.data['x'], self.data['y']]) + pts = self.mapPointsToDevice(pts) + if pts is None: return + # Cull points that are outside view + viewMask = self.getViewMask(pts) + #pts = pts[:,mask] + #data = self.data[mask] + if self.opts['useCache'] and self._exportOpts is False: + # Draw symbols from pre-rendered atlas + atlas = self.fragmentAtlas.getAtlas() - if self.target == None: - list(imap(QtCore.QRectF.moveTo, data['targetRect'], pts[0,:], pts[1,:])) - self.target = data['targetRect'] + # Update targetRects if necessary + updateMask = viewMask & np.equal(self.data['targetRect'], None) + if np.any(updateMask): + updatePts = pts[:,updateMask] + width = self.data[updateMask]['width']*2 + self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) + + data = self.data[viewMask] if USE_PYSIDE: - list(imap(p.drawPixmap, self.target, repeat(atlas), data['sourceRect'])) + list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) else: - p.drawPixmapFragments(self.target.tolist(), data['sourceRect'].tolist(), atlas) + p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas) else: + # render each symbol individually p.setRenderHint(p.Antialiasing, aa) - for i in range(len(self.data)): - rec = data[i] + data = self.data[viewMask] + pts = pts[:,viewMask] + for i, rec in enumerate(data): p.resetTransform() p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']) drawSymbol(p, *self.getSpotOpts(rec, scale)) diff --git a/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py new file mode 100644 index 00000000..ef8271bf --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/ScatterPlotItem.py @@ -0,0 +1,23 @@ +import pyqtgraph as pg +import numpy as np +app = pg.mkQApp() +plot = pg.plot() +app.processEvents() + +# set view range equal to its bounding rect. +# This causes plots to look the same regardless of pxMode. +plot.setRange(rect=plot.boundingRect()) + + +def test_modes(): + for i, pxMode in enumerate([True, False]): + for j, useCache in enumerate([True, False]): + s = pg.ScatterPlotItem() + s.opts['useCache'] = useCache + plot.addItem(s) + s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode) + s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30]) + + +if __name__ == '__main__': + test_modes() From b3e07a0e0acd17da00bd34dd08b57729000aa412 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 18 Jan 2014 19:59:36 -0500 Subject: [PATCH 29/37] ImageItem fix against a51c30d --- pyqtgraph/graphicsItems/ImageItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 9e40e325..7c80859d 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -310,8 +310,8 @@ class ImageItem(GraphicsObject): if self.image is None: return None,None if step == 'auto': - step = (np.ceil(self.image.shape[0] / targetSize), - np.ceil(self.image.shape[1] / targetSize)) + step = (np.ceil(self.image.shape[0] / targetImageSize), + np.ceil(self.image.shape[1] / targetImageSize)) if np.isscalar(step): step = (step, step) stepData = self.image[::step[0], ::step[1]] From eae32af0c7113a85262eb86a25bb554231a15807 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 18 Jan 2014 23:30:03 -0500 Subject: [PATCH 30/37] Added symbol to Legend example --- examples/Legend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Legend.py b/examples/Legend.py index 2cd982ea..f7841151 100644 --- a/examples/Legend.py +++ b/examples/Legend.py @@ -14,7 +14,7 @@ plt.addLegend() #l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset) #l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case -c1 = plt.plot([1,3,2,4], pen='r', name='red plot') +c1 = plt.plot([1,3,2,4], pen='r', symbol='o', symbolPen='r', symbolBrush=0.5, name='red plot') c2 = plt.plot([2,1,4,3], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='green plot') #l.addItem(c1, 'red plot') #l.addItem(c2, 'green plot') From ca68f05f1f3af76059504793f4695856f991f90b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 19 Jan 2014 08:37:58 -0500 Subject: [PATCH 31/37] Fix: PlotCurveItem now ignores clip-to-view when auto-range is enabled. --- CHANGELOG | 1 + pyqtgraph/graphicsItems/PlotDataItem.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 77a7509a..3db12cb9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -41,6 +41,7 @@ pyqtgraph-0.9.9 [unreleased] - PolyLineROI.setPen() now changes the pen of its segments as well - Prevent divide-by-zero in AxisItem - Major speedup when using ScatterPlotItem in pxMode + - PlotCurveItem ignores clip-to-view when auto range is enabled pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index e8c4145c..c1f9fd6a 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -531,15 +531,17 @@ class PlotDataItem(GraphicsObject): ## downsampling is expensive; delay until after clipping. if self.opts['clipToView']: - # this option presumes that x-values have uniform spacing - range = self.viewRect() - if range is not None: - dx = float(x[-1]-x[0]) / (len(x)-1) - # clip to visible region extended by downsampling value - x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1) - x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1) - x = x[x0:x1] - y = y[x0:x1] + view = self.getViewBox() + if view is None or not view.autoRangeEnabled()[0]: + # this option presumes that x-values have uniform spacing + range = self.viewRect() + if range is not None: + dx = float(x[-1]-x[0]) / (len(x)-1) + # clip to visible region extended by downsampling value + x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1) + x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1) + x = x[x0:x1] + y = y[x0:x1] if ds > 1: if self.opts['downsampleMethod'] == 'subsample': From 2f1cb26549d70b713155427f4d43ae0a59e5e311 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 19 Jan 2014 21:14:49 -0500 Subject: [PATCH 32/37] remove tools/debian; will add a new branch with debian/ at the root. --- tools/debian/compat | 1 - tools/debian/control | 49 -------------------------- tools/debian/copyright | 10 ------ tools/debian/python-pyqtgraph.install | 1 - tools/debian/python3-pyqtgraph.install | 1 - tools/debian/rules | 15 -------- tools/debian/source/format | 1 - tools/debian/watch | 3 -- 8 files changed, 81 deletions(-) delete mode 100644 tools/debian/compat delete mode 100644 tools/debian/control delete mode 100644 tools/debian/copyright delete mode 100644 tools/debian/python-pyqtgraph.install delete mode 100644 tools/debian/python3-pyqtgraph.install delete mode 100755 tools/debian/rules delete mode 100644 tools/debian/source/format delete mode 100644 tools/debian/watch diff --git a/tools/debian/compat b/tools/debian/compat deleted file mode 100644 index 45a4fb75..00000000 --- a/tools/debian/compat +++ /dev/null @@ -1 +0,0 @@ -8 diff --git a/tools/debian/control b/tools/debian/control deleted file mode 100644 index a516920d..00000000 --- a/tools/debian/control +++ /dev/null @@ -1,49 +0,0 @@ -Source: python-pyqtgraph -Maintainer: Luke Campagnola -Section: python -Priority: optional -Standards-Version: 3.9.4 -Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools, python3-all, python3-setuptools, python-docutils, python-sphinx (>= 1.0.7+dfsg-1~) -X-Python-Version: >= 2.6 -X-Python3-Version: >= 3.2 - -Package: python-pyqtgraph -Architecture: all -Homepage: http://www.pyqtgraph.org -Depends: python-qt4 | python-pyside, python-scipy, python-numpy, ${python:Depends}, ${misc:Depends} -Suggests: python-pyqtgraph-doc, python-opengl, python-qt4-gl -Description: Scientific Graphics and GUI Library (Python 2) - PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. - It is intended for use in mathematics / scientific / engineering applications. - Despite being written entirely in python, the library is very fast due to its - heavy leverage of numpy for number crunching and Qt's GraphicsView framework - for fast display. - . - This is the Python 2 version of the package. - -Package: python3-pyqtgraph -Architecture: all -Homepage: http://www.pyqtgraph.org -Depends: python-qt4 | python-pyside, python-scipy, python-numpy, ${python3:Depends}, ${misc:Depends} -Suggests: python-pyqtgraph-doc, python-opengl, python-qt4-gl -Description: Scientific Graphics and GUI Library (Python 3) - PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. - It is intended for use in mathematics / scientific / engineering applications. - Despite being written entirely in python, the library is very fast due to its - heavy leverage of numpy for number crunching and Qt's GraphicsView framework - for fast display. - . - This is the Python 3 version of the package. - -Package: python-pyqtgraph-doc -Architecture: all -Section: doc -Depends: ${sphinxdoc:Depends}, ${misc:Depends} -Description: Scientific Graphics and GUI Library (common documentation) - PyQtGraph is a pure-python graphics and GUI library built on PyQt4 and numpy. - It is intended for use in mathematics / scientific / engineering applications. - Despite being written entirely in python, the library is very fast due to its - heavy leverage of numpy for number crunching and Qt's GraphicsView framework - for fast display. - . - This is the common documentation package. diff --git a/tools/debian/copyright b/tools/debian/copyright deleted file mode 100644 index 22791ae3..00000000 --- a/tools/debian/copyright +++ /dev/null @@ -1,10 +0,0 @@ -Copyright (c) 2012 University of North Carolina at Chapel Hill -Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') - -The MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/tools/debian/python-pyqtgraph.install b/tools/debian/python-pyqtgraph.install deleted file mode 100644 index b2cc1360..00000000 --- a/tools/debian/python-pyqtgraph.install +++ /dev/null @@ -1 +0,0 @@ -usr/lib/python2* diff --git a/tools/debian/python3-pyqtgraph.install b/tools/debian/python3-pyqtgraph.install deleted file mode 100644 index 4606faae..00000000 --- a/tools/debian/python3-pyqtgraph.install +++ /dev/null @@ -1 +0,0 @@ -usr/lib/python3* diff --git a/tools/debian/rules b/tools/debian/rules deleted file mode 100755 index 3132fbfd..00000000 --- a/tools/debian/rules +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/make -f -#export DH_VERBOSE=1 -export PYBUILD_NAME=pyqtgraph - -%: - dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild - -override_dh_installdocs: - PYTHONPATH=`pwd` make -C doc html - dh_installdocs -ppython-pyqtgraph-doc doc/build/html - dh_installdocs -A - -override_dh_clean: - dh_clean - find ./ -name "*.pyc" -delete \ No newline at end of file diff --git a/tools/debian/source/format b/tools/debian/source/format deleted file mode 100644 index 163aaf8d..00000000 --- a/tools/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (quilt) diff --git a/tools/debian/watch b/tools/debian/watch deleted file mode 100644 index 85ff1a55..00000000 --- a/tools/debian/watch +++ /dev/null @@ -1,3 +0,0 @@ -version=3 -opts=uversionmangle=s/(rc|dev|a|b|c)/~$1/ \ -https://pypi.python.org/packages/source/p/pyqtgraph/pyqtgraph-(.*)\.(?:tar\.gz|zip|tar\.bz2) From d81998461fbe4540daa2fd1025d61553ab7d25d7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 22 Jan 2014 14:23:10 -0500 Subject: [PATCH 33/37] copy setHelpers.py changes from debian/ branch --- tools/setupHelpers.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index 5b17069a..9e6be1d5 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -49,7 +49,7 @@ def getGitVersion(tagPrefix): lastTag = gitCommit(lastTagName) head = gitCommit('HEAD') if head != lastTag: - branch = re.search(r'\* (.*)', check_output(['git', 'branch'], universal_newlines=True)).group(1) + branch = getGitBranch() gitVersion = gitVersion + "-%s-%s" % (branch, head[:10]) # any uncommitted modifications? @@ -65,6 +65,13 @@ def getGitVersion(tagPrefix): return gitVersion +def getGitBranch(): + m = re.search(r'\* (.*)', check_output(['git', 'branch'], universal_newlines=True)) + if m is None: + return '' + else: + return m.group(1) + def getVersionStrings(pkg): """ Returns 4 version strings: @@ -102,10 +109,11 @@ def getVersionStrings(pkg): forcedVersion = sys.argv[i].replace('--force-version=', '') sys.argv.pop(i) + ## Finally decide on a version string to use: if forcedVersion is not None: version = forcedVersion - elif gitVersion is not None: + elif gitVersion is not None and getGitBranch() != 'debian': # ignore git version if this is debian branch version = gitVersion sys.stderr.write("Detected git commit; will use version string: '%s'\n" % version) else: @@ -121,7 +129,7 @@ from generateChangelog import generateDebianChangelog class DebCommand(Command): description = "build .deb package using `debuild -us -uc`" maintainer = "Luke Campagnola " - debTemplate = "tools/debian" + debTemplate = "debian" debDir = "deb_build" user_options = [] From 23779f004eb85ee448de11d9b431b55a009f2410 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 23 Jan 2014 10:34:26 -0500 Subject: [PATCH 34/37] - Fixed FillBetweenItem to force PlotCurveItem to generate path - Added FillBetweenItem.setCurves() - Added FillBetweenItem example --- CHANGELOG | 2 + examples/__main__.py | 1 + pyqtgraph/graphicsItems/FillBetweenItem.py | 64 +++++++++++++++++++--- pyqtgraph/graphicsItems/PlotDataItem.py | 2 +- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3db12cb9..4888d8d4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ pyqtgraph-0.9.9 [unreleased] - Get, set current value - Added Flowchart.sigChartChanged - ImageItem.getHistogram is more clever about constructing histograms + - Added FillBetweenItem.setCurves() Bugfixes: - PlotCurveItem now has correct clicking behavior--clicks within a few px @@ -42,6 +43,7 @@ pyqtgraph-0.9.9 [unreleased] - Prevent divide-by-zero in AxisItem - Major speedup when using ScatterPlotItem in pxMode - PlotCurveItem ignores clip-to-view when auto range is enabled + - FillBetweenItem now forces PlotCurveItem to generate path pyqtgraph-0.9.8 2013-11-24 diff --git a/examples/__main__.py b/examples/__main__.py index 7d75e36a..e7dbe5eb 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -37,6 +37,7 @@ examples = OrderedDict([ ('IsocurveItem', 'isocurve.py'), ('GraphItem', 'GraphItem.py'), ('ErrorBarItem', 'ErrorBarItem.py'), + ('FillBetweenItem', 'FillBetweenItem.py'), ('ImageItem - video', 'ImageItem.py'), ('ImageItem - draw', 'Draw.py'), ('Region-of-Interest', 'ROIExamples.py'), diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py index 13e5fa6b..3cf33acd 100644 --- a/pyqtgraph/graphicsItems/FillBetweenItem.py +++ b/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -1,24 +1,70 @@ from ..Qt import QtGui from .. import functions as fn +from .PlotDataItem import PlotDataItem +from .PlotCurveItem import PlotCurveItem class FillBetweenItem(QtGui.QGraphicsPathItem): """ GraphicsItem filling the space between two PlotDataItems. """ - def __init__(self, p1, p2, brush=None): + def __init__(self, curve1=None, curve2=None, brush=None): QtGui.QGraphicsPathItem.__init__(self) - self.p1 = p1 - self.p2 = p2 - p1.sigPlotChanged.connect(self.updatePath) - p2.sigPlotChanged.connect(self.updatePath) + self.curves = None + if curve1 is not None and curve2 is not None: + self.setCurves(curve1, curve2) + elif curve1 is not None or curve2 is not None: + raise Exception("Must specify two curves to fill between.") + if brush is not None: self.setBrush(fn.mkBrush(brush)) - self.setZValue(min(p1.zValue(), p2.zValue())-1) + self.updatePath() + + def setCurves(self, curve1, curve2): + """Set the curves to fill between. + + Arguments must be instances of PlotDataItem or PlotCurveItem.""" + + if self.curves is not None: + for c in self.curves: + try: + c.sigPlotChanged.disconnect(self.curveChanged) + except TypeError: + pass + + curves = [curve1, curve2] + for c in curves: + if not isinstance(c, PlotDataItem) and not isinstance(c, PlotCurveItem): + raise TypeError("Curves must be PlotDataItem or PlotCurveItem.") + self.curves = curves + curve1.sigPlotChanged.connect(self.curveChanged) + curve2.sigPlotChanged.connect(self.curveChanged) + self.setZValue(min(curve1.zValue(), curve2.zValue())-1) + self.curveChanged() + + def setBrush(self, *args, **kwds): + """Change the fill brush. Acceps the same arguments as pg.mkBrush()""" + QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds)) + + def curveChanged(self): self.updatePath() def updatePath(self): - p1 = self.p1.curve.path - p2 = self.p2.curve.path + if self.curves is None: + self.setPath(QtGui.QPainterPath()) + return + paths = [] + for c in self.curves: + if isinstance(c, PlotDataItem): + paths.append(c.curve.getPath()) + elif isinstance(c, PlotCurveItem): + paths.append(c.getPath()) + path = QtGui.QPainterPath() - path.addPolygon(p1.toSubpathPolygons()[0] + p2.toReversed().toSubpathPolygons()[0]) + p1 = paths[0].toSubpathPolygons() + p2 = paths[1].toReversed().toSubpathPolygons() + if len(p1) == 0 or len(p2) == 0: + self.setPath(QtGui.QPainterPath()) + return + + path.addPolygon(p1[0] + p2[0]) self.setPath(path) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index c1f9fd6a..8baab719 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -15,7 +15,7 @@ class PlotDataItem(GraphicsObject): GraphicsItem for displaying plot curves, scatter plots, or both. While it is possible to use :class:`PlotCurveItem ` or :class:`ScatterPlotItem ` individually, this class - provides a unified interface to both. Inspances of :class:`PlotDataItem` are + provides a unified interface to both. Instances of :class:`PlotDataItem` are usually created by plot() methods such as :func:`pyqtgraph.plot` and :func:`PlotItem.plot() `. From 529e9aaaff656456c20c9da6b55a0232f89bc135 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 23 Jan 2014 13:25:00 -0500 Subject: [PATCH 35/37] py2.6 fix in setupHelpers --- tools/setupHelpers.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index 9e6be1d5..ea6aba3f 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -1,5 +1,16 @@ import os, sys, re -from subprocess import check_output +try: + from subprocess import check_output +except ImportError: + import subprocess as sp + def check_output(*args, **kwds): + kwds['stdout'] = sp.PIPE + proc = sp.Popen(*args, **kwds) + output = proc.stdout.read() + proc.wait() + if proc.returncode != 0: + raise Exception("Process had nonzero return value", proc.returncode) + return output def listAllPackages(pkgroot): path = os.getcwd() From 2f6bd8de3713759612a4e7b05d4b5025d105306c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 23 Jan 2014 13:28:30 -0500 Subject: [PATCH 36/37] quiet setup.py warning about install_requires --- setup.py | 5 +++++ tools/setupHelpers.py | 1 + 2 files changed, 6 insertions(+) diff --git a/setup.py b/setup.py index 16d66f61..7f2db6bf 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,11 @@ setupOpts = dict( from distutils.core import setup import distutils.dir_util import os, sys, re +try: + # just avoids warning about install_requires + import setuptools +except ImportError: + pass path = os.path.split(__file__)[0] sys.path.insert(0, os.path.join(path, 'tools')) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index ea6aba3f..ed13388e 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -2,6 +2,7 @@ import os, sys, re try: from subprocess import check_output except ImportError: + print "fake check_output" import subprocess as sp def check_output(*args, **kwds): kwds['stdout'] = sp.PIPE From ba00ce530feab798779a0bde01bc645251a944e6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 23 Jan 2014 13:32:20 -0500 Subject: [PATCH 37/37] remove print --- tools/setupHelpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index ed13388e..ea6aba3f 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -2,7 +2,6 @@ import os, sys, re try: from subprocess import check_output except ImportError: - print "fake check_output" import subprocess as sp def check_output(*args, **kwds): kwds['stdout'] = sp.PIPE